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 cellSecond cell
+ +
+
+ +

Hello World

+

Closing text.

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. *