diff --git a/packages/ckeditor5-ckbox/src/ckboxcommand.ts b/packages/ckeditor5-ckbox/src/ckboxcommand.ts index ad1ab39834a..7473f0f850a 100644 --- a/packages/ckeditor5-ckbox/src/ckboxcommand.ts +++ b/packages/ckeditor5-ckbox/src/ckboxcommand.ts @@ -201,6 +201,9 @@ export default class CKBoxCommand extends Command { return; } + // 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 fa5e86ccd69..c246f0d1ca2 100644 --- a/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts +++ b/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts @@ -108,6 +108,9 @@ export default class CKBoxImageEditCommand extends Command { return; } + // 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 fae13bede02..7f8f66c2b9f 100644 --- a/packages/ckeditor5-clipboard/src/dragdrop.ts +++ b/packages/ckeditor5-clipboard/src/dragdrop.ts @@ -669,6 +669,9 @@ export default class DragDrop extends Plugin { style: 'position: fixed; left: -999999px;' } ); + // 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 e46399c1d38..21ace94f5fe 100644 --- a/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts +++ b/packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts @@ -68,6 +68,8 @@ 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 } ); @@ -125,10 +127,20 @@ 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 + // - 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 ); + } if ( !target || !target.closest( '.ck-editor__editable' ) ) { return; diff --git a/packages/ckeditor5-clipboard/src/dragdroptarget.ts b/packages/ckeditor5-clipboard/src/dragdroptarget.ts index fd61791e5be..39e08fd39a3 100644 --- a/packages/ckeditor5-clipboard/src/dragdroptarget.ts +++ b/packages/ckeditor5-clipboard/src/dragdroptarget.ts @@ -29,6 +29,7 @@ import { DomEmitterMixin, delay, ResizeObserver, + getParentOrHostElement, type DomEmitter } from '@ckeditor/ckeditor5-utils'; @@ -521,7 +522,9 @@ function findScrollableElement( domNode: HTMLElement ): HTMLElement { let domElement: HTMLElement = domNode; do { - domElement = domElement.parentElement!; + // TODO ShadowRoot + // - use helper for easier parent element access + domElement = getParentOrHostElement( domElement ) as HTMLElement; const overflow = global.window.getComputedStyle( domElement ).overflowY; diff --git a/packages/ckeditor5-engine/src/view/domconverter.ts b/packages/ckeditor5-engine/src/view/domconverter.ts index 3c040881e03..3c4bcbd701d 100644 --- a/packages/ckeditor5-engine/src/view/domconverter.ts +++ b/packages/ckeditor5-engine/src/view/domconverter.ts @@ -32,6 +32,9 @@ import { isComment, isValidAttributeName, first, + getSelection, + getParentOrHostElement, + getActiveElement, env } from '@ckeditor/ckeditor5-utils'; @@ -1089,8 +1092,10 @@ export default class DomConverter { */ public focus( viewEditable: EditableElement ): void { const domEditable = this.mapViewToDom( viewEditable ); + const activeElement = domEditable && getActiveElement( domEditable ); - if ( domEditable && domEditable.ownerDocument.activeElement !== domEditable ) { + // TODO ShadowRoot + if ( domEditable && activeElement !== domEditable ) { // Save the scrollX and scrollY positions before the focus. const { scrollX, scrollY } = global.window; const scrollPositions: Array<[ number, number ]> = []; @@ -1135,7 +1140,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 +1208,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; } @@ -1848,7 +1854,8 @@ function forEachDomElementAncestor( element: DomElement, callback: ( node: DomEl while ( node ) { callback( node ); - node = node.parentElement; + // TODO ShadowRoot + node = getParentOrHostElement( node ) as DomElement | null; } } 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/mutationobserver.ts b/packages/ckeditor5-engine/src/view/observer/mutationobserver.ts index 28b4cf8c22c..9712c3d8155 100644 --- a/packages/ckeditor5-engine/src/view/observer/mutationobserver.ts +++ b/packages/ckeditor5-engine/src/view/observer/mutationobserver.ts @@ -86,6 +86,7 @@ export default class MutationObserver extends Observer { this._domElements.add( domElement ); if ( this.isEnabled ) { + // TODO ShadowRoot - will this work if widget has its own Shadow DOM? this._mutationObserver.observe( domElement, this._config ); } } diff --git a/packages/ckeditor5-engine/src/view/observer/selectionobserver.ts b/packages/ckeditor5-engine/src/view/observer/selectionobserver.ts index e6c29374ed3..2f24e60402f 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'; @@ -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,6 +145,8 @@ 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. if ( this._documents.has( domDocument ) ) { return; @@ -154,8 +154,10 @@ export default class SelectionObserver extends Observer { // 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', () => { // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) { // @if CK_DEBUG_TYPING // _debouncedLine(); @@ -181,7 +183,8 @@ export default class SelectionObserver extends Observer { return; } - 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(); @@ -206,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(); @@ -247,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 = domDocument.defaultView!.getSelection()!; + 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 e37264fecd7..e3f1cd70537 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'; @@ -993,12 +994,14 @@ 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(); domRange.selectNodeContents( container ); - domSelection.addRange( domRange ); + domSelection.setBaseAndExtent( + domRange.startContainer, domRange.startOffset, + domRange.endContainer, domRange.endOffset + ); } /** @@ -1007,7 +1010,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 ) ) { @@ -1070,7 +1073,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. @@ -1090,10 +1093,12 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() { * Removes the DOM selection. */ private _removeDomSelection(): void { + // TODO ShadowRoot - this currently does not work in Shadow DOM but also looks like it has no effect for ( const doc of this.domDocuments ) { 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 ); @@ -1250,6 +1255,7 @@ function fixGeckoSelectionAfterBr( focus: ReturnType. if ( childAtOffset && ( childAtOffset as DomElement ).tagName == 'BR' ) { + // TODO ShadowRoot domSelection.addRange( domSelection.getRangeAt( 0 ) ); } } 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-minimap/src/minimap.ts b/packages/ckeditor5-minimap/src/minimap.ts index a4eb3d342a4..bcc0bc23477 100644 --- a/packages/ckeditor5-minimap/src/minimap.ts +++ b/packages/ckeditor5-minimap/src/minimap.ts @@ -84,7 +84,7 @@ export default class Minimap extends Plugin { this._scrollableRootAncestor = findClosestScrollableAncestor( editingRootElement ); // DOM root element is not yet attached to the document. - if ( !editingRootElement.ownerDocument.body.contains( editingRootElement ) ) { + if ( !editingRootElement.isConnected ) { editor.ui.once( 'update', this._onUiReady.bind( this ) ); return; diff --git a/packages/ckeditor5-table/src/tableselection.ts b/packages/ckeditor5-table/src/tableselection.ts index 5300f48c84f..9c995b1b087 100644 --- a/packages/ckeditor5-table/src/tableselection.ts +++ b/packages/ckeditor5-table/src/tableselection.ts @@ -232,8 +232,12 @@ export default class TableSelection extends Plugin { highlighted.add( viewElement ); } - const lastViewCell = conversionApi.mapper.toViewElement( selectedCells[ selectedCells.length - 1 ] ); - viewWriter.setSelection( lastViewCell, 0 ); + // TODO ShadowRoot - find nearest selectable position so browser won't try to fix it + const lastModelCell = selectedCells[ selectedCells.length - 1 ]; + const modelRange = editor.model.schema.getNearestSelectionRange( editor.model.createPositionAt( lastModelCell, 0 ), 'forward' ); + const viewRange = conversionApi.mapper.toViewRange( modelRange ); + + viewWriter.setSelection( viewRange.start ); }, { priority: 'lowest' } ) ); function clearHighlightedTableCells( viewWriter: DowncastWriter ) { 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/clickoutsidehandler.ts b/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts index 64cd33d4420..21307a86207 100644 --- a/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts +++ b/packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts @@ -42,6 +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. const path = typeof domEvt.composedPath == 'function' ? domEvt.composedPath() : []; const contextElementsList = typeof contextElements == 'function' ? contextElements() : contextElements; diff --git a/packages/ckeditor5-ui/src/bindings/draggableviewmixin.ts b/packages/ckeditor5-ui/src/bindings/draggableviewmixin.ts index d709a2a2abd..95f072f618b 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 - those events will propagate across the shadow DOM boundary (bubbles and composed flags set) 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 - those events will propagate across the shadow DOM boundary (bubbles and composed flags set) 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..f59bd748f82 100644 --- a/packages/ckeditor5-ui/src/colorpicker/colorpickerview.ts +++ b/packages/ckeditor5-ui/src/colorpicker/colorpickerview.ts @@ -10,7 +10,7 @@ import { convertColor, convertToHex, registerCustomElement, type ColorPickerViewConfig } from './utils.js'; import type { HexColor } from '@ckeditor/ckeditor5-core'; -import { type Locale, global, env } from '@ckeditor/ckeditor5-utils'; +import { type Locale, global, env, getActiveElement } from '@ckeditor/ckeditor5-utils'; import { debounce, type DebouncedFunc } from 'lodash-es'; import View from '../view.js'; import type InputTextView from '../inputtext/inputtextview.js'; @@ -141,7 +141,8 @@ 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. - if ( document.activeElement !== this.picker ) { + // TODO ShadowRoot + if ( getActiveElement( this.element! ) !== this.picker ) { this.picker.setAttribute( 'color', this._hexColor ); } diff --git a/packages/ckeditor5-ui/src/dropdown/utils.ts b/packages/ckeditor5-ui/src/dropdown/utils.ts index 02918783076..cf15d7a2bd3 100644 --- a/packages/ckeditor5-ui/src/dropdown/utils.ts +++ b/packages/ckeditor5-ui/src/dropdown/utils.ts @@ -31,7 +31,7 @@ import type { FalsyValue } from '../template.js'; import type BodyCollection from '../editorui/bodycollection.js'; import { - global, + getActiveElement, priorities, logWarning, type Collection, @@ -606,7 +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. - if ( elements.some( element => element.contains( global.document.activeElement ) ) ) { + // TODO ShadowRoot - the activeElement is valid for the closest ShadowRoot + if ( elements.some( element => getActiveElement( element ) && element.contains( getActiveElement( element ) ) ) ) { dropdownView.buttonView.focus(); } } ); diff --git a/packages/ckeditor5-ui/src/editorui/bodycollection.ts b/packages/ckeditor5-ui/src/editorui/bodycollection.ts index e07074fb44d..0ccdd541253 100644 --- a/packages/ckeditor5-ui/src/editorui/bodycollection.ts +++ b/packages/ckeditor5-ui/src/editorui/bodycollection.ts @@ -86,9 +86,15 @@ export default class BodyCollection extends ViewCollection { if ( !wrapper ) { wrapper = createElement( document, 'div', { class: 'ck-body-wrapper' } ); + // TODO ShadowRoot document.body.appendChild( wrapper ); } + // TODO ShadowRoot (this won't work for closed shadow root) + if ( wrapper.shadowRoot ) { + wrapper = wrapper.shadowRoot; + } + wrapper.appendChild( this._bodyCollectionContainer ); } @@ -103,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/editorui/editorui.ts b/packages/ckeditor5-ui/src/editorui/editorui.ts index 21851581a36..c7240b4533b 100644 --- a/packages/ckeditor5-ui/src/editorui/editorui.ts +++ b/packages/ckeditor5-ui/src/editorui/editorui.ts @@ -239,6 +239,7 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() // Register the element, so it becomes available for Alt+F10 and Esc navigation. this.focusTracker.add( domElement ); + this.tooltipManager.registerShadowRoot( domElement.getRootNode() ); const setUpKeystrokeHandler = () => { // The editing view of the editor is already listening to keystrokes from DOM roots (see: KeyObserver). @@ -307,10 +308,12 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() public addToolbar( toolbarView: ToolbarView, options: FocusableToolbarOptions = {} ): void { if ( toolbarView.isRendered ) { this.focusTracker.add( toolbarView.element! ); + this.tooltipManager.registerShadowRoot( toolbarView.element!.getRootNode() ); this.editor.keystrokes.listenTo( toolbarView.element! ); } else { toolbarView.once( 'render', () => { this.focusTracker.add( toolbarView.element! ); + this.tooltipManager.registerShadowRoot( toolbarView.element!.getRootNode() ); this.editor.keystrokes.listenTo( toolbarView.element! ); } ); } @@ -393,6 +396,7 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() const menuBarViewElement = menuBarView.element!; this.focusTracker.add( menuBarViewElement ); + this.tooltipManager.registerShadowRoot( menuBarViewElement.getRootNode() ); this.editor.keystrokes.listenTo( menuBarViewElement ); const normalizedMenuBarConfig = normalizeMenuBarConfig( this.editor.config.get( 'menuBar' ) || {} ); @@ -679,10 +683,15 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() for ( const view of body ) { this.focusTracker.add( view.element! ); + + // TODO ShadowRoot This must register all shadow roots the editor is touching + // TODO how should we handle nested shadow roots? + this.tooltipManager.registerShadowRoot( view.element!.getRootNode() ); } body.on>( 'add', ( evt, view ) => { this.focusTracker.add( view.element! ); + this.tooltipManager.registerShadowRoot( view.element!.getRootNode() ); } ); body.on>( 'remove', ( evt, view ) => { diff --git a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts index 3fda8cbbcc8..e2b53d0ca32 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; @@ -400,6 +401,9 @@ export default class BalloonPanelView extends View { let targetElement = getDomElement( options.target ); 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/toolbar/toolbarview.ts b/packages/ckeditor5-ui/src/toolbar/toolbarview.ts index 728b49fa645..9f5e52654d0 100644 --- a/packages/ckeditor5-ui/src/toolbar/toolbarview.ts +++ b/packages/ckeditor5-ui/src/toolbar/toolbarview.ts @@ -871,7 +871,7 @@ class DynamicGrouping implements ToolbarBehavior { // from DOM. DOMRects won't work anyway and there will be tons of warning in the console and // nothing else. This happens, for instance, when the toolbar is detached from DOM and // some logic adds or removes its #items. - if ( !this.viewElement!.ownerDocument.body.contains( this.viewElement! ) ) { + if ( !this.viewElement!.isConnected ) { return; } diff --git a/packages/ckeditor5-ui/src/tooltipmanager.ts b/packages/ckeditor5-ui/src/tooltipmanager.ts index 21834edeb4a..9ab281fd2cd 100644 --- a/packages/ckeditor5-ui/src/tooltipmanager.ts +++ b/packages/ckeditor5-ui/src/tooltipmanager.ts @@ -16,6 +16,7 @@ import { first, global, isVisible, + isShadowRoot, type EventInfo, type PositioningFunction } from '@ckeditor/ckeditor5-utils'; @@ -129,6 +130,11 @@ export default class TooltipManager extends /* #__PURE__ */ DomEmitterMixin() { */ private _unpinTooltipDebounced!: DebouncedFunc; + /** + * TODO + */ + private _shadowRoots = new Set(); + private readonly _watchdogExcluded!: true; /** @@ -187,15 +193,17 @@ 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, '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. @@ -228,10 +236,29 @@ export default class TooltipManager extends /* #__PURE__ */ DomEmitterMixin() { this.balloonPanelView.destroy(); this.stopListening(); + this._shadowRoots.clear(); + TooltipManager._instance = null; } } + public registerShadowRoot( node: ShadowRoot | Node ): void { + if ( !isShadowRoot( node ) || this._shadowRoots.has( node ) ) { + return; + } + + this._shadowRoots.add( node ); + this._addMouseEnterLeaveListeners( node ); + } + + /** + * TODO ShadowRoot + */ + 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/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-utils/src/dom/getactiveelement.ts b/packages/ckeditor5-utils/src/dom/getactiveelement.ts new file mode 100644 index 00000000000..36ec02be98c --- /dev/null +++ b/packages/ckeditor5-utils/src/dom/getactiveelement.ts @@ -0,0 +1,17 @@ +/** + * @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 Node, Document, Element */ + +/** + * @module utils/dom/getactiveelement + */ + +/** + * TODO + */ +export default function getActiveElement( node: Node ): Element | null { + return ( node.getRootNode() as ShadowRoot | Document ).activeElement; +} diff --git a/packages/ckeditor5-utils/src/dom/getancestors.ts b/packages/ckeditor5-utils/src/dom/getancestors.ts index 60f0d8e51e6..d45fb272afb 100644 --- a/packages/ckeditor5-utils/src/dom/getancestors.ts +++ b/packages/ckeditor5-utils/src/dom/getancestors.ts @@ -22,6 +22,9 @@ export default function getAncestors( node: Node ): Array { const nodes: Array = []; let currentNode: Node | null = node; + // TODO ShadowRoot + // - this is used only in DomConverter#getHostViewElement() and getCommonAncestor() helper (not used anywhere) + // - this is scoped inside a shadow DOM // 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/getcommonancestor.ts b/packages/ckeditor5-utils/src/dom/getcommonancestor.ts index 9864b3c6aa8..03c989d01a2 100644 --- a/packages/ckeditor5-utils/src/dom/getcommonancestor.ts +++ b/packages/ckeditor5-utils/src/dom/getcommonancestor.ts @@ -17,6 +17,9 @@ import getAncestors from './getancestors.js'; * @returns Lowest common ancestor of both nodes or `null` if nodes do not have a common ancestor. */ export default function getCommonAncestor( nodeA: Node, nodeB: Node ): Node | null { + // TODO ShadowRoot + // - this is scoped inside a shadow DOM as getAncestors() helper + // - this helper is not used in the editor code const ancestorsA = getAncestors( nodeA ); const ancestorsB = getAncestors( nodeB ); diff --git a/packages/ckeditor5-utils/src/dom/getparentorhostelement.ts b/packages/ckeditor5-utils/src/dom/getparentorhostelement.ts new file mode 100644 index 00000000000..0c546775b44 --- /dev/null +++ b/packages/ckeditor5-utils/src/dom/getparentorhostelement.ts @@ -0,0 +1,17 @@ +/** + * @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/getparentorhostelement + */ + +import isShadowRoot from './isshadowroot.js'; + +/** + * TODO + */ +export default function getParentOrHostElement( node: Node ): Element | null { + return isShadowRoot( node.parentNode ) ? node.parentNode.host : node.parentElement; +} diff --git a/packages/ckeditor5-utils/src/dom/getpositionedancestor.ts b/packages/ckeditor5-utils/src/dom/getpositionedancestor.ts index ec6ccbd399b..04c6a75595e 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 - looks like it always returns correct offset parent if ( element.offsetParent === global.document.body ) { return null; } diff --git a/packages/ckeditor5-utils/src/dom/getrangefrommouseevent.ts b/packages/ckeditor5-utils/src/dom/getrangefrommouseevent.ts index bb378aafed6..8dd98d124dc 100644 --- a/packages/ckeditor5-utils/src/dom/getrangefrommouseevent.ts +++ b/packages/ckeditor5-utils/src/dom/getrangefrommouseevent.ts @@ -7,6 +7,8 @@ * @module utils/dom/getrangefrommouseevent */ +import isShadowRoot from './isshadowroot.js'; + /** * Returns a DOM range from a given point specified by a mouse event. * @@ -23,22 +25,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 = isShadowRoot( domRootNode ) ? 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 new file mode 100644 index 00000000000..ba5f0cc9085 --- /dev/null +++ b/packages/ckeditor5-utils/src/dom/getselection.ts @@ -0,0 +1,71 @@ +/** + * @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 + */ + +import isShadowRoot from './isshadowroot.js'; + +/** + * TODO + */ +export default function getSelection( node: Node ): Selection | null { + const rootNode = node.getRootNode(); + + if ( isShadowRoot( rootNode ) ) { + // 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 ) { + 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, + + // 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 ); + } + } 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/isshadowroot.ts b/packages/ckeditor5-utils/src/dom/isshadowroot.ts new file mode 100644 index 00000000000..eb70f385554 --- /dev/null +++ b/packages/ckeditor5-utils/src/dom/isshadowroot.ts @@ -0,0 +1,23 @@ +/** + * @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/isshadowroot + */ + +/** + * Checks if the object is a native DOM ShadowRoot. + */ +export default function isShadowRoot( obj: any ): obj is ShadowRoot { + if ( !obj ) { + return false; + } + + if ( obj.ownerDocument && obj.ownerDocument.defaultView ) { + return obj instanceof obj.ownerDocument.defaultView.ShadowRoot; + } + + return false; +} diff --git a/packages/ckeditor5-utils/src/dom/rect.ts b/packages/ckeditor5-utils/src/dom/rect.ts index 4d9a4d16e3d..b21fa83e374 100644 --- a/packages/ckeditor5-utils/src/dom/rect.ts +++ b/packages/ckeditor5-utils/src/dom/rect.ts @@ -13,6 +13,7 @@ import getBorderWidths from './getborderwidths.js'; import isText from './istext.js'; import getPositionedAncestor from './getpositionedancestor.js'; import global from './global.js'; +import getParentOrHostElement from './getparentorhostelement.js'; const rectProperties: Array = [ 'top', 'right', 'bottom', 'left', 'width', 'height' ]; @@ -116,7 +117,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 +317,8 @@ export default class Rect { ) ) { child = parent; - parent = parent.parentNode; + // TODO ShadowRoot + parent = getParentOrHostElement( parent ); continue; } @@ -334,7 +336,8 @@ export default class Rect { } child = parent; - parent = parent.parentNode; + // TODO ShadowRoot + parent = getParentOrHostElement( parent ); } return visibleRect; diff --git a/packages/ckeditor5-utils/src/dom/scroll.ts b/packages/ckeditor5-utils/src/dom/scroll.ts index 3341121b583..41072428ce2 100644 --- a/packages/ckeditor5-utils/src/dom/scroll.ts +++ b/packages/ckeditor5-utils/src/dom/scroll.ts @@ -10,6 +10,7 @@ import isRange from './isrange.js'; import Rect from './rect.js'; import isText from './istext.js'; +import getParentOrHostElement from './getparentorhostelement.js'; type IfTrue = T extends true ? true : never; @@ -346,7 +347,7 @@ function scrollAncestorsToShowRect>( } } - parent = parent.parentNode as HTMLElement; + parent = getParentOrHostElement( parent ) as HTMLElement; } } @@ -403,7 +404,8 @@ function getParentElement( elementOrRange: HTMLElement | Range ): HTMLElement { return parent; } else { - return elementOrRange.parentNode as HTMLElement; + // TODO ShadowRoot - should this look outside the shadow DOM? + return getParentOrHostElement( elementOrRange ) as HTMLElement; } } diff --git a/packages/ckeditor5-utils/src/index.ts b/packages/ckeditor5-utils/src/index.ts index 0cc02b52f64..6b73c921969 100644 --- a/packages/ckeditor5-utils/src/index.ts +++ b/packages/ckeditor5-utils/src/index.ts @@ -54,6 +54,7 @@ export { default as getAncestors } from './dom/getancestors.js'; export { default as getDataFromElement } from './dom/getdatafromelement.js'; export { default as getBorderWidths } from './dom/getborderwidths.js'; export { default as getRangeFromMouseEvent } from './dom/getrangefrommouseevent.js'; +export { default as getParentOrHostElement } from './dom/getparentorhostelement.js'; export { default as isText } from './dom/istext.js'; export { default as Rect, type RectSource } from './dom/rect.js'; export { default as ResizeObserver } from './dom/resizeobserver.js'; @@ -63,11 +64,14 @@ export { default as indexOf } from './dom/indexof.js'; export { default as insertAt } from './dom/insertat.js'; export { default as isComment } from './dom/iscomment.js'; export { default as isNode } from './dom/isnode.js'; +export { default as isShadowRoot } from './dom/isshadowroot.js'; export { default as isRange } from './dom/isrange.js'; 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 { default as getActiveElement } from './dom/getactiveelement.js'; export * from './dom/scroll.js'; export * from './keyboard.js'; 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 ) { diff --git a/packages/ckeditor5-widget/src/widgetresize/resizer.ts b/packages/ckeditor5-widget/src/widgetresize/resizer.ts index a0e55f18348..786e636ad21 100644 --- a/packages/ckeditor5-widget/src/widgetresize/resizer.ts +++ b/packages/ckeditor5-widget/src/widgetresize/resizer.ts @@ -512,5 +512,5 @@ function extractCoordinates( event: MouseEvent ) { } function existsInDom( element: Node | DocumentFragment | undefined | null ) { - return element && element.ownerDocument && element.ownerDocument.contains( element ); + return element && element.isConnected; } diff --git a/packages/ckeditor5-widget/src/widgettoolbarrepository.ts b/packages/ckeditor5-widget/src/widgettoolbarrepository.ts index 777e0b0f572..31f9cd480a5 100644 --- a/packages/ckeditor5-widget/src/widgettoolbarrepository.ts +++ b/packages/ckeditor5-widget/src/widgettoolbarrepository.ts @@ -190,6 +190,7 @@ export default class WidgetToolbarRepository extends Plugin { initialized: false }; + // TODO ShadowRoot - cycling toolbars does not work correctly // Register the toolbar so it becomes available for Alt+F10 and Esc navigation. editor.ui.addToolbar( toolbarView, { isContextual: true, diff --git a/tests/manual/all-features.js b/tests/manual/all-features.js index 8da91aea4d6..72d4f395521 100644 --- a/tests/manual/all-features.js +++ b/tests/manual/all-features.js @@ -8,6 +8,8 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js'; import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment.js'; import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js'; +import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar.js'; +import BalloonToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js'; import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage.js'; import AutoLink from '@ckeditor/ckeditor5-link/src/autolink.js'; import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js'; @@ -64,7 +66,8 @@ ClassicEditor Alignment, IndentBlock, PasteFromOffice, PageBreak, HorizontalLine, ShowBlocks, SpecialCharacters, SpecialCharactersEssentials, WordCount, - CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport + CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport, + BlockToolbar, BalloonToolbar ], toolbar: [ 'heading', 'style', @@ -89,6 +92,17 @@ ClassicEditor '|', 'undo', 'redo', 'findAndReplace' ], + blockToolbar: [ + 'heading', + '|', + 'bulletedList', + 'numberedList', + '|', + 'blockQuote', + 'insertTable', + 'mediaEmbed' + ], + balloonToolbar: [ 'bold', 'italic', 'link' ], cloudServices: CS_CONFIG, table: { contentToolbar: [ diff --git a/tests/manual/shadow.html b/tests/manual/shadow.html new file mode 100644 index 00000000000..004e74d902e --- /dev/null +++ b/tests/manual/shadow.html @@ -0,0 +1,268 @@ + + + + + + + +
+ + + +
diff --git a/tests/manual/shadow.js b/tests/manual/shadow.js new file mode 100644 index 00000000000..7008fcf3f93 --- /dev/null +++ b/tests/manual/shadow.js @@ -0,0 +1,276 @@ +/** + * @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, CSSStyleSheet */ + +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'; +import BalloonToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js'; +import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment.js'; +import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage.js'; +import AutoLink from '@ckeditor/ckeditor5-link/src/autolink.js'; +import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js'; +import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock.js'; +import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage.js'; +import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js'; +import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor.js'; +import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor.js'; +import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily.js'; +import FontSize from '@ckeditor/ckeditor5-font/src/fontsize.js'; +import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js'; +import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline.js'; +import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed.js'; +import HtmlComment from '@ckeditor/ckeditor5-html-support/src/htmlcomment.js'; +import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize.js'; +import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert.js'; +import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock.js'; +import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage.js'; +import ListProperties from '@ckeditor/ckeditor5-list/src/listproperties.js'; +import Mention from '@ckeditor/ckeditor5-mention/src/mention.js'; +import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak.js'; +import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice.js'; +import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js'; +import ShowBlocks from '@ckeditor/ckeditor5-show-blocks/src/showblocks.js'; +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js'; +import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js'; +import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js'; +import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js'; +import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js'; +import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js'; +import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties.js'; +import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties.js'; +import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption.js'; +import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize.js'; +import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation.js'; +import TextPartLanguage from '@ckeditor/ckeditor5-language/src/textpartlanguage.js'; +import TodoList from '@ckeditor/ckeditor5-list/src/todolist.js'; +import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js'; +import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount.js'; +import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices.js'; +import Style from '@ckeditor/ckeditor5-style/src/style.js'; +import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport.js'; + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config.js'; + +const editorComponent = document.querySelector( '#editor-component' ); + +const editorShadow = editorComponent.attachShadow( { mode: 'open' } ); +const bodyCollectionWrapperShadow = document.querySelector( '.ck-body-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.replace( /:root/g, ':host' ) ); + + editorShadow.adoptedStyleSheets = [ shadowSheet ]; + bodyCollectionWrapperShadow.adoptedStyleSheets = [ shadowSheet ]; + + sheet.disabled = true; + break; + } +} + +ClassicEditor + .create( editorElement, { + initialData: document.querySelector( '#editor-data' ).innerHTML, + plugins: [ + ArticlePluginSet, Underline, Strikethrough, Superscript, Subscript, Code, RemoveFormat, + FindAndReplace, FontColor, FontBackgroundColor, FontFamily, FontSize, Highlight, + CodeBlock, TodoList, ListProperties, TableProperties, TableCellProperties, TableCaption, TableColumnResize, + EasyImage, ImageResize, ImageInsert, LinkImage, AutoImage, HtmlEmbed, HtmlComment, + AutoLink, Mention, TextTransformation, + Alignment, IndentBlock, + PasteFromOffice, PageBreak, HorizontalLine, ShowBlocks, + SpecialCharacters, SpecialCharactersEssentials, WordCount, + CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport, + BlockToolbar, BalloonToolbar + ], + toolbar: [ + 'heading', 'style', + '|', + 'removeFormat', 'bold', 'italic', 'strikethrough', 'underline', 'code', 'subscript', 'superscript', 'link', + '|', + 'highlight', 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', + '|', + 'bulletedList', 'numberedList', 'todoList', + '|', + 'blockQuote', 'insertImage', 'insertTable', 'mediaEmbed', 'codeBlock', + '|', + 'htmlEmbed', + '|', + 'alignment', 'outdent', 'indent', + '|', + 'pageBreak', 'horizontalLine', 'specialCharacters', + '|', + 'textPartLanguage', + '|', + 'sourceEditing', 'showBlocks', + '|', + 'undo', 'redo', 'findAndReplace' + ], + blockToolbar: [ + 'heading', + '|', + 'bulletedList', + 'numberedList', + '|', + 'blockQuote', + 'insertTable', + 'mediaEmbed' + ], + balloonToolbar: [ 'bold', 'italic', 'link' ], + cloudServices: CS_CONFIG, + table: { + contentToolbar: [ + 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties', 'toggleTableCaption' + ] + }, + image: { + styles: [ + 'alignCenter', + 'alignLeft', + 'alignRight' + ], + resizeOptions: [ + { + name: 'resizeImage:original', + label: 'Original size', + value: null + }, + { + name: 'resizeImage:50', + label: '50%', + value: '50' + }, + { + name: 'resizeImage:75', + label: '75%', + value: '75' + } + ], + toolbar: [ + 'imageTextAlternative', 'toggleImageCaption', '|', + 'imageStyle:inline', 'imageStyle:breakText', 'imageStyle:wrapText', '|', + 'resizeImage' + ] + }, + placeholder: 'Type the content here!', + mention: { + feeds: [ + { + marker: '@', + feed: [ + '@apple', '@bears', '@brownie', '@cake', '@cake', '@candy', '@canes', '@chocolate', '@cookie', '@cotton', '@cream', + '@cupcake', '@danish', '@donut', '@dragée', '@fruitcake', '@gingerbread', '@gummi', '@ice', '@jelly-o', + '@liquorice', '@macaroon', '@marzipan', '@oat', '@pie', '@plum', '@pudding', '@sesame', '@snaps', '@soufflé', + '@sugar', '@sweet', '@topping', '@wafer' + ], + minimumCharacters: 1 + } + ] + }, + menuBar: { + isVisible: true + }, + link: { + decorators: { + isExternal: { + mode: 'manual', + label: 'Open in a new tab', + attributes: { + target: '_blank', + rel: 'noopener noreferrer' + } + }, + isDownloadable: { + mode: 'manual', + label: 'Downloadable', + attributes: { + download: 'download' + } + }, + isGallery: { + mode: 'manual', + label: 'Gallery link', + classes: 'gallery' + } + } + }, + htmlEmbed: { + showPreviews: true, + sanitizeHtml: html => ( { html, hasChange: false } ) + }, + list: { + properties: { + styles: true, + startIndex: true, + reversed: true + } + }, + style: { + definitions: [ + { + name: 'Article category', + element: 'h3', + classes: [ 'category' ] + }, + { + name: 'Title', + element: 'h2', + classes: [ 'document-title' ] + }, + { + name: 'Subtitle', + element: 'h3', + classes: [ 'document-subtitle' ] + }, + { + name: 'Info box', + element: 'p', + classes: [ 'info-box' ] + }, + { + name: 'Side quote', + element: 'blockquote', + classes: [ 'side-quote' ] + }, + { + name: 'Marker', + element: 'span', + classes: [ 'marker' ] + }, + { + name: 'Spoiler', + element: 'span', + classes: [ 'spoiler' ] + }, + { + name: 'Code (dark)', + element: 'pre', + classes: [ 'fancy-code', 'fancy-code-dark' ] + }, + { + name: 'Code (bright)', + element: 'pre', + classes: [ 'fancy-code', 'fancy-code-bright' ] + } + ] + } + } ) + .then( editor => { + window.editor = editor; + + editor.plugins.get( 'WordCount' ).on( 'update', ( evt, stats ) => { + console.log( `Characters: ${ stats.characters }, words: ${ stats.words }.` ); + } ); + } ) + .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