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 @@
+
+
+
+
+
+
+
+
+
+
+
Lists in the table
+
+
+
+
+
Bulleted list
+
Numbered list
+
To do list
+
+
+
+
+
UL List item 1
+
UL List item 2
+
+
+
+
+
OL List item 1
+
OL List item 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Basic features overview
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit.1
+
Basic styles
+
Ad alias, architecto culpa cumque dignissimos dolor eos incidunt ipsa itaquelaboriosam magnam molestias nihil numquam odit quam, suscipit veritatis voluptate voluptatum.2
+
+ Details
+ Something small enough to escape casual notice.
+
+
+
+
<video> tag as HTML snippet
+
+
+
+
+
<iframe> tag as HTML snippet
+
+
+
+
+
Paste from Office
+
Paste here:
+
+
+
Text part language
+
+
+ Un lenguaje (del provenzal lenguatge y este del latín lingua) es un sistema de comunicación
+ estructurado para el que existe un contexto de uso y ciertos principios combinatorios formales. Existen
+ contextos tanto naturales como artificiales.
+
+
+
+
+ اللغة نسق من الإشارات والرموز، يشكل أداة من أدوات المعرفة، وتعتبر اللغة أهم وسائل التفاهم
+ والاحتكاك بين أفراد امجتمع في جميع ميادين الحياة. وبدون اللغة يتعذر نشاط الناس المعرفي.
+
+