Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Shadow DOM] Allow selection from target encapsulated by a shadow root #4737

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions src/core/main/ts/api/dom/Selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() : (<any> 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() : (<any> win.document).selection;
};

/**
* Returns the browsers internal range object.
Expand Down
65 changes: 65 additions & 0 deletions src/core/test/ts/browser/api/dom/SelectionTest.ts
Original file line number Diff line number Diff line change
@@ -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);
});