From e2f810eeb715757b03c4356661619a8c2c69e6ea Mon Sep 17 00:00:00 2001 From: Martin Bayly Date: Fri, 4 Jan 2019 17:14:26 -0800 Subject: [PATCH] allow selection from target encapsulated by a shadow root --- src/core/main/ts/api/dom/Selection.ts | 30 +++++++-- .../test/ts/browser/api/dom/SelectionTest.ts | 65 +++++++++++++++++++ 2 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 src/core/test/ts/browser/api/dom/SelectionTest.ts diff --git a/src/core/main/ts/api/dom/Selection.ts b/src/core/main/ts/api/dom/Selection.ts index 7759b1bb4ef..ff575e785f2 100644 --- a/src/core/main/ts/api/dom/Selection.ts +++ b/src/core/main/ts/api/dom/Selection.ts @@ -264,13 +264,29 @@ export const Selection = function (dom: DOMUtils, win: Window, serializer, edito setRng(rng); }; - /** - * Returns the browsers internal selection object. - * - * @method getSel - * @return {Selection} Internal browser selection object. - */ - const getSel = (): NativeSelection => win.getSelection ? win.getSelection() : ( win.document).selection; + const getSel = (): NativeSelection => { + let selectionRoot: any = win; + try { + // We need to return Shadow Root if target element is under Shadow DOM and not using iframe + if (!editor.iframeElement && editor.targetElm.matches && editor.targetElm.matches(':host *')) { + (function (target: any) { + while (target.parentNode) { + target = target.parentNode; + } + // If there is no parent node, but there is `host` property - we've just found Shadow Root, + // where we'll get selection. Note for Chrome the shadowRoot implements the DocumentOrShadowRoot mixin + // but in other browsers it is still implemented on the document + if (target.host) { + selectionRoot = target.getSelection ? target : win.document; + } + })(editor.targetElm); + } + } catch (err) { + // Nothing, even if `.matches` method is present - it doesn't mean that browsers understands ':host *' selector + } + + return selectionRoot.getSelection ? selectionRoot.getSelection() : ( win.document).selection; + }; /** * Returns the browsers internal range object. diff --git a/src/core/test/ts/browser/api/dom/SelectionTest.ts b/src/core/test/ts/browser/api/dom/SelectionTest.ts new file mode 100644 index 00000000000..c16630e1c80 --- /dev/null +++ b/src/core/test/ts/browser/api/dom/SelectionTest.ts @@ -0,0 +1,65 @@ +import { Assertions, Logger, Pipeline, Step } from '@ephox/agar'; +import { TinyLoader } from '@ephox/mcagar'; +import { Selection } from 'tinymce/core/api/dom/Selection'; +import Theme from 'tinymce/themes/modern/Theme'; +import DOMUtils from 'tinymce/core/api/dom/DOMUtils'; +import ViewBlock from '../../../module/test/ViewBlock'; +import { UnitTest } from '@ephox/bedrock'; +import { document } from '@ephox/dom-globals'; + +UnitTest.asynctest('browser.tinymce.core.api.dom.SelectionTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + + Theme(); + + const DOM = DOMUtils.DOM; + const viewBlock = ViewBlock(); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + + const sTestEmptyDocumentSelection = Logger.t('Returns empty document selection', Step.sync(function () { + document.getSelection().removeAllRanges(); + const selection = Selection(DOM, DOM.win, null, editor); + Assertions.assertEq('empty selection', null, selection.getSel().anchorNode); + })); + + const sTestSimpleDocumentSelection = Logger.t('Returns document selection', Step.sync(function () { + viewBlock.attach(); + document.getSelection().selectAllChildren(viewBlock.get()); + + const selection = Selection(DOM, DOM.win, null, editor); + Assertions.assertEq('document selection', 'DIV', selection.getSel().anchorNode.nodeName); + })); + + const sTestSimpleShadowSelection = Logger.t('Returns shadow root selection', Step.sync(function () { + const div = viewBlock.get(); + if (div.attachShadow) { + const shadow = div.attachShadow({mode: 'open'}); + shadow.appendChild(editor.targetElm); + const para = document.createElement('p'); + para.textContent = 'how now brown cow'; + editor.targetElm.appendChild(para); + viewBlock.attach(); + const selectionRoot = shadow.getSelection ? shadow : document; + selectionRoot.getSelection().selectAllChildren(editor.targetElm); + + const selection = Selection(DOM, DOM.win, null, editor); + Assertions.assertEq('shadow selection', true, selection.getSel().containsNode(para.firstChild, false)); + } + + })); + + Pipeline.async({}, [ + sTestEmptyDocumentSelection, + sTestSimpleDocumentSelection, + sTestSimpleShadowSelection + ], onSuccess, onFailure); + }, { + skin_url: '/project/js/tinymce/skins/lightgray', + inline: true + }, function () { + viewBlock.detach(); + success(); + }, failure); +});