diff --git a/packages/ckeditor5-clipboard/src/clipboardobserver.ts b/packages/ckeditor5-clipboard/src/clipboardobserver.ts
index bb6759debf5..614fdd13cd5 100644
--- a/packages/ckeditor5-clipboard/src/clipboardobserver.ts
+++ b/packages/ckeditor5-clipboard/src/clipboardobserver.ts
@@ -12,6 +12,7 @@ import { EventInfo } from '@ckeditor/ckeditor5-utils';
import {
DataTransfer,
DomEventObserver,
+ getPointViewRange,
type DomEventData,
type EditingView,
type ViewDocumentFragment,
@@ -92,7 +93,7 @@ export default class ClipboardObserver extends DomEventObserver<
};
if ( domEvent.type == 'drop' || domEvent.type == 'dragover' ) {
- evtData.dropRange = getDropViewRange( this.view, domEvent as DragEvent );
+ evtData.dropRange = getPointViewRange( this.view, domEvent as DragEvent );
}
this.fire( domEvent.type, domEvent, evtData );
@@ -115,30 +116,6 @@ export interface ClipboardEventData {
dropRange?: ViewRange | null;
}
-function getDropViewRange( view: EditingView, domEvent: DragEvent & { rangeParent?: Node; rangeOffset?: number } ) {
- const domDoc = ( domEvent.target as Node ).ownerDocument!;
- const x = domEvent.clientX;
- const y = domEvent.clientY;
- let domRange;
-
- // Webkit & Blink.
- if ( domDoc.caretRangeFromPoint && domDoc.caretRangeFromPoint( x, y ) ) {
- domRange = domDoc.caretRangeFromPoint( x, y );
- }
- // FF.
- else if ( domEvent.rangeParent ) {
- domRange = domDoc.createRange();
- domRange.setStart( domEvent.rangeParent, domEvent.rangeOffset! );
- domRange.collapse( true );
- }
-
- if ( domRange ) {
- return view.domConverter.domRangeToView( domRange );
- }
-
- return null;
-}
-
/**
* Fired as a continuation of the {@link module:engine/view/document~Document#event:paste} and
* {@link module:engine/view/document~Document#event:drop} events.
diff --git a/packages/ckeditor5-editor-multi-root/package.json b/packages/ckeditor5-editor-multi-root/package.json
index 5882fbe28b4..5846e748a4c 100644
--- a/packages/ckeditor5-editor-multi-root/package.json
+++ b/packages/ckeditor5-editor-multi-root/package.json
@@ -22,7 +22,6 @@
"devDependencies": {
"@ckeditor/ckeditor5-basic-styles": "43.0.0",
"@ckeditor/ckeditor5-dev-utils": "^42.0.0",
- "@ckeditor/ckeditor5-essentials": "43.0.0",
"@ckeditor/ckeditor5-enter": "43.0.0",
"@ckeditor/ckeditor5-heading": "43.0.0",
"@ckeditor/ckeditor5-paragraph": "43.0.0",
@@ -32,6 +31,10 @@
"@ckeditor/ckeditor5-undo": "43.0.0",
"@ckeditor/ckeditor5-clipboard": "43.0.0",
"@ckeditor/ckeditor5-watchdog": "43.0.0",
+ "@ckeditor/ckeditor5-image": "43.0.0",
+ "@ckeditor/ckeditor5-link": "43.0.0",
+ "@ckeditor/ckeditor5-adapter-ckfinder": "43.0.0",
+ "@ckeditor/ckeditor5-ckfinder": "43.0.0",
"typescript": "5.0.4",
"webpack": "^5.58.1",
"webpack-cli": "^4.9.0"
diff --git a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.html b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.html
index 5c76b005872..2a896841058 100644
--- a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.html
+++ b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.html
@@ -1,3 +1,9 @@
+
+
+
+
+
+
@@ -12,7 +18,19 @@
The toolbar
The editable
Exciting intro text to an article.
-
Exciting news!
Lorem ipsum dolor sit amet.
+
+
+
+ First cell |
+ Second cell |
+
+
+
+
+
+
+
Hello World
+
diff --git a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.js b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.js
index ad2a372ac8a..e3dde250bf4 100644
--- a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.js
+++ b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.js
@@ -10,7 +10,13 @@ import Heading from '@ckeditor/ckeditor5-heading/src/heading.js';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
-import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
+import Image from '@ckeditor/ckeditor5-image/src/image.js';
+import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage.js';
+import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert.js';
+import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage.js';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js';
+import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter.js';
+import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder.js';
const editorData = {
intro: document.querySelector( '#editor-intro' ),
@@ -23,8 +29,26 @@ let editor;
function initEditor() {
MultiRootEditor
.create( editorData, {
- plugins: [ Essentials, Paragraph, Heading, Bold, Italic ],
- toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
+ plugins: [
+ Paragraph, Heading, Bold, Italic,
+ Image, ImageInsert, AutoImage, LinkImage,
+ ArticlePluginSet, CKFinderUploadAdapter, CKFinder
+ ],
+ toolbar: [
+ 'heading', '|', 'bold', 'italic', 'undo', 'redo', '|',
+ 'insertImage', 'insertTable', 'blockQuote'
+ ],
+ image: {
+ toolbar: [
+ 'imageStyle:inline', 'imageStyle:block',
+ 'imageStyle:wrapText', '|', 'toggleImageCaption',
+ 'imageTextAlternative'
+ ]
+ },
+ ckfinder: {
+ // eslint-disable-next-line max-len
+ uploadUrl: 'https://ckeditor.com/apps/ckfinder/3.5.0/core/connector/php/connector.php?command=QuickUpload&type=Files&responseType=json'
+ }
} )
.then( newEditor => {
console.log( 'Editor was initialized', newEditor );
@@ -55,3 +79,5 @@ function destroyEditor() {
document.getElementById( 'initEditor' ).addEventListener( 'click', initEditor );
document.getElementById( 'destroyEditor' ).addEventListener( 'click', destroyEditor );
+
+initEditor();
diff --git a/packages/ckeditor5-engine/src/index.ts b/packages/ckeditor5-engine/src/index.ts
index 96d7800e5ca..ccd18f70b17 100644
--- a/packages/ckeditor5-engine/src/index.ts
+++ b/packages/ckeditor5-engine/src/index.ts
@@ -133,7 +133,7 @@ export type { SelectionChangeRangeEvent } from './model/selection.js';
export { default as DataTransfer } from './view/datatransfer.js';
export { default as DomConverter } from './view/domconverter.js';
export { default as Renderer } from './view/renderer.js';
-export { default as EditingView } from './view/view.js';
+export { default as EditingView, getPointViewRange } from './view/view.js';
export { default as ViewDocument } from './view/document.js';
export { default as ViewText } from './view/text.js';
export { default as ViewElement, type ElementAttributes as ViewElementAttributes } from './view/element.js';
diff --git a/packages/ckeditor5-engine/src/view/view.ts b/packages/ckeditor5-engine/src/view/view.ts
index 0a5812dd94f..b084a010067 100644
--- a/packages/ckeditor5-engine/src/view/view.ts
+++ b/packages/ckeditor5-engine/src/view/view.ts
@@ -810,6 +810,44 @@ export default class View extends /* #__PURE__ */ ObservableMixin() {
}
}
+/**
+ * Get the view range from the given DOM event.
+ *
+ * @param view The view instance.
+ * @param domEvent The DOM event.
+ * @returns The view range or `null` if the range cannot be created.
+ */
+export function getPointViewRange(
+ view: View,
+ domEvent: MouseEvent & {
+ rangeParent?: HTMLElement;
+ rangeOffset?: number;
+ }
+): Range | null {
+ const domDoc = ( domEvent.target as HTMLElement ).ownerDocument!;
+ const x = domEvent.clientX;
+ const y = domEvent.clientY;
+ let domRange;
+
+ // Webkit & Blink.
+ if ( domDoc.caretRangeFromPoint && domDoc.caretRangeFromPoint( x, y ) ) {
+ domRange = domDoc.caretRangeFromPoint( x, y );
+ }
+
+ // FF.
+ else if ( domEvent.rangeParent ) {
+ domRange = domDoc.createRange();
+ domRange.setStart( domEvent.rangeParent, domEvent.rangeOffset! );
+ domRange.collapse( true );
+ }
+
+ if ( domRange ) {
+ return view.domConverter.domRangeToView( domRange );
+ }
+
+ return null;
+}
+
/**
* Fired after a topmost {@link module:engine/view/view~View#change change block} and all
* {@link module:engine/view/document~Document#registerPostFixer post-fixers} are executed.
diff --git a/packages/ckeditor5-widget/src/widget.ts b/packages/ckeditor5-widget/src/widget.ts
index 67184ec307e..83f8746ac83 100644
--- a/packages/ckeditor5-widget/src/widget.ts
+++ b/packages/ckeditor5-widget/src/widget.ts
@@ -12,6 +12,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core';
import {
MouseObserver,
TreeWalker,
+ getPointViewRange,
type DomEventData,
type DowncastSelectionEvent,
type DowncastWriter,
@@ -288,17 +289,39 @@ export default class Widget extends Plugin {
return;
}
- // Do nothing for single or double click inside nested editable.
- if ( isInsideNestedEditable( element ) ) {
- return;
- }
-
// If target is not a widget element - check if one of the ancestors is.
if ( !isWidget( element ) ) {
- element = element.findAncestor( isWidget );
+ const widgetElement = element.findAncestor( isWidget );
- if ( !element ) {
- return;
+ if ( !widgetElement || isInsideNestedEditable( element ) ) {
+ const editableElement = findClosestEditableAncestor( element );
+
+ if ( !editableElement || !element.childCount ) {
+ return;
+ }
+
+ // Pick view range from the point where the mouse was clicked.
+ const clickTargetFromPoint = ( () => {
+ const range = getPointViewRange( view, domEventData!.domEvent );
+
+ return range && range.start.parent;
+ } )();
+
+ // If the click target is a text node, we need to get the parent element.
+ const clickElementFromPoint = clickTargetFromPoint && clickTargetFromPoint.is( '$text' ) ?
+ clickTargetFromPoint.parent : clickTargetFromPoint;
+
+ // If the element is a widget, we need to select the widget itself otherwise we need to select the first ancestor widget.
+ if ( clickElementFromPoint && clickElementFromPoint.is( 'element' ) ) {
+ element = isWidget( clickElementFromPoint ) ?
+ clickElementFromPoint : clickElementFromPoint.findAncestor( isWidget );
+ }
+
+ if ( !element ) {
+ return;
+ }
+ } else {
+ element = widgetElement;
}
}
@@ -633,6 +656,17 @@ function isInsideNestedEditable( element: ViewElement ) {
return false;
}
+/**
+ * Returns the closest editable ancestor element, it includes the element itself.
+ */
+function findClosestEditableAncestor( element: ViewElement ) {
+ if ( element.is( 'editableElement' ) ) {
+ return element;
+ }
+
+ return element.findAncestor( element => element.is( 'editableElement' ) );
+}
+
/**
* Checks whether the specified `element` is a child of the `parent` element.
*