ui: Allow text selection of clickable elements and their contents (#5770)
* ui: Allow text selection of clickable elements and their contents This commit disables a click on mousedown be removing the `href` attribute and moving it to a `data-href` attribute. On mouseup it will only move it back if there is no selection. This means that an anchor will only be followed on click _if_ there is no selection. This fixes the fact that whenever you select some copy within a clickable element it immediately throws you into the linked page when you release your mouse. Further notes: We use the `isCollapsed` property here which 'seems' to be classed as 'experimental' in one place where I researched it: https://developer.mozilla.org/en-US/docs/Web/API/Selection/isCollapsed Although in others it makes no mention of this 'experimental' e.g: - https://webplatform.github.io/docs/dom/Selection/isCollapsed/ - https://w3c.github.io/selection-api/#dom-selection-iscollapsed I may have gone a little overboard in feature detection for this, but I conscious of that fact that if `isCollapsed` doesn't exist at some point in the future (something that seems unlikely). The code here will have no effect on the UI. But I'd specifically like a second pair of eyes on that. * ui: Don't break right click, detects a secondary click on mousedown * ui: Put anchor selection capability behind an ENV var
This commit is contained in:
parent
f9710c2c92
commit
466c3c6899
|
@ -0,0 +1,62 @@
|
||||||
|
import env from 'consul-ui/env';
|
||||||
|
|
||||||
|
const SECONDARY_BUTTON = 2;
|
||||||
|
const isSelecting = function(win = window) {
|
||||||
|
const selection = win.getSelection();
|
||||||
|
let selecting = false;
|
||||||
|
try {
|
||||||
|
selecting = 'isCollapsed' in selection && !selection.isCollapsed;
|
||||||
|
} catch (e) {
|
||||||
|
// passthrough
|
||||||
|
}
|
||||||
|
return selecting;
|
||||||
|
};
|
||||||
|
export default {
|
||||||
|
name: 'selection',
|
||||||
|
initialize(container) {
|
||||||
|
if (env('CONSUL_UI_DISABLE_ANCHOR_SELECTION')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dom = container.lookup('service:dom');
|
||||||
|
const findAnchor = function(el) {
|
||||||
|
return el.tagName === 'A' ? el : dom.closest('a', el);
|
||||||
|
};
|
||||||
|
const mousedown = function(e) {
|
||||||
|
const $a = findAnchor(e.target);
|
||||||
|
if ($a) {
|
||||||
|
if (typeof e.button !== 'undefined' && e.button === SECONDARY_BUTTON) {
|
||||||
|
const dataHref = $a.dataset.href;
|
||||||
|
if (dataHref) {
|
||||||
|
$a.setAttribute('href', dataHref);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const href = $a.getAttribute('href');
|
||||||
|
if (href) {
|
||||||
|
$a.dataset.href = href;
|
||||||
|
$a.removeAttribute('href');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mouseup = function(e) {
|
||||||
|
const $a = findAnchor(e.target);
|
||||||
|
if ($a) {
|
||||||
|
const href = $a.dataset.href;
|
||||||
|
if (!isSelecting() && href) {
|
||||||
|
$a.setAttribute('href', href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener('mousedown', mousedown);
|
||||||
|
document.body.addEventListener('mouseup', mouseup);
|
||||||
|
|
||||||
|
container.reopen({
|
||||||
|
willDestroy: function() {
|
||||||
|
document.body.removeEventListener('mousedown', mousedown);
|
||||||
|
document.body.removeEventListener('mouseup', mouseup);
|
||||||
|
return this._super(...arguments);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,9 +1,15 @@
|
||||||
const clickEvent = function() {
|
const clickEvent = function($el) {
|
||||||
return new MouseEvent('click', {
|
['mousedown', 'mouseup', 'click']
|
||||||
bubbles: true,
|
.map(function(type) {
|
||||||
cancelable: true,
|
return new MouseEvent(type, {
|
||||||
view: window,
|
bubbles: true,
|
||||||
});
|
cancelable: true,
|
||||||
|
view: window,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.forEach(function(event) {
|
||||||
|
$el.dispatchEvent(event);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
export default function(closest, click = clickEvent) {
|
export default function(closest, click = clickEvent) {
|
||||||
// TODO: Decide whether we should use `e` for ease
|
// TODO: Decide whether we should use `e` for ease
|
||||||
|
@ -24,7 +30,7 @@ export default function(closest, click = clickEvent) {
|
||||||
// closest should probably be relaced with a finder function
|
// closest should probably be relaced with a finder function
|
||||||
const $a = closest('tr', e.target).querySelector('a');
|
const $a = closest('tr', e.target).querySelector('a');
|
||||||
if ($a) {
|
if ($a) {
|
||||||
$a.dispatchEvent(click());
|
click($a);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,10 @@ module.exports = function(environment) {
|
||||||
};
|
};
|
||||||
// TODO: These should probably go onto APP
|
// TODO: These should probably go onto APP
|
||||||
ENV = Object.assign({}, ENV, {
|
ENV = Object.assign({}, ENV, {
|
||||||
|
// TODO: Let people alter this, as with anchor selection
|
||||||
CONSUL_UI_DISABLE_REALTIME: false,
|
CONSUL_UI_DISABLE_REALTIME: false,
|
||||||
|
CONSUL_UI_DISABLE_ANCHOR_SELECTION:
|
||||||
|
typeof process.env.CONSUL_UI_DISABLE_ANCHOR_SELECTION !== 'undefined',
|
||||||
CONSUL_GIT_SHA: (function() {
|
CONSUL_GIT_SHA: (function() {
|
||||||
if (process.env.CONSUL_GIT_SHA) {
|
if (process.env.CONSUL_GIT_SHA) {
|
||||||
return process.env.CONSUL_GIT_SHA;
|
return process.env.CONSUL_GIT_SHA;
|
||||||
|
|
|
@ -39,16 +39,16 @@ test("it does nothing if an anchor isn't found", function(assert) {
|
||||||
});
|
});
|
||||||
assert.equal(actual, expected);
|
assert.equal(actual, expected);
|
||||||
});
|
});
|
||||||
test('it dispatches the result of `click` if an anchor is found', function(assert) {
|
test('it dispatches the result of `mouseup`, `mousedown`, `click` if an anchor is found', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(3);
|
||||||
const expected = 'click';
|
const expected = ['mousedown', 'mouseup', 'click'];
|
||||||
const closest = function() {
|
const closest = function() {
|
||||||
return {
|
return {
|
||||||
querySelector: function() {
|
querySelector: function() {
|
||||||
return {
|
return {
|
||||||
dispatchEvent: function(ev) {
|
dispatchEvent: function(ev) {
|
||||||
const actual = ev.type;
|
const actual = ev.type;
|
||||||
assert.equal(actual, expected);
|
assert.equal(actual, expected.shift());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue