From fedf8beaf84ee57e345979d438138ddbd6cf45a1 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 6 Sep 2024 16:41:48 +0200 Subject: [PATCH] Active element handling updated to work in shadow DOM. --- packages/ckeditor5-ckbox/src/ckboxcommand.ts | 2 ++ .../ckboximageedit/ckboximageeditcommand.ts | 2 ++ packages/ckeditor5-clipboard/src/dragdrop.ts | 2 ++ .../src/dragdropblocktoolbar.ts | 7 ++++++- .../ckeditor5-clipboard/src/dragdroptarget.ts | 2 ++ .../ckeditor5-engine/src/view/domconverter.ts | 5 +++-- .../src/view/observer/selectionobserver.ts | 19 +++++++++++-------- .../ckeditor5-engine/src/view/renderer.ts | 1 + .../src/bindings/clickoutsidehandler.ts | 5 +++-- .../menu/dropdownmenunestedmenuview.ts | 1 - packages/ckeditor5-ui/src/dropdown/utils.ts | 4 ++-- .../src/panel/balloon/balloonpanelview.ts | 4 +++- packages/ckeditor5-ui/src/tooltipmanager.ts | 1 + .../src/dom/findclosestscrollableancestor.ts | 2 ++ packages/ckeditor5-widget/src/utils.ts | 1 + 15 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5-ckbox/src/ckboxcommand.ts b/packages/ckeditor5-ckbox/src/ckboxcommand.ts index ce66493929f..7473f0f850a 100644 --- a/packages/ckeditor5-ckbox/src/ckboxcommand.ts +++ b/packages/ckeditor5-ckbox/src/ckboxcommand.ts @@ -202,6 +202,8 @@ export default class CKBoxCommand extends Command { } // TODO ShadowRoot + // - can we append it to the body collection? + // - does CKBox support Shadow DOM? 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 1e226e5e70b..2c3ffd0793d 100644 --- a/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts +++ b/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts @@ -109,6 +109,8 @@ export default class CKBoxImageEditCommand extends Command { } // TODO ShadowRoot + // - can we append it to the body collection? + // - does CKBox support Shadow DOM? 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 225ef32f8d9..ae4d78b9891 100644 --- a/packages/ckeditor5-clipboard/src/dragdrop.ts +++ b/packages/ckeditor5-clipboard/src/dragdrop.ts @@ -670,6 +670,8 @@ export default class DragDrop extends Plugin { } ); // TODO ShadowRoot + // - can we append it to the body collection? + // - is the preview generated correctly in the Shadow DOM 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 8a9b560ed39..60d148d7e3d 100644 --- a/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts +++ b/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts @@ -68,7 +68,9 @@ export default class DragDropBlockToolbar extends Plugin { const element = blockToolbar.buttonView.element!; this._domEmitter.listenTo( element, 'dragstart', ( evt, data ) => this._handleBlockDragStart( data ) ); + // TODO ShadowRoot + // - those events will propagate across the shadow DOM boundary (bubbles and composed flags set) 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 } ); @@ -133,7 +135,10 @@ 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 + // TODO ShadowRoot + // - this is a workaround, works this way only in open shadow root + // - we should use map of known shadow roots and not depend on the shadowRoot property (it's there only for open mode) + // - the ShadowRoot#elementFromPoint() is non-standard but available in all browsers. if ( target && target.shadowRoot && target.shadowRoot.elementFromPoint ) { target = target.shadowRoot.elementFromPoint( clientX, clientY ); } diff --git a/packages/ckeditor5-clipboard/src/dragdroptarget.ts b/packages/ckeditor5-clipboard/src/dragdroptarget.ts index 3f7289c38e7..7fb4111bd20 100644 --- a/packages/ckeditor5-clipboard/src/dragdroptarget.ts +++ b/packages/ckeditor5-clipboard/src/dragdroptarget.ts @@ -521,6 +521,8 @@ function findScrollableElement( domNode: HTMLElement ): HTMLElement { let domElement: HTMLElement = domNode; do { + // TODO ShadowRoot + // - use helper for easier parent element access domElement = domElement.parentNode instanceof ShadowRoot ? domElement.parentNode.host as HTMLElement : domElement.parentElement!; diff --git a/packages/ckeditor5-engine/src/view/domconverter.ts b/packages/ckeditor5-engine/src/view/domconverter.ts index 6c5c459f91d..9ffdbba1c25 100644 --- a/packages/ckeditor5-engine/src/view/domconverter.ts +++ b/packages/ckeditor5-engine/src/view/domconverter.ts @@ -1091,7 +1091,8 @@ export default class DomConverter { public focus( viewEditable: EditableElement ): void { const domEditable = this.mapViewToDom( viewEditable ); - if ( domEditable && domEditable.ownerDocument.activeElement !== domEditable ) { + // TODO ShadowRoot + if ( domEditable && domEditable.getRootNode().activeElement !== domEditable ) { // Save the scrollX and scrollY positions before the focus. const { scrollX, scrollY } = global.window; const scrollPositions: Array<[ number, number ]> = []; @@ -1850,7 +1851,7 @@ function forEachDomElementAncestor( element: DomElement, callback: ( node: DomEl while ( node ) { callback( node ); - node = node.parentElement; + node = node.parentElement; // TODO ShadowRoot } } diff --git a/packages/ckeditor5-engine/src/view/observer/selectionobserver.ts b/packages/ckeditor5-engine/src/view/observer/selectionobserver.ts index b8a9c45f024..4ff7d63e53f 100644 --- a/packages/ckeditor5-engine/src/view/observer/selectionobserver.ts +++ b/packages/ckeditor5-engine/src/view/observer/selectionobserver.ts @@ -115,8 +115,6 @@ export default class SelectionObserver extends Observer { * @inheritDoc */ public override observe( domElement: HTMLElement ): void { - const domDocument = domElement.ownerDocument; - const startDocumentIsSelecting = () => { this.document.isSelecting = true; @@ -131,7 +129,7 @@ export default class SelectionObserver extends Observer { // Make sure that model selection is up-to-date at the end of selecting process. // Sometimes `selectionchange` events could arrive after the `mouseup` event and that selection could be already outdated. - this._handleSelectionChange( domDocument ); + this._handleSelectionChange( domElement ); this.document.isSelecting = false; @@ -147,16 +145,19 @@ export default class SelectionObserver extends Observer { this.listenTo( domElement, 'keydown', endDocumentIsSelecting, { priority: 'highest', useCapture: true } ); this.listenTo( domElement, 'keyup', endDocumentIsSelecting, { priority: 'highest', useCapture: true } ); + const domDocument = domElement.ownerDocument; + // Add document-wide listeners only once. This method could be called for multiple editing roots. - // TODO ShadowRoot if ( this._documents.has( domDocument ) ) { return; } // This listener is using capture mode to make sure that selection is upcasted before any other // handler would like to check it and update (for example table multi cell selection). + // TODO ShadowRoot - this event will propagate across the shadow DOM boundary (bubbles and composed flags set) this.listenTo( domDocument, 'mouseup', endDocumentIsSelecting, { priority: 'highest', useCapture: true } ); + // TODO ShadowRoot - this event is always fired from the document, even inside a Shadow DOM. this.listenTo( domDocument, 'selectionchange', ( evt, domEvent ) => { // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) { // @if CK_DEBUG_TYPING // _debouncedLine(); @@ -182,6 +183,7 @@ export default class SelectionObserver extends Observer { return; } + // TODO ShadowRoot - this will not work if separate roots are in separate shadow DOMs this._handleSelectionChange( domElement ); // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) { @@ -207,7 +209,8 @@ export default class SelectionObserver extends Observer { // @if CK_DEBUG_TYPING // ); // @if CK_DEBUG_TYPING // } - this._handleSelectionChange( domDocument ); + // TODO ShadowRoot - this will not work if separate roots are in separate shadow DOMs + this._handleSelectionChange( domElement ); // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) { // @if CK_DEBUG_TYPING // console.groupEnd(); @@ -248,14 +251,14 @@ export default class SelectionObserver extends Observer { * a selection changes and fires {@link module:engine/view/document~Document#event:selectionChange} event on every change * and {@link module:engine/view/document~Document#event:selectionChangeDone} when a selection stop changing. * - * @param domDocument DOM document. + * @param domElement DOM element. */ - private _handleSelectionChange( domDocument: Document ) { + private _handleSelectionChange( domElement: HTMLElement ) { if ( !this.isEnabled ) { return; } - const domSelection = getSelection( domDocument )!; + const domSelection = getSelection( domElement )!; 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 03d74835ba4..68d8e76bc5d 100644 --- a/packages/ckeditor5-engine/src/view/renderer.ts +++ b/packages/ckeditor5-engine/src/view/renderer.ts @@ -1095,6 +1095,7 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() { const domSelection = doc.getSelection()!; if ( domSelection.rangeCount ) { + // TODO ShadowRoot - the activeElement of the closest ShadowRoot? const activeDomElement = doc.activeElement!; const viewElement = this.domConverter.mapDomToView( activeDomElement as DomElement ); diff --git a/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts b/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts index c9a07f5ffb8..21307a86207 100644 --- a/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts +++ b/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts @@ -42,8 +42,9 @@ 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. + // 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/dropdown/menu/dropdownmenunestedmenuview.ts b/packages/ckeditor5-ui/src/dropdown/menu/dropdownmenunestedmenuview.ts index 09d56135733..701d04c18a1 100644 --- a/packages/ckeditor5-ui/src/dropdown/menu/dropdownmenunestedmenuview.ts +++ b/packages/ckeditor5-ui/src/dropdown/menu/dropdownmenunestedmenuview.ts @@ -303,7 +303,6 @@ 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 c645739e53e..50ee6c4b0c5 100644 --- a/packages/ckeditor5-ui/src/dropdown/utils.ts +++ b/packages/ckeditor5-ui/src/dropdown/utils.ts @@ -606,8 +606,8 @@ 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 ) ) ) { + // TODO ShadowRoot - the activeElement is valid for the closest ShadowRoot + if ( elements.some( element => element.getRootNode().activeElement && element.contains( element.getRootNode().activeElement ) ) ) { dropdownView.buttonView.focus(); } } ); diff --git a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts index a6a4a71a97b..e2b53d0ca32 100644 --- a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts +++ b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts @@ -399,9 +399,11 @@ export default class BalloonPanelView extends View { } let targetElement = getDomElement( options.target ); - // TODO ShadowRoot const limiterElement = options.limiter ? getDomElement( options.limiter ) : global.document.body; + // TODO ShadowRoot + // - we need to listen to the scroll event on every ShadowRoot + // (it is not composed and does not propagate to parent DOM) // Then we need to listen on scroll event of eny element in the document. this.listenTo( global.document, 'scroll', ( evt, domEvt ) => { const scrollTarget = domEvt.target as Element; diff --git a/packages/ckeditor5-ui/src/tooltipmanager.ts b/packages/ckeditor5-ui/src/tooltipmanager.ts index a25a40b56f0..1fd70229c7a 100644 --- a/packages/ckeditor5-ui/src/tooltipmanager.ts +++ b/packages/ckeditor5-ui/src/tooltipmanager.ts @@ -192,6 +192,7 @@ export default class TooltipManager extends /* #__PURE__ */ DomEmitterMixin() { this._pinTooltipDebounced = debounce( this._pinTooltip, 600 ); this._unpinTooltipDebounced = debounce( this._unpinTooltip, 400 ); + // TODO ShadowRoot - make sure those events propagate to parent shadow DOM this.listenTo( global.document, 'keydown', this._onKeyDown.bind( this ), { useCapture: true } ); this.listenTo( global.document, 'focus', this._onEnterOrFocus.bind( this ), { useCapture: true } ); diff --git a/packages/ckeditor5-utils/src/dom/findclosestscrollableancestor.ts b/packages/ckeditor5-utils/src/dom/findclosestscrollableancestor.ts index 0f29a0a68a9..17759753367 100644 --- a/packages/ckeditor5-utils/src/dom/findclosestscrollableancestor.ts +++ b/packages/ckeditor5-utils/src/dom/findclosestscrollableancestor.ts @@ -17,10 +17,12 @@ import global from './global.js'; */ export default function findClosestScrollableAncestor( domElement: HTMLElement ): HTMLElement | null { let element = domElement.parentElement; + if ( !element ) { return null; } + // TODO: ShadowRoot while ( element.tagName != 'BODY' ) { const overflow = element.style.overflowY || global.window.getComputedStyle( element ).overflowY; diff --git a/packages/ckeditor5-widget/src/utils.ts b/packages/ckeditor5-widget/src/utils.ts index fc712029cf3..b6ac2ad0243 100644 --- a/packages/ckeditor5-widget/src/utils.ts +++ b/packages/ckeditor5-widget/src/utils.ts @@ -483,6 +483,7 @@ export function calculateResizeHostAncestorWidth( domResizeHost: HTMLElement ): let checkedElement = domResizeHostParent!; while ( isNaN( parentWidth ) ) { + // TODO ShadowRoot checkedElement = checkedElement.parentElement!; if ( ++currentLevel > ancestorLevelLimit ) {