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() {
|
||||
return new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
});
|
||||
const clickEvent = function($el) {
|
||||
['mousedown', 'mouseup', 'click']
|
||||
.map(function(type) {
|
||||
return new MouseEvent(type, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
});
|
||||
})
|
||||
.forEach(function(event) {
|
||||
$el.dispatchEvent(event);
|
||||
});
|
||||
};
|
||||
export default function(closest, click = clickEvent) {
|
||||
// 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
|
||||
const $a = closest('tr', e.target).querySelector('a');
|
||||
if ($a) {
|
||||
$a.dispatchEvent(click());
|
||||
click($a);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -29,7 +29,10 @@ module.exports = function(environment) {
|
|||
};
|
||||
// TODO: These should probably go onto APP
|
||||
ENV = Object.assign({}, ENV, {
|
||||
// TODO: Let people alter this, as with anchor selection
|
||||
CONSUL_UI_DISABLE_REALTIME: false,
|
||||
CONSUL_UI_DISABLE_ANCHOR_SELECTION:
|
||||
typeof process.env.CONSUL_UI_DISABLE_ANCHOR_SELECTION !== 'undefined',
|
||||
CONSUL_GIT_SHA: (function() {
|
||||
if (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);
|
||||
});
|
||||
test('it dispatches the result of `click` if an anchor is found', function(assert) {
|
||||
assert.expect(1);
|
||||
const expected = 'click';
|
||||
test('it dispatches the result of `mouseup`, `mousedown`, `click` if an anchor is found', function(assert) {
|
||||
assert.expect(3);
|
||||
const expected = ['mousedown', 'mouseup', 'click'];
|
||||
const closest = function() {
|
||||
return {
|
||||
querySelector: function() {
|
||||
return {
|
||||
dispatchEvent: function(ev) {
|
||||
const actual = ev.type;
|
||||
assert.equal(actual, expected);
|
||||
assert.equal(actual, expected.shift());
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue