From 6a11868cd6d2829a5c830aa709a4a0280d31b901 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 26 Aug 2024 18:47:22 +0200 Subject: [PATCH 01/17] Early PoC of CKEditor5 inside a Shadow DOM. --- .../ckeditor5-engine/src/view/domconverter.ts | 8 ++- packages/ckeditor5-engine/src/view/filler.ts | 4 +- .../src/view/observer/inputobserver.ts | 4 +- .../src/view/observer/selectionobserver.ts | 7 +- .../ckeditor5-engine/src/view/renderer.ts | 8 ++- .../ckeditor5-engine/src/view/uielement.ts | 4 +- .../src/editorui/bodycollection.ts | 1 + packages/ckeditor5-ui/src/tooltipmanager.ts | 1 + .../ckeditor5-utils/src/dom/getancestors.ts | 1 + .../src/dom/getpositionedancestor.ts | 1 + .../ckeditor5-utils/src/dom/getselection.ts | 66 +++++++++++++++++++ packages/ckeditor5-utils/src/dom/rect.ts | 6 +- packages/ckeditor5-utils/src/dom/scroll.ts | 8 ++- packages/ckeditor5-utils/src/index.ts | 1 + tests/manual/shadow.html | 36 ++++++++++ tests/manual/shadow.js | 63 ++++++++++++++++++ tests/manual/shadow.md | 0 17 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 packages/ckeditor5-utils/src/dom/getselection.ts create mode 100644 tests/manual/shadow.html create mode 100644 tests/manual/shadow.js create mode 100644 tests/manual/shadow.md diff --git a/packages/ckeditor5-engine/src/view/domconverter.ts b/packages/ckeditor5-engine/src/view/domconverter.ts index 383f6a209e6..6c5c459f91d 100644 --- a/packages/ckeditor5-engine/src/view/domconverter.ts +++ b/packages/ckeditor5-engine/src/view/domconverter.ts @@ -32,7 +32,8 @@ import { isComment, isValidAttributeName, first, - env + env, + getSelection } from '@ckeditor/ckeditor5-utils'; import type ViewNode from './node.js'; @@ -1135,7 +1136,7 @@ export default class DomConverter { } // Check if DOM selection is inside editor editable element. - const domSelection = domEditable.ownerDocument.defaultView!.getSelection()!; + const domSelection = getSelection( domEditable )!; const newViewSelection = this.domSelectionToView( domSelection ); const selectionInEditable = newViewSelection && newViewSelection.rangeCount > 0; @@ -1203,7 +1204,8 @@ export default class DomConverter { * @param DOM Selection instance to check. */ public isDomSelectionBackward( selection: DomSelection ): boolean { - if ( selection.isCollapsed ) { + // TODO ShadowRoot have invalid isCollapsed, check first range and if this issue is not resolved in Chrome. + if ( selection.isCollapsed && ( !selection.rangeCount || selection.getRangeAt( 0 ).collapsed ) ) { return false; } diff --git a/packages/ckeditor5-engine/src/view/filler.ts b/packages/ckeditor5-engine/src/view/filler.ts index 52a7a490e6b..ba99defae7d 100644 --- a/packages/ckeditor5-engine/src/view/filler.ts +++ b/packages/ckeditor5-engine/src/view/filler.ts @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import { keyCodes, isText, type KeystrokeInfo } from '@ckeditor/ckeditor5-utils'; +import { keyCodes, isText, getSelection, type KeystrokeInfo } from '@ckeditor/ckeditor5-utils'; import type View from './view.js'; import type DomEventData from './observer/domeventdata.js'; import type { ViewDocumentArrowKeyEvent } from './observer/arrowkeysobserver.js'; @@ -158,7 +158,7 @@ export function injectQuirksHandling( view: View ): void { */ function jumpOverInlineFiller( evt: unknown, data: DomEventData & KeystrokeInfo ) { if ( data.keyCode == keyCodes.arrowleft ) { - const domSelection = data.domTarget.ownerDocument.defaultView!.getSelection()!; + const domSelection = getSelection( data.domTarget )!; if ( domSelection.rangeCount == 1 && domSelection.getRangeAt( 0 ).collapsed ) { const domParent = domSelection.getRangeAt( 0 ).startContainer; diff --git a/packages/ckeditor5-engine/src/view/observer/inputobserver.ts b/packages/ckeditor5-engine/src/view/observer/inputobserver.ts index 4bea6774a02..c2263353757 100644 --- a/packages/ckeditor5-engine/src/view/observer/inputobserver.ts +++ b/packages/ckeditor5-engine/src/view/observer/inputobserver.ts @@ -11,7 +11,7 @@ import DomEventObserver from './domeventobserver.js'; import type DomEventData from './domeventdata.js'; import type ViewRange from '../range.js'; import DataTransfer from '../datatransfer.js'; -import { env } from '@ckeditor/ckeditor5-utils'; +import { env, getSelection } from '@ckeditor/ckeditor5-utils'; // @if CK_DEBUG_TYPING // const { _debouncedLine } = require( '../../dev-utils/utils.js' ); @@ -105,7 +105,7 @@ export default class InputObserver extends DomEventObserver<'beforeinput'> { // For Android devices we use a fallback to the current DOM selection, Android modifies it according // to the expected target ranges of input event. else if ( env.isAndroid ) { - const domSelection = ( domEvent.target as HTMLElement ).ownerDocument.defaultView!.getSelection()!; + const domSelection = getSelection( domEvent.target as HTMLElement )!; targetRanges = Array.from( view.domConverter.domSelectionToView( domSelection ).getRanges() ); diff --git a/packages/ckeditor5-engine/src/view/observer/selectionobserver.ts b/packages/ckeditor5-engine/src/view/observer/selectionobserver.ts index 796f44e3599..b8a9c45f024 100644 --- a/packages/ckeditor5-engine/src/view/observer/selectionobserver.ts +++ b/packages/ckeditor5-engine/src/view/observer/selectionobserver.ts @@ -12,7 +12,7 @@ import Observer from './observer.js'; import MutationObserver from './mutationobserver.js'; import FocusObserver from './focusobserver.js'; -import { env } from '@ckeditor/ckeditor5-utils'; +import { env, getSelection } from '@ckeditor/ckeditor5-utils'; import { debounce, type DebouncedFunc } from 'lodash-es'; import type View from '../view.js'; @@ -148,6 +148,7 @@ export default class SelectionObserver extends Observer { this.listenTo( domElement, 'keyup', endDocumentIsSelecting, { priority: 'highest', useCapture: true } ); // Add document-wide listeners only once. This method could be called for multiple editing roots. + // TODO ShadowRoot if ( this._documents.has( domDocument ) ) { return; } @@ -181,7 +182,7 @@ export default class SelectionObserver extends Observer { return; } - this._handleSelectionChange( domDocument ); + this._handleSelectionChange( domElement ); // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) { // @if CK_DEBUG_TYPING // console.groupEnd(); @@ -254,7 +255,7 @@ export default class SelectionObserver extends Observer { return; } - const domSelection = domDocument.defaultView!.getSelection()!; + const domSelection = getSelection( domDocument )!; if ( this.checkShouldIgnoreEventFromTarget( domSelection.anchorNode! ) ) { return; diff --git a/packages/ckeditor5-engine/src/view/renderer.ts b/packages/ckeditor5-engine/src/view/renderer.ts index 2fb75cb5b9b..c4384823b51 100644 --- a/packages/ckeditor5-engine/src/view/renderer.ts +++ b/packages/ckeditor5-engine/src/view/renderer.ts @@ -23,6 +23,7 @@ import { isText, remove, indexOf, + getSelection, type DiffResult, type ObservableChangeEvent } from '@ckeditor/ckeditor5-utils'; @@ -990,7 +991,7 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() { container.textContent = this.selection.fakeSelectionLabel || '\u00A0'; - const domSelection = domDocument.getSelection()!; + const domSelection = getSelection( domRoot )!; const domRange = domDocument.createRange(); domSelection.removeAllRanges(); @@ -1004,7 +1005,7 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() { * @param domRoot A valid DOM root where the DOM selection should be rendered. */ private _updateDomSelection( domRoot: DomElement ) { - const domSelection = domRoot.ownerDocument.defaultView!.getSelection()!; + const domSelection = getSelection( domRoot )!; // Let's check whether DOM selection needs updating at all. if ( !this._domSelectionNeedsUpdate( domSelection ) ) { @@ -1067,7 +1068,7 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() { */ private _fakeSelectionNeedsUpdate( domRoot: DomElement ): boolean { const container = this._fakeSelectionContainer; - const domSelection = domRoot.ownerDocument.getSelection()!; + const domSelection = getSelection( domRoot )!; // Fake selection needs to be updated if there's no fake selection container, or the container currently sits // in a different root. @@ -1087,6 +1088,7 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() { * Removes the DOM selection. */ private _removeDomSelection(): void { + // TODO ShadowRoot for ( const doc of this.domDocuments ) { const domSelection = doc.getSelection()!; diff --git a/packages/ckeditor5-engine/src/view/uielement.ts b/packages/ckeditor5-engine/src/view/uielement.ts index 2aa3bd38a89..3d2dd2e2444 100644 --- a/packages/ckeditor5-engine/src/view/uielement.ts +++ b/packages/ckeditor5-engine/src/view/uielement.ts @@ -9,7 +9,7 @@ import Element, { type ElementAttributes } from './element.js'; import Node from './node.js'; -import { CKEditorError, keyCodes } from '@ckeditor/ckeditor5-utils'; +import { CKEditorError, keyCodes, getSelection } from '@ckeditor/ckeditor5-utils'; import type View from './view.js'; import type Document from './document.js'; @@ -173,7 +173,7 @@ function getFillerOffset() { */ function jumpOverUiElement( evt: unknown, data: KeyEventData, domConverter: DomConverter ) { if ( data.keyCode == keyCodes.arrowright ) { - const domSelection = data.domTarget.ownerDocument.defaultView!.getSelection()!; + const domSelection = getSelection( data.domTarget )!; const domSelectionCollapsed = domSelection.rangeCount == 1 && domSelection.getRangeAt( 0 ).collapsed; // Jump over UI element if selection is collapsed or shift key is pressed. These are the cases when selection would extend. diff --git a/packages/ckeditor5-ui/src/editorui/bodycollection.ts b/packages/ckeditor5-ui/src/editorui/bodycollection.ts index e07074fb44d..13643090b26 100644 --- a/packages/ckeditor5-ui/src/editorui/bodycollection.ts +++ b/packages/ckeditor5-ui/src/editorui/bodycollection.ts @@ -86,6 +86,7 @@ export default class BodyCollection extends ViewCollection { if ( !wrapper ) { wrapper = createElement( document, 'div', { class: 'ck-body-wrapper' } ); + // TODO ShadowRoot document.body.appendChild( wrapper ); } diff --git a/packages/ckeditor5-ui/src/tooltipmanager.ts b/packages/ckeditor5-ui/src/tooltipmanager.ts index 21834edeb4a..ef05a0e769e 100644 --- a/packages/ckeditor5-ui/src/tooltipmanager.ts +++ b/packages/ckeditor5-ui/src/tooltipmanager.ts @@ -187,6 +187,7 @@ export default class TooltipManager extends /* #__PURE__ */ DomEmitterMixin() { this._pinTooltipDebounced = debounce( this._pinTooltip, 600 ); this._unpinTooltipDebounced = debounce( this._unpinTooltip, 400 ); + // TODO ShadowRoot this.listenTo( global.document, 'keydown', this._onKeyDown.bind( this ), { useCapture: true } ); this.listenTo( global.document, 'mouseenter', this._onEnterOrFocus.bind( this ), { useCapture: true } ); this.listenTo( global.document, 'mouseleave', this._onLeaveOrBlur.bind( this ), { useCapture: true } ); diff --git a/packages/ckeditor5-utils/src/dom/getancestors.ts b/packages/ckeditor5-utils/src/dom/getancestors.ts index 60f0d8e51e6..b3135d22095 100644 --- a/packages/ckeditor5-utils/src/dom/getancestors.ts +++ b/packages/ckeditor5-utils/src/dom/getancestors.ts @@ -22,6 +22,7 @@ export default function getAncestors( node: Node ): Array { const nodes: Array = []; let currentNode: Node | null = node; + // TODO ShadowRoot? // We are interested in `Node`s `DocumentFragment`s only. while ( currentNode && currentNode.nodeType != Node.DOCUMENT_NODE ) { nodes.unshift( currentNode ); diff --git a/packages/ckeditor5-utils/src/dom/getpositionedancestor.ts b/packages/ckeditor5-utils/src/dom/getpositionedancestor.ts index ec6ccbd399b..e41cfbe0e46 100644 --- a/packages/ckeditor5-utils/src/dom/getpositionedancestor.ts +++ b/packages/ckeditor5-utils/src/dom/getpositionedancestor.ts @@ -19,6 +19,7 @@ export default function getPositionedAncestor( element?: HTMLElement ): HTMLElem return null; } + // TODO ShadowRoot? if ( element.offsetParent === global.document.body ) { return null; } diff --git a/packages/ckeditor5-utils/src/dom/getselection.ts b/packages/ckeditor5-utils/src/dom/getselection.ts new file mode 100644 index 00000000000..c689a29b2fc --- /dev/null +++ b/packages/ckeditor5-utils/src/dom/getselection.ts @@ -0,0 +1,66 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module utils/dom/getselection + */ + +/** + * TODO + */ +export default function getSelection( node: Node ): Selection | null { + const rootNode = node.getRootNode(); + + if ( rootNode instanceof ShadowRoot ) { + // Safari & current spec. + const domSelection = rootNode.ownerDocument.defaultView!.getSelection()!; + + if ( typeof domSelection.getComposedRanges == 'function' ) { + // TODO Does it work if in multiple nested shadows? + const ranges = domSelection.getComposedRanges( rootNode ); + + // TODO for now just a DOM selection wrapper + return { + rangeCount: ranges.length, + + getRangeAt( index: number ) { + return ranges[ index ]; + }, + + isCollapsed: !ranges.length || ranges[ 0 ].isCollapsed, + + // TODO backward does not recognize correctly + ...ranges.length && { + anchorNode: domSelection.direction != 'backward' ? ranges[ 0 ].startContainer : ranges[ 0 ].endContainer, + anchorOffset: domSelection.direction != 'backward' ? ranges[ 0 ].startOffset : ranges[ 0 ].endOffset, + focusNode: domSelection.direction != 'backward' ? ranges[ 0 ].endContainer : ranges[ 0 ].startContainer, + focusOffset: domSelection.direction != 'backward' ? ranges[ 0 ].endContainer : ranges[ 0 ].startContainer + }, + + removeAllRanges() { + return domSelection.removeAllRanges(); + }, + + setBaseAndExtent( ...args ) { + return domSelection.setBaseAndExtent( ...args ); + }, + + addRange( ...args ) { + return domSelection.addRange( ...args ); + } + } as any; + } + + // Blink. + if ( typeof rootNode.getSelection == 'function' ) { + return rootNode.getSelection(); + } + + // Firefox. + return rootNode.host.ownerDocument.defaultView!.getSelection(); + } + + return rootNode.defaultView.getSelection(); +} diff --git a/packages/ckeditor5-utils/src/dom/rect.ts b/packages/ckeditor5-utils/src/dom/rect.ts index 4d9a4d16e3d..ccd89d2fcfb 100644 --- a/packages/ckeditor5-utils/src/dom/rect.ts +++ b/packages/ckeditor5-utils/src/dom/rect.ts @@ -116,7 +116,7 @@ export default class Rect { // will fail to obtain the geometry and the rect instance makes little sense to the features using it. // To get rid of this warning make sure the source passed to the constructor is a descendant of `window.document.body`. // @if CK_DEBUG // const sourceNode = isSourceRange ? source.startContainer : source; - // @if CK_DEBUG // if ( !sourceNode.ownerDocument || !sourceNode.ownerDocument.body.contains( sourceNode ) ) { + // @if CK_DEBUG // if ( !sourceNode.ownerDocument || !sourceNode.isConnected ) { // @if CK_DEBUG // console.warn( // @if CK_DEBUG // 'rect-source-not-in-dom: The source of this rect does not belong to any rendered DOM tree.', // @if CK_DEBUG // { source } ); @@ -316,7 +316,7 @@ export default class Rect { ) ) { child = parent; - parent = parent.parentNode; + parent = parent.parentNode; // TODO ShadowRoot? continue; } @@ -334,7 +334,7 @@ export default class Rect { } child = parent; - parent = parent.parentNode; + parent = parent.parentNode; // TODO ShadowRoot? } return visibleRect; diff --git a/packages/ckeditor5-utils/src/dom/scroll.ts b/packages/ckeditor5-utils/src/dom/scroll.ts index 3341121b583..af2e53e3831 100644 --- a/packages/ckeditor5-utils/src/dom/scroll.ts +++ b/packages/ckeditor5-utils/src/dom/scroll.ts @@ -346,7 +346,11 @@ function scrollAncestorsToShowRect>( } } - parent = parent.parentNode as HTMLElement; + if ( parent.parentNode instanceof ShadowRoot ) { + parent = parent.parentNode.host as HTMLElement; + } else { + parent = parent.parentNode as HTMLElement; + } } } @@ -403,7 +407,7 @@ function getParentElement( elementOrRange: HTMLElement | Range ): HTMLElement { return parent; } else { - return elementOrRange.parentNode as HTMLElement; + return elementOrRange.parentNode as HTMLElement; // TODO ShadowRoot? } } diff --git a/packages/ckeditor5-utils/src/index.ts b/packages/ckeditor5-utils/src/index.ts index 51053005fcd..d93fcd780f3 100644 --- a/packages/ckeditor5-utils/src/index.ts +++ b/packages/ckeditor5-utils/src/index.ts @@ -67,6 +67,7 @@ export { default as isValidAttributeName } from './dom/isvalidattributename.js'; export { default as isVisible } from './dom/isvisible.js'; export { getOptimalPosition, type Options as PositionOptions, type PositioningFunction, type DomPoint } from './dom/position.js'; export { default as remove } from './dom/remove.js'; +export { default as getSelection } from './dom/getselection.js'; export * from './dom/scroll.js'; export * from './keyboard.js'; diff --git a/tests/manual/shadow.html b/tests/manual/shadow.html new file mode 100644 index 00000000000..245bf623357 --- /dev/null +++ b/tests/manual/shadow.html @@ -0,0 +1,36 @@ + + + + +
+ + diff --git a/tests/manual/shadow.js b/tests/manual/shadow.js new file mode 100644 index 00000000000..1bb087bc63e --- /dev/null +++ b/tests/manual/shadow.js @@ -0,0 +1,63 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js'; + +const editorShadow = document.querySelector( '#editor-wrapper' ).attachShadow( { mode: 'open' } ); +const editorElement = document.createElement( 'div' ); + +editorShadow.appendChild( editorElement ); + +for ( const sheet of document.styleSheets ) { + if ( sheet.ownerNode?.dataset?.cke ) { + const shadowSheet = new CSSStyleSheet(); + shadowSheet.replaceSync( sheet.ownerNode.textContent ); + editorShadow.adoptedStyleSheets = [ shadowSheet ]; + break; + } +} + +ClassicEditor + .create( editorElement, { + plugins: [ ArticlePluginSet ], + toolbar: [ + 'heading', + '|', + 'bold', + 'italic', + 'link', + 'bulletedList', + 'numberedList', + '|', + 'outdent', + 'indent', + '|', + 'blockQuote', + 'insertTable', + 'mediaEmbed', + 'undo', + 'redo' + ], + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:wrapText', '|', 'imageTextAlternative' ] + }, + table: { + contentToolbar: [ + 'tableColumn', + 'tableRow', + 'mergeTableCells' + ] + }, + initialData: document.querySelector( '#editor' ).innerHTML + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/tests/manual/shadow.md b/tests/manual/shadow.md new file mode 100644 index 00000000000..e69de29bb2d From c10f714347a79e2ec70685e0d20088519fbbaa34 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 27 Aug 2024 15:47:27 +0200 Subject: [PATCH 02/17] Add support for drop target in a shadow DOM. --- .../ckeditor5-clipboard/src/dragdroptarget.ts | 4 ++- .../ckeditor5-engine/src/view/renderer.ts | 7 +++-- .../src/dom/getrangefrommouseevent.ts | 29 +++++++++++++++---- .../ckeditor5-utils/src/dom/getselection.ts | 16 ++++++---- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/dragdroptarget.ts b/packages/ckeditor5-clipboard/src/dragdroptarget.ts index fd61791e5be..3f7289c38e7 100644 --- a/packages/ckeditor5-clipboard/src/dragdroptarget.ts +++ b/packages/ckeditor5-clipboard/src/dragdroptarget.ts @@ -521,7 +521,9 @@ function findScrollableElement( domNode: HTMLElement ): HTMLElement { let domElement: HTMLElement = domNode; do { - domElement = domElement.parentElement!; + domElement = domElement.parentNode instanceof ShadowRoot ? + domElement.parentNode.host as HTMLElement : + domElement.parentElement!; const overflow = global.window.getComputedStyle( domElement ).overflowY; diff --git a/packages/ckeditor5-engine/src/view/renderer.ts b/packages/ckeditor5-engine/src/view/renderer.ts index c4384823b51..03d74835ba4 100644 --- a/packages/ckeditor5-engine/src/view/renderer.ts +++ b/packages/ckeditor5-engine/src/view/renderer.ts @@ -994,9 +994,11 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() { const domSelection = getSelection( domRoot )!; const domRange = domDocument.createRange(); - domSelection.removeAllRanges(); domRange.selectNodeContents( container ); - domSelection.addRange( domRange ); + domSelection.setBaseAndExtent( + domRange.startContainer, domRange.startOffset, + domRange.endContainer, domRange.endOffset + ); } /** @@ -1249,6 +1251,7 @@ function fixGeckoSelectionAfterBr( focus: ReturnType. if ( childAtOffset && ( childAtOffset as DomElement ).tagName == 'BR' ) { + // TODO ShadowRoot domSelection.addRange( domSelection.getRangeAt( 0 ) ); } } diff --git a/packages/ckeditor5-utils/src/dom/getrangefrommouseevent.ts b/packages/ckeditor5-utils/src/dom/getrangefrommouseevent.ts index bb378aafed6..3a3e164c00e 100644 --- a/packages/ckeditor5-utils/src/dom/getrangefrommouseevent.ts +++ b/packages/ckeditor5-utils/src/dom/getrangefrommouseevent.ts @@ -23,22 +23,39 @@ export default function getRangeFromMouseEvent( return null; } - const domDoc = ( domEvent.target as HTMLElement ).ownerDocument; + const domTarget = domEvent.target as HTMLElement; + const domDoc = domTarget.ownerDocument; + const domRootNode = domTarget.getRootNode(); const x = domEvent.clientX; const y = domEvent.clientY; - let domRange = null; + + // TODO + // Available in Chrome 128+ + if ( domDoc.caretPositionFromPoint && typeof domDoc.caretPositionFromPoint == 'function' ) { + const shadowRoot = domRootNode instanceof ShadowRoot ? domRootNode : null; + const caretPosition = domDoc.caretPositionFromPoint( x, y, shadowRoot ? { shadowRoots: [ domRootNode ] } : {} ); + const domRange = domDoc.createRange(); + + domRange.setStart( caretPosition.offsetNode, caretPosition.offset ); + domRange.collapse( true ); + + return domRange; + } // Webkit & Blink. if ( domDoc.caretRangeFromPoint && domDoc.caretRangeFromPoint( x, y ) ) { - domRange = domDoc.caretRangeFromPoint( x, y ); + return domDoc.caretRangeFromPoint( x, y ); } // FF. - else if ( domEvent.rangeParent ) { - domRange = domDoc.createRange(); + if ( domEvent.rangeParent ) { + const domRange = domDoc.createRange(); + domRange.setStart( domEvent.rangeParent, domEvent.rangeOffset! ); domRange.collapse( true ); + + return domRange; } - return domRange; + return null; } diff --git a/packages/ckeditor5-utils/src/dom/getselection.ts b/packages/ckeditor5-utils/src/dom/getselection.ts index c689a29b2fc..931a0ddfa95 100644 --- a/packages/ckeditor5-utils/src/dom/getselection.ts +++ b/packages/ckeditor5-utils/src/dom/getselection.ts @@ -21,12 +21,21 @@ export default function getSelection( node: Node ): Selection | null { // TODO Does it work if in multiple nested shadows? const ranges = domSelection.getComposedRanges( rootNode ); + console.log( 'sel', ranges, domSelection.direction ); + // TODO for now just a DOM selection wrapper return { rangeCount: ranges.length, getRangeAt( index: number ) { - return ranges[ index ]; + const staticRange = ranges[ index ]; + const range = rootNode.ownerDocument.createRange(); + + range.setStart( staticRange.startContainer, staticRange.startOffset ); + range.setEnd( staticRange.endContainer, staticRange.endOffset ); + + // Return the Range as it includes commonAncestorContainer property. + return range; }, isCollapsed: !ranges.length || ranges[ 0 ].isCollapsed, @@ -44,11 +53,8 @@ export default function getSelection( node: Node ): Selection | null { }, setBaseAndExtent( ...args ) { + console.log( 'sel set base and extent' ); return domSelection.setBaseAndExtent( ...args ); - }, - - addRange( ...args ) { - return domSelection.addRange( ...args ); } } as any; } From a2d80c010894358c4dac8d426f4a59135060dbb0 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 2 Sep 2024 18:50:49 +0200 Subject: [PATCH 03/17] The body collection in a shadow root. Listening for global events on all relevant shadow roots. --- .../src/bindings/clickoutsidehandler.ts | 2 ++ .../src/editorui/bodycollection.ts | 5 ++++ .../ckeditor5-ui/src/editorui/editorui.ts | 6 +++++ packages/ckeditor5-ui/src/tooltipmanager.ts | 27 ++++++++++++++++--- tests/manual/shadow.html | 2 ++ tests/manual/shadow.js | 6 +++++ 6 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts b/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts index 64cd33d4420..c9a07f5ffb8 100644 --- a/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts +++ b/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts @@ -42,6 +42,8 @@ export default function clickOutsideHandler( // Check if `composedPath` is `undefined` in case the browser does not support native shadow DOM. // Can be removed when all supported browsers support native shadow DOM. + // TODO ShadowRoot This won't work for closed shadow root. + // We probably should listen to all shadow roots we know of and have access to. const path = typeof domEvt.composedPath == 'function' ? domEvt.composedPath() : []; const contextElementsList = typeof contextElements == 'function' ? contextElements() : contextElements; diff --git a/packages/ckeditor5-ui/src/editorui/bodycollection.ts b/packages/ckeditor5-ui/src/editorui/bodycollection.ts index 13643090b26..7a1782898ce 100644 --- a/packages/ckeditor5-ui/src/editorui/bodycollection.ts +++ b/packages/ckeditor5-ui/src/editorui/bodycollection.ts @@ -90,6 +90,11 @@ export default class BodyCollection extends ViewCollection { document.body.appendChild( wrapper ); } + // TODO ShadowRoot (this won't work for closed shadow root) + if ( wrapper.shadowRoot ) { + wrapper = wrapper.shadowRoot; + } + wrapper.appendChild( this._bodyCollectionContainer ); } diff --git a/packages/ckeditor5-ui/src/editorui/editorui.ts b/packages/ckeditor5-ui/src/editorui/editorui.ts index 21851581a36..37288b3588b 100644 --- a/packages/ckeditor5-ui/src/editorui/editorui.ts +++ b/packages/ckeditor5-ui/src/editorui/editorui.ts @@ -161,6 +161,12 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() this.once( 'ready', () => { this._bindBodyCollectionWithFocusTracker(); + // TODO ShadowRoot This must register all shadow roots the editor is touching + // TODO how should we handle nested shadow roots? + for ( const element of this.focusTracker.elements ) { + this.tooltipManager.registerShadowRoot( element.getRootNode() ); + } + this.isReady = true; } ); diff --git a/packages/ckeditor5-ui/src/tooltipmanager.ts b/packages/ckeditor5-ui/src/tooltipmanager.ts index ef05a0e769e..a25a40b56f0 100644 --- a/packages/ckeditor5-ui/src/tooltipmanager.ts +++ b/packages/ckeditor5-ui/src/tooltipmanager.ts @@ -129,6 +129,11 @@ export default class TooltipManager extends /* #__PURE__ */ DomEmitterMixin() { */ private _unpinTooltipDebounced!: DebouncedFunc; + /** + * TODO + */ + private _shadowRoots = new Set(); + private readonly _watchdogExcluded!: true; /** @@ -187,16 +192,16 @@ export default class TooltipManager extends /* #__PURE__ */ DomEmitterMixin() { this._pinTooltipDebounced = debounce( this._pinTooltip, 600 ); this._unpinTooltipDebounced = debounce( this._unpinTooltip, 400 ); - // TODO ShadowRoot this.listenTo( global.document, 'keydown', this._onKeyDown.bind( this ), { useCapture: true } ); - this.listenTo( global.document, 'mouseenter', this._onEnterOrFocus.bind( this ), { useCapture: true } ); - this.listenTo( global.document, 'mouseleave', this._onLeaveOrBlur.bind( this ), { useCapture: true } ); this.listenTo( global.document, 'focus', this._onEnterOrFocus.bind( this ), { useCapture: true } ); this.listenTo( global.document, 'blur', this._onLeaveOrBlur.bind( this ), { useCapture: true } ); this.listenTo( global.document, 'scroll', this._onScroll.bind( this ), { useCapture: true } ); + // TODO ShadowRoot + this._addMouseEnterLeaveListeners( global.document ); + // Because this class is a singleton, its only instance is shared across all editors and connects them through the reference. // This causes issues with the ContextWatchdog. When an error is thrown in one editor, the watchdog traverses the references // and (because of shared tooltip manager) figures that the error affects all editors and restarts them all. @@ -229,10 +234,26 @@ export default class TooltipManager extends /* #__PURE__ */ DomEmitterMixin() { this.balloonPanelView.destroy(); this.stopListening(); + this._shadowRoots.clear(); + TooltipManager._instance = null; } } + public registerShadowRoot( node: ShadowRoot ): void { + if ( this._shadowRoots.has( node ) ) { + return; + } + + this._shadowRoots.add( node ); + this._addMouseEnterLeaveListeners( node ); + } + + private _addMouseEnterLeaveListeners( node: Document | ShadowRoot ): void { + this.listenTo( node, 'mouseenter', this._onEnterOrFocus.bind( this ), { useCapture: true } ); + this.listenTo( node, 'mouseleave', this._onLeaveOrBlur.bind( this ), { useCapture: true } ); + } + /** * Returns {@link #balloonPanelView} {@link module:utils/dom/position~PositioningFunction positioning functions} for a given position * name. diff --git a/tests/manual/shadow.html b/tests/manual/shadow.html index 245bf623357..bc22c13c81f 100644 --- a/tests/manual/shadow.html +++ b/tests/manual/shadow.html @@ -34,3 +34,5 @@

Heading 1

Quote

+ +
diff --git a/tests/manual/shadow.js b/tests/manual/shadow.js index 1bb087bc63e..da311d73a10 100644 --- a/tests/manual/shadow.js +++ b/tests/manual/shadow.js @@ -9,6 +9,8 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor. import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js'; const editorShadow = document.querySelector( '#editor-wrapper' ).attachShadow( { mode: 'open' } ); +const bodyCollectionWrapperShadow = document.querySelector( '.ck-body-wrapper' ).attachShadow( { mode: 'open' } ); + const editorElement = document.createElement( 'div' ); editorShadow.appendChild( editorElement ); @@ -17,7 +19,11 @@ for ( const sheet of document.styleSheets ) { if ( sheet.ownerNode?.dataset?.cke ) { const shadowSheet = new CSSStyleSheet(); shadowSheet.replaceSync( sheet.ownerNode.textContent ); + editorShadow.adoptedStyleSheets = [ shadowSheet ]; + bodyCollectionWrapperShadow.adoptedStyleSheets = [ shadowSheet ]; + + // sheet.disabled = true; break; } } From c6126719da0f42e8b39f71f90082b034cb5f2618 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 3 Sep 2024 13:54:18 +0200 Subject: [PATCH 04/17] CSS root properties should be translated to the host element. --- tests/manual/shadow.html | 6 ++++++ tests/manual/shadow.js | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/manual/shadow.html b/tests/manual/shadow.html index bc22c13c81f..c66482f4846 100644 --- a/tests/manual/shadow.html +++ b/tests/manual/shadow.html @@ -4,6 +4,12 @@ max-width: 800px; margin: 20px auto; } + + /* + #editor-wrapper, .ck-body-wrapper { + --ck-color-base-background: #f7ff68; + } + */ diff --git a/tests/manual/shadow.js b/tests/manual/shadow.js index da311d73a10..e7f724c5351 100644 --- a/tests/manual/shadow.js +++ b/tests/manual/shadow.js @@ -18,12 +18,12 @@ editorShadow.appendChild( editorElement ); for ( const sheet of document.styleSheets ) { if ( sheet.ownerNode?.dataset?.cke ) { const shadowSheet = new CSSStyleSheet(); - shadowSheet.replaceSync( sheet.ownerNode.textContent ); + shadowSheet.replaceSync( sheet.ownerNode.textContent.replace( /:root/g, ':host' ) ); editorShadow.adoptedStyleSheets = [ shadowSheet ]; bodyCollectionWrapperShadow.adoptedStyleSheets = [ shadowSheet ]; - // sheet.disabled = true; + sheet.disabled = true; break; } } From 97618c8650d5d67180fd8acd25abb810d7188291 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 3 Sep 2024 14:23:07 +0200 Subject: [PATCH 05/17] Marked shadow root places in code. Handling of block toolbar dragging. --- packages/ckeditor5-ckbox/src/ckboxcommand.ts | 1 + .../src/ckboximageedit/ckboximageeditcommand.ts | 1 + packages/ckeditor5-clipboard/src/dragdrop.ts | 1 + .../ckeditor5-clipboard/src/dragdropblocktoolbar.ts | 12 ++++++++++-- packages/ckeditor5-ui/src/arialiveannouncer.ts | 1 + .../ckeditor5-ui/src/bindings/draggableviewmixin.ts | 2 ++ .../ckeditor5-ui/src/colorpicker/colorpickerview.ts | 1 + .../src/dropdown/menu/dropdownmenunestedmenuview.ts | 1 + packages/ckeditor5-ui/src/dropdown/utils.ts | 1 + .../ckeditor5-ui/src/editorui/bodycollection.ts | 1 + .../src/panel/balloon/balloonpanelview.ts | 2 ++ tests/manual/shadow.js | 13 ++++++++++++- 12 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-ckbox/src/ckboxcommand.ts b/packages/ckeditor5-ckbox/src/ckboxcommand.ts index ad1ab39834a..ce66493929f 100644 --- a/packages/ckeditor5-ckbox/src/ckboxcommand.ts +++ b/packages/ckeditor5-ckbox/src/ckboxcommand.ts @@ -201,6 +201,7 @@ export default class CKBoxCommand extends Command { return; } + // TODO ShadowRoot this._wrapper = createElement( document, 'div', { class: 'ck ckbox-wrapper' } ); document.body.appendChild( this._wrapper ); diff --git a/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts b/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts index 906dcae8039..1e226e5e70b 100644 --- a/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts +++ b/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts @@ -108,6 +108,7 @@ export default class CKBoxImageEditCommand extends Command { return; } + // TODO ShadowRoot const wrapper = createElement( document, 'div', { class: 'ck ckbox-wrapper' } ); this._wrapper = wrapper; diff --git a/packages/ckeditor5-clipboard/src/dragdrop.ts b/packages/ckeditor5-clipboard/src/dragdrop.ts index 2e26adeb46c..225ef32f8d9 100644 --- a/packages/ckeditor5-clipboard/src/dragdrop.ts +++ b/packages/ckeditor5-clipboard/src/dragdrop.ts @@ -669,6 +669,7 @@ export default class DragDrop extends Plugin { style: 'position: fixed; left: -999999px;' } ); + // TODO ShadowRoot global.document.body.appendChild( this._previewContainer ); } else if ( this._previewContainer.firstElementChild ) { this._previewContainer.removeChild( this._previewContainer.firstElementChild ); diff --git a/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts b/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts index e46399c1d38..5fb3a61b1d6 100644 --- a/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts +++ b/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts @@ -68,6 +68,7 @@ export default class DragDropBlockToolbar extends Plugin { const element = blockToolbar.buttonView.element!; this._domEmitter.listenTo( element, 'dragstart', ( evt, data ) => this._handleBlockDragStart( data ) ); + // TODO ShadowRoot this._domEmitter.listenTo( global.document, 'dragover', ( evt, data ) => this._handleBlockDragging( data ) ); this._domEmitter.listenTo( global.document, 'drop', ( evt, data ) => this._handleBlockDragging( data ) ); this._domEmitter.listenTo( global.document, 'dragend', () => this._handleBlockDragEnd(), { useCapture: true } ); @@ -125,10 +126,17 @@ export default class DragDropBlockToolbar extends Plugin { return; } + const view = this.editor.editing.view; + const clientX = domEvent.clientX + ( this.editor.locale.contentLanguageDirection == 'ltr' ? 100 : -100 ); const clientY = domEvent.clientY; - const target = document.elementFromPoint( clientX, clientY ); - const view = this.editor.editing.view; + + let target = document.elementFromPoint( clientX, clientY ); + + // TODO ShadowRoot - this is a workaround, works this way only in open shadow root + if ( target && target.shadowRoot ) { + target = target.shadowRoot.elementFromPoint( clientX, clientY ); + } if ( !target || !target.closest( '.ck-editor__editable' ) ) { return; diff --git a/packages/ckeditor5-ui/src/arialiveannouncer.ts b/packages/ckeditor5-ui/src/arialiveannouncer.ts index 5c5f1eaca4e..e27f01a3bf6 100644 --- a/packages/ckeditor5-ui/src/arialiveannouncer.ts +++ b/packages/ckeditor5-ui/src/arialiveannouncer.ts @@ -96,6 +96,7 @@ export default class AriaLiveAnnouncer { if ( !this.view ) { this.view = new AriaLiveAnnouncerView( editor.locale ); + // TODO ShadowRoot - make sure that it can announce if it's inside a shadow root editor.ui.view.body.add( this.view ); } diff --git a/packages/ckeditor5-ui/src/bindings/draggableviewmixin.ts b/packages/ckeditor5-ui/src/bindings/draggableviewmixin.ts index d709a2a2abd..a9058a16e0c 100644 --- a/packages/ckeditor5-ui/src/bindings/draggableviewmixin.ts +++ b/packages/ckeditor5-ui/src/bindings/draggableviewmixin.ts @@ -77,6 +77,7 @@ export default function DraggableViewMixin>( view * Attaches the listeners for the dragging and drag end. */ private _attachDragListeners() { + // TODO ShadowRoot this.listenTo( global.document, 'mouseup', this._onDragEndBound ); this.listenTo( global.document, 'touchend', this._onDragEndBound ); this.listenTo( global.document, 'mousemove', this._onDragBound ); @@ -87,6 +88,7 @@ export default function DraggableViewMixin>( view * Detaches the listeners after the drag end. */ private _detachDragListeners() { + // TODO ShadowRoot this.stopListening( global.document, 'mouseup', this._onDragEndBound ); this.stopListening( global.document, 'touchend', this._onDragEndBound ); this.stopListening( global.document, 'mousemove', this._onDragBound ); diff --git a/packages/ckeditor5-ui/src/colorpicker/colorpickerview.ts b/packages/ckeditor5-ui/src/colorpicker/colorpickerview.ts index 9dfa07226c6..6b67a2ddeb4 100644 --- a/packages/ckeditor5-ui/src/colorpicker/colorpickerview.ts +++ b/packages/ckeditor5-ui/src/colorpicker/colorpickerview.ts @@ -141,6 +141,7 @@ export default class ColorPickerView extends View { this.on( 'change:_hexColor', () => { // Update the selected color in the color picker palette when it's not focused. // It means the user typed the color in the input. + // TODO ShadowRoot if ( document.activeElement !== this.picker ) { this.picker.setAttribute( 'color', this._hexColor ); } diff --git a/packages/ckeditor5-ui/src/dropdown/menu/dropdownmenunestedmenuview.ts b/packages/ckeditor5-ui/src/dropdown/menu/dropdownmenunestedmenuview.ts index 7efeadeec2e..7fc3db73619 100644 --- a/packages/ckeditor5-ui/src/dropdown/menu/dropdownmenunestedmenuview.ts +++ b/packages/ckeditor5-ui/src/dropdown/menu/dropdownmenunestedmenuview.ts @@ -302,6 +302,7 @@ export default class DropdownMenuNestedMenuView extends View implements Focusabl keystrokes.listenTo( panelView.element! ); panelView.pin( { positions: this._panelPositions, + // TODO ShadowRoot limiter: global.document.body, element: panelView.element!, target: buttonView.element!, diff --git a/packages/ckeditor5-ui/src/dropdown/utils.ts b/packages/ckeditor5-ui/src/dropdown/utils.ts index 02918783076..c645739e53e 100644 --- a/packages/ckeditor5-ui/src/dropdown/utils.ts +++ b/packages/ckeditor5-ui/src/dropdown/utils.ts @@ -606,6 +606,7 @@ function focusDropdownButtonOnClose( dropdownView: DropdownView ) { // If the dropdown was closed, move the focus back to the button (#12125). // Don't touch the focus, if it moved somewhere else (e.g. moved to the editing root on #execute) (#12178). // Note: Don't use the state of the DropdownView#focusTracker here. It fires #blur with the timeout. + // TODO ShadowRoot if ( elements.some( element => element.contains( global.document.activeElement ) ) ) { dropdownView.buttonView.focus(); } diff --git a/packages/ckeditor5-ui/src/editorui/bodycollection.ts b/packages/ckeditor5-ui/src/editorui/bodycollection.ts index 7a1782898ce..0ccdd541253 100644 --- a/packages/ckeditor5-ui/src/editorui/bodycollection.ts +++ b/packages/ckeditor5-ui/src/editorui/bodycollection.ts @@ -109,6 +109,7 @@ export default class BodyCollection extends ViewCollection { this._bodyCollectionContainer.remove(); } + // TODO ShadowRoot const wrapper = document.querySelector( '.ck-body-wrapper' ); if ( wrapper && wrapper.childElementCount == 0 ) { diff --git a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts index 3fda8cbbcc8..a6a4a71a97b 100644 --- a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts +++ b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts @@ -289,6 +289,7 @@ export default class BalloonPanelView extends View { defaultPositions.northArrowSouthEast, defaultPositions.viewportStickyNorth ], + // TODO ShadowRoot limiter: global.document.body, fitInViewport: true }, options ) as PositionOptions; @@ -398,6 +399,7 @@ export default class BalloonPanelView extends View { } let targetElement = getDomElement( options.target ); + // TODO ShadowRoot const limiterElement = options.limiter ? getDomElement( options.limiter ) : global.document.body; // Then we need to listen on scroll event of eny element in the document. diff --git a/tests/manual/shadow.js b/tests/manual/shadow.js index e7f724c5351..0d3e245cc6d 100644 --- a/tests/manual/shadow.js +++ b/tests/manual/shadow.js @@ -7,6 +7,7 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js'; import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js'; +import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar.js'; const editorShadow = document.querySelector( '#editor-wrapper' ).attachShadow( { mode: 'open' } ); const bodyCollectionWrapperShadow = document.querySelector( '.ck-body-wrapper' ).attachShadow( { mode: 'open' } ); @@ -30,7 +31,7 @@ for ( const sheet of document.styleSheets ) { ClassicEditor .create( editorElement, { - plugins: [ ArticlePluginSet ], + plugins: [ ArticlePluginSet, BlockToolbar ], toolbar: [ 'heading', '|', @@ -49,6 +50,16 @@ ClassicEditor 'undo', 'redo' ], + blockToolbar: [ + 'heading', + '|', + 'bulletedList', + 'numberedList', + '|', + 'blockQuote', + 'insertTable', + 'mediaEmbed' + ], image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:wrapText', '|', 'imageTextAlternative' ] }, From b6ab6623e7fc8d1b278df30e009c224f989371bd Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 3 Sep 2024 15:27:02 +0200 Subject: [PATCH 06/17] Component focus handling. --- .../ckeditor5-clipboard/src/dragdropblocktoolbar.ts | 2 +- tests/manual/shadow.html | 6 ++++-- tests/manual/shadow.js | 11 ++++++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts b/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts index 5fb3a61b1d6..8a9b560ed39 100644 --- a/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts +++ b/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts @@ -134,7 +134,7 @@ export default class DragDropBlockToolbar extends Plugin { let target = document.elementFromPoint( clientX, clientY ); // TODO ShadowRoot - this is a workaround, works this way only in open shadow root - if ( target && target.shadowRoot ) { + if ( target && target.shadowRoot && target.shadowRoot.elementFromPoint ) { target = target.shadowRoot.elementFromPoint( clientX, clientY ); } diff --git a/tests/manual/shadow.html b/tests/manual/shadow.html index c66482f4846..01f223b4927 100644 --- a/tests/manual/shadow.html +++ b/tests/manual/shadow.html @@ -6,14 +6,16 @@ } /* - #editor-wrapper, .ck-body-wrapper { + #editor-component, .ck-body-wrapper { --ck-color-base-background: #f7ff68; } */ -
+

+ +