diff --git a/ui-v2/app/instance-initializers/selection.js b/ui-v2/app/instance-initializers/selection.js new file mode 100644 index 0000000000..e106234fcb --- /dev/null +++ b/ui-v2/app/instance-initializers/selection.js @@ -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); + }, + }); + }, +}; diff --git a/ui-v2/app/utils/dom/click-first-anchor.js b/ui-v2/app/utils/dom/click-first-anchor.js index 0a8a6f23ff..da3699fbc7 100644 --- a/ui-v2/app/utils/dom/click-first-anchor.js +++ b/ui-v2/app/utils/dom/click-first-anchor.js @@ -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); } }; } diff --git a/ui-v2/config/environment.js b/ui-v2/config/environment.js index aaeed171fd..11ce0aef41 100644 --- a/ui-v2/config/environment.js +++ b/ui-v2/config/environment.js @@ -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; diff --git a/ui-v2/tests/unit/utils/dom/click-first-anchor-test.js b/ui-v2/tests/unit/utils/dom/click-first-anchor-test.js index e8c64d662a..2b01fef676 100644 --- a/ui-v2/tests/unit/utils/dom/click-first-anchor-test.js +++ b/ui-v2/tests/unit/utils/dom/click-first-anchor-test.js @@ -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()); }, }; },