Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Early PoC of CKEditor5 inside a Shadow DOM. #16975

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6a11868
Early PoC of CKEditor5 inside a Shadow DOM.
niegowski Aug 26, 2024
b498a03
Merge branch 'refs/heads/master' into ck/shadow-poc
niegowski Aug 27, 2024
c10f714
Add support for drop target in a shadow DOM.
niegowski Aug 27, 2024
a2d80c0
The body collection in a shadow root. Listening for global events on …
niegowski Sep 2, 2024
c612671
CSS root properties should be translated to the host element.
niegowski Sep 3, 2024
97618c8
Marked shadow root places in code. Handling of block toolbar dragging.
niegowski Sep 3, 2024
b6ab662
Component focus handling.
niegowski Sep 3, 2024
3c1d883
Merge branch 'refs/heads/master' into ck/shadow-poc
niegowski Sep 6, 2024
fedf8be
Active element handling updated to work in shadow DOM.
niegowski Sep 6, 2024
d81a4db
Added some shadow helpers.
niegowski Sep 9, 2024
0c71edf
Added notes.
niegowski Sep 10, 2024
d6134b5
Merge branch 'refs/heads/master' into ck/shadow-poc
niegowski Sep 27, 2024
3ca2b7d
Merge branch 'refs/heads/master' into ck/shadow-poc
niegowski Oct 4, 2024
2ae8696
Fixed checking if some element is connected with the DOM document.
niegowski Oct 4, 2024
c744843
Avoid selection fixing on multiple table cells selection.
niegowski Oct 4, 2024
b9573d4
Fixed image resizer in shadow dom.
niegowski Oct 7, 2024
8b5daff
Code cleaning.
niegowski Oct 7, 2024
54a65fe
Updated focus handling.
niegowski Oct 7, 2024
97b974e
Active element fixes.
niegowski Oct 7, 2024
e1a8bdf
Added comment about toolbar focus cycling.
niegowski Oct 7, 2024
5a62869
Manual test clear.
niegowski Oct 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Active element handling updated to work in shadow DOM.
niegowski committed Sep 6, 2024
commit fedf8beaf84ee57e345979d438138ddbd6cf45a1
2 changes: 2 additions & 0 deletions packages/ckeditor5-ckbox/src/ckboxcommand.ts
Original file line number Diff line number Diff line change
@@ -202,6 +202,8 @@ export default class CKBoxCommand extends Command {
}

// TODO ShadowRoot
// - can we append it to the body collection?
// - does CKBox support Shadow DOM?
this._wrapper = createElement( document, 'div', { class: 'ck ckbox-wrapper' } );
document.body.appendChild( this._wrapper );

Original file line number Diff line number Diff line change
@@ -109,6 +109,8 @@ export default class CKBoxImageEditCommand extends Command {
}

// TODO ShadowRoot
// - can we append it to the body collection?
// - does CKBox support Shadow DOM?
const wrapper = createElement( document, 'div', { class: 'ck ckbox-wrapper' } );

this._wrapper = wrapper;
2 changes: 2 additions & 0 deletions packages/ckeditor5-clipboard/src/dragdrop.ts
Original file line number Diff line number Diff line change
@@ -670,6 +670,8 @@ export default class DragDrop extends Plugin {
} );

// TODO ShadowRoot
// - can we append it to the body collection?
// - is the preview generated correctly in the Shadow DOM
global.document.body.appendChild( this._previewContainer );
} else if ( this._previewContainer.firstElementChild ) {
this._previewContainer.removeChild( this._previewContainer.firstElementChild );
7 changes: 6 additions & 1 deletion packages/ckeditor5-clipboard/src/dragdropblocktoolbar.ts
Original file line number Diff line number Diff line change
@@ -68,7 +68,9 @@ export default class DragDropBlockToolbar extends Plugin {
const element = blockToolbar.buttonView.element!;

this._domEmitter.listenTo( element, 'dragstart', ( evt, data ) => this._handleBlockDragStart( data ) );

// TODO ShadowRoot
// - those events will propagate across the shadow DOM boundary (bubbles and composed flags set)
this._domEmitter.listenTo( global.document, 'dragover', ( evt, data ) => this._handleBlockDragging( data ) );
this._domEmitter.listenTo( global.document, 'drop', ( evt, data ) => this._handleBlockDragging( data ) );
this._domEmitter.listenTo( global.document, 'dragend', () => this._handleBlockDragEnd(), { useCapture: true } );
@@ -133,7 +135,10 @@ export default class DragDropBlockToolbar extends Plugin {

let target = document.elementFromPoint( clientX, clientY );

// TODO ShadowRoot - this is a workaround, works this way only in open shadow root
// TODO ShadowRoot
// - this is a workaround, works this way only in open shadow root
// - we should use map of known shadow roots and not depend on the shadowRoot property (it's there only for open mode)
// - the ShadowRoot#elementFromPoint() is non-standard but available in all browsers.
if ( target && target.shadowRoot && target.shadowRoot.elementFromPoint ) {
target = target.shadowRoot.elementFromPoint( clientX, clientY );
}
2 changes: 2 additions & 0 deletions packages/ckeditor5-clipboard/src/dragdroptarget.ts
Original file line number Diff line number Diff line change
@@ -521,6 +521,8 @@ function findScrollableElement( domNode: HTMLElement ): HTMLElement {
let domElement: HTMLElement = domNode;

do {
// TODO ShadowRoot
// - use helper for easier parent element access
domElement = domElement.parentNode instanceof ShadowRoot ?
domElement.parentNode.host as HTMLElement :
domElement.parentElement!;
5 changes: 3 additions & 2 deletions packages/ckeditor5-engine/src/view/domconverter.ts
Original file line number Diff line number Diff line change
@@ -1091,7 +1091,8 @@ export default class DomConverter {
public focus( viewEditable: EditableElement ): void {
const domEditable = this.mapViewToDom( viewEditable );

if ( domEditable && domEditable.ownerDocument.activeElement !== domEditable ) {
// TODO ShadowRoot
if ( domEditable && domEditable.getRootNode().activeElement !== domEditable ) {
// Save the scrollX and scrollY positions before the focus.
const { scrollX, scrollY } = global.window;
const scrollPositions: Array<[ number, number ]> = [];
@@ -1850,7 +1851,7 @@ function forEachDomElementAncestor( element: DomElement, callback: ( node: DomEl

while ( node ) {
callback( node );
node = node.parentElement;
node = node.parentElement; // TODO ShadowRoot
}
}

19 changes: 11 additions & 8 deletions packages/ckeditor5-engine/src/view/observer/selectionobserver.ts
Original file line number Diff line number Diff line change
@@ -115,8 +115,6 @@ export default class SelectionObserver extends Observer {
* @inheritDoc
*/
public override observe( domElement: HTMLElement ): void {
const domDocument = domElement.ownerDocument;

const startDocumentIsSelecting = () => {
this.document.isSelecting = true;

@@ -131,7 +129,7 @@ export default class SelectionObserver extends Observer {

// Make sure that model selection is up-to-date at the end of selecting process.
// Sometimes `selectionchange` events could arrive after the `mouseup` event and that selection could be already outdated.
this._handleSelectionChange( domDocument );
this._handleSelectionChange( domElement );

this.document.isSelecting = false;

@@ -147,16 +145,19 @@ export default class SelectionObserver extends Observer {
this.listenTo( domElement, 'keydown', endDocumentIsSelecting, { priority: 'highest', useCapture: true } );
this.listenTo( domElement, 'keyup', endDocumentIsSelecting, { priority: 'highest', useCapture: true } );

const domDocument = domElement.ownerDocument;

// Add document-wide listeners only once. This method could be called for multiple editing roots.
// TODO ShadowRoot
if ( this._documents.has( domDocument ) ) {
return;
}

// This listener is using capture mode to make sure that selection is upcasted before any other
// handler would like to check it and update (for example table multi cell selection).
// TODO ShadowRoot - this event will propagate across the shadow DOM boundary (bubbles and composed flags set)
this.listenTo( domDocument, 'mouseup', endDocumentIsSelecting, { priority: 'highest', useCapture: true } );

// TODO ShadowRoot - this event is always fired from the document, even inside a Shadow DOM.
this.listenTo( domDocument, 'selectionchange', ( evt, domEvent ) => {
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // _debouncedLine();
@@ -182,6 +183,7 @@ export default class SelectionObserver extends Observer {
return;
}

// TODO ShadowRoot - this will not work if separate roots are in separate shadow DOMs
this._handleSelectionChange( domElement );

// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
@@ -207,7 +209,8 @@ export default class SelectionObserver extends Observer {
// @if CK_DEBUG_TYPING // );
// @if CK_DEBUG_TYPING // }

this._handleSelectionChange( domDocument );
// TODO ShadowRoot - this will not work if separate roots are in separate shadow DOMs
this._handleSelectionChange( domElement );

// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.groupEnd();
@@ -248,14 +251,14 @@ export default class SelectionObserver extends Observer {
* a selection changes and fires {@link module:engine/view/document~Document#event:selectionChange} event on every change
* and {@link module:engine/view/document~Document#event:selectionChangeDone} when a selection stop changing.
*
* @param domDocument DOM document.
* @param domElement DOM element.
*/
private _handleSelectionChange( domDocument: Document ) {
private _handleSelectionChange( domElement: HTMLElement ) {
if ( !this.isEnabled ) {
return;
}

const domSelection = getSelection( domDocument )!;
const domSelection = getSelection( domElement )!;

if ( this.checkShouldIgnoreEventFromTarget( domSelection.anchorNode! ) ) {
return;
1 change: 1 addition & 0 deletions packages/ckeditor5-engine/src/view/renderer.ts
Original file line number Diff line number Diff line change
@@ -1095,6 +1095,7 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
const domSelection = doc.getSelection()!;

if ( domSelection.rangeCount ) {
// TODO ShadowRoot - the activeElement of the closest ShadowRoot?
const activeDomElement = doc.activeElement!;
const viewElement = this.domConverter.mapDomToView( activeDomElement as DomElement );

5 changes: 3 additions & 2 deletions packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts
Original file line number Diff line number Diff line change
@@ -42,8 +42,9 @@ export default function clickOutsideHandler(

// Check if `composedPath` is `undefined` in case the browser does not support native shadow DOM.
// Can be removed when all supported browsers support native shadow DOM.
// TODO ShadowRoot This won't work for closed shadow root.
// We probably should listen to all shadow roots we know of and have access to.
// TODO ShadowRoot
// - This won't work for closed shadow root.
// - We probably should listen to all shadow roots we know of and have access to.
const path = typeof domEvt.composedPath == 'function' ? domEvt.composedPath() : [];

const contextElementsList = typeof contextElements == 'function' ? contextElements() : contextElements;
Original file line number Diff line number Diff line change
@@ -303,7 +303,6 @@ export default class DropdownMenuNestedMenuView extends View implements Focusabl
keystrokes.listenTo( panelView.element! );
panelView.pin( {
positions: this._panelPositions,
// TODO ShadowRoot
limiter: global.document.body,
element: panelView.element!,
target: buttonView.element!,
4 changes: 2 additions & 2 deletions packages/ckeditor5-ui/src/dropdown/utils.ts
Original file line number Diff line number Diff line change
@@ -606,8 +606,8 @@ function focusDropdownButtonOnClose( dropdownView: DropdownView ) {
// If the dropdown was closed, move the focus back to the button (#12125).
// Don't touch the focus, if it moved somewhere else (e.g. moved to the editing root on #execute) (#12178).
// Note: Don't use the state of the DropdownView#focusTracker here. It fires #blur with the timeout.
// TODO ShadowRoot
if ( elements.some( element => element.contains( global.document.activeElement ) ) ) {
// TODO ShadowRoot - the activeElement is valid for the closest ShadowRoot
if ( elements.some( element => element.getRootNode().activeElement && element.contains( element.getRootNode().activeElement ) ) ) {
dropdownView.buttonView.focus();
}
} );
4 changes: 3 additions & 1 deletion packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.ts
Original file line number Diff line number Diff line change
@@ -399,9 +399,11 @@ export default class BalloonPanelView extends View {
}

let targetElement = getDomElement( options.target );
// TODO ShadowRoot
const limiterElement = options.limiter ? getDomElement( options.limiter ) : global.document.body;

// TODO ShadowRoot
// - we need to listen to the scroll event on every ShadowRoot
// (it is not composed and does not propagate to parent DOM)
// Then we need to listen on scroll event of eny element in the document.
this.listenTo( global.document, 'scroll', ( evt, domEvt ) => {
const scrollTarget = domEvt.target as Element;
1 change: 1 addition & 0 deletions packages/ckeditor5-ui/src/tooltipmanager.ts
Original file line number Diff line number Diff line change
@@ -192,6 +192,7 @@ export default class TooltipManager extends /* #__PURE__ */ DomEmitterMixin() {
this._pinTooltipDebounced = debounce( this._pinTooltip, 600 );
this._unpinTooltipDebounced = debounce( this._unpinTooltip, 400 );

// TODO ShadowRoot - make sure those events propagate to parent shadow DOM
this.listenTo( global.document, 'keydown', this._onKeyDown.bind( this ), { useCapture: true } );

this.listenTo( global.document, 'focus', this._onEnterOrFocus.bind( this ), { useCapture: true } );
Original file line number Diff line number Diff line change
@@ -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;

1 change: 1 addition & 0 deletions packages/ckeditor5-widget/src/utils.ts
Original file line number Diff line number Diff line change
@@ -483,6 +483,7 @@ export function calculateResizeHostAncestorWidth( domResizeHost: HTMLElement ):
let checkedElement = domResizeHostParent!;

while ( isNaN( parentWidth ) ) {
// TODO ShadowRoot
checkedElement = checkedElement.parentElement!;

if ( ++currentLevel > ancestorLevelLimit ) {