Parcourir la source

Added `utils.viewToModelPositionOutsideModelElement`.
Changed inline-widget manual sample to use it.
Removed CSS-created '{}' from manual sample.

Szymon Cofalik il y a 7 ans
Parent
commit
e0f51ba6e5

+ 60 - 0
packages/ckeditor5-widget/src/utils.js

@@ -292,6 +292,66 @@ export function findOptimalInsertionPosition( selection, model ) {
 	return selection.focus;
 	return selection.focus;
 }
 }
 
 
+/**
+ * Maps view position to model position if view position is inside a view inline widget which has content while
+ * the model element is empty.
+ *
+ *		editor.editing.mapper.on(
+ *			'viewToModelPosition',
+ *			viewToModelPositionOutsideModelElement( model, viewElement => viewElement.hasClass( 'placeholder' ) )
+ *		);
+ *
+ * For example:
+ *
+ *		// Model:
+ *		<placeholder type="name"></placeholder>
+ *
+ *		// View:
+ *		<span class="placeholder">name</span>
+ *
+ * In such case, view position inside `<span>` could not be correct mapped to the model.
+ *
+ * The callback will try to map the view offset of selection to an expected model position.
+ *
+ * 1. When the position is at the end (or in the middle) of the inline widget:
+ *
+ *		// View:
+ *		<p>foo <span class="placeholder">name|</span> bar</p>
+ *
+ *		// Model:
+ *		<paragraph>foo <placeholder type="name"></inline-widget>| bar</paragraph>
+ *
+ * 2. When the position is at the beginning of the inline widget:
+ *
+ *		// View:
+ *		<p>foo <span class="placeholder">|name</span> bar</p>
+ *
+ *		// Model:
+ *		<paragraph>foo |<placeholder type="name"></inline-widget> bar</paragraph>
+ *
+ * **Note:** remember to {@link module:engine/conversion/mapper~Mapper#bindElements bind} model and view element.
+ *
+ * @param {module:engine/model/model~Model} model Model instance on which the callback operates.
+ * @param {Function} viewElementMatcher Function that is passed a view element and should return `true` if the custom mapping
+ * should be applied to the given view element.
+ * @return {Function}
+ */
+export function viewToModelPositionOutsideModelElement( model, viewElementMatcher ) {
+	return ( evt, data ) => {
+		const { mapper, viewPosition } = data;
+
+		const viewParent = mapper.findMappedViewAncestor( viewPosition );
+
+		if ( !viewElementMatcher( viewParent ) ) {
+			return;
+		}
+
+		const modelParent = mapper.toModelElement( viewParent );
+
+		data.modelPosition = model.createPositionAt( modelParent, viewPosition.isAtStart ? 'before' : 'after' );
+	};
+}
+
 // Default filler offset function applied to all widget elements.
 // Default filler offset function applied to all widget elements.
 //
 //
 // @returns {null}
 // @returns {null}

+ 2 - 10
packages/ckeditor5-widget/tests/manual/inline-widget.html

@@ -1,7 +1,7 @@
 <div id="editor">
 <div id="editor">
-	<h1>Hello <placeholder>name</placeholder>!</h1>
+	<h1>Hello <placeholder>{name}</placeholder>!</h1>
 
 
-	<p>We would like to invite you to <placeholder>place</placeholder> that will take place on <placeholder>date</placeholder>.</p>
+	<p>We would like to invite you to <placeholder>{place}</placeholder> that will take place on <placeholder>{date}</placeholder>.</p>
 </div>
 </div>
 
 
 <h2>Model contents:</h2>
 <h2>Model contents:</h2>
@@ -20,14 +20,6 @@
 		display: none;
 		display: none;
 	}
 	}
 
 
-	placeholder:before{
-		content: '{'
-	}
-
-	placeholder:after{
-		content: '}'
-	}
-
 	/* This will show box when the fake selection is active. */
 	/* This will show box when the fake selection is active. */
 	.ck.ck-content div[style*="left: -9999px;"]:before {
 	.ck.ck-content div[style*="left: -9999px;"]:before {
 		background: hsla(9,100%,56%,.3);
 		background: hsla(9,100%,56%,.3);

+ 12 - 4
packages/ckeditor5-widget/tests/manual/inline-widget.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-/* global console */
+/* global console, window */
 
 
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
@@ -17,7 +17,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
-import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
+import { toWidget, viewToModelPositionOutsideModelElement } from '../../src/utils';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter';
 import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter';
@@ -57,7 +57,7 @@ class InlineWidget extends Plugin {
 					const text = viewElement.getChild( 0 );
 					const text = viewElement.getChild( 0 );
 
 
 					if ( text.is( 'text' ) ) {
 					if ( text.is( 'text' ) ) {
-						type = text.data;
+						type = text.data.slice( 1, -1 );
 					}
 					}
 				}
 				}
 
 
@@ -65,12 +65,18 @@ class InlineWidget extends Plugin {
 			}
 			}
 		} );
 		} );
 
 
+		editor.editing.mapper.on(
+			'viewToModelPosition',
+			viewToModelPositionOutsideModelElement( editor.model, viewElement => viewElement.name == 'placeholder' )
+		);
+
 		this._createToolbarButton();
 		this._createToolbarButton();
 
 
 		function createPlaceholderView( modelItem, viewWriter ) {
 		function createPlaceholderView( modelItem, viewWriter ) {
 			const widgetElement = viewWriter.createContainerElement( 'placeholder' );
 			const widgetElement = viewWriter.createContainerElement( 'placeholder' );
+			const viewText = viewWriter.createText( '{' + modelItem.getAttribute( 'type' ) + '}' );
 
 
-			viewWriter.insert( viewWriter.createPositionAt( widgetElement, 0 ), viewWriter.createText( modelItem.getAttribute( 'type' ) ) );
+			viewWriter.insert( viewWriter.createPositionAt( widgetElement, 0 ), viewText );
 
 
 			return widgetElement;
 			return widgetElement;
 		}
 		}
@@ -112,6 +118,8 @@ ClassicEditor
 		toolbar: [ 'heading', '|', 'bold', '|', 'placeholder', '|', 'insertTable', '|', 'undo', 'redo' ]
 		toolbar: [ 'heading', '|', 'bold', '|', 'placeholder', '|', 'insertTable', '|', 'undo', 'redo' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
+		window.editor = editor;
+
 		editor.model.document.on( 'change', () => {
 		editor.model.document.on( 'change', () => {
 			printModelContents( editor );
 			printModelContents( editor );
 		} );
 		} );

+ 87 - 2
packages/ckeditor5-widget/tests/utils.js

@@ -6,8 +6,9 @@
 /* global document */
 /* global document */
 
 
 import DowncastWriter from '@ckeditor/ckeditor5-engine/src/view/downcastwriter';
 import DowncastWriter from '@ckeditor/ckeditor5-engine/src/view/downcastwriter';
-import Text from '@ckeditor/ckeditor5-engine/src/view/text';
+import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
 import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
 import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
+import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
 import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement';
 import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement';
 import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
 import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
 import {
 import {
@@ -18,6 +19,7 @@ import {
 	toWidgetEditable,
 	toWidgetEditable,
 	setHighlightHandling,
 	setHighlightHandling,
 	findOptimalInsertionPosition,
 	findOptimalInsertionPosition,
+	viewToModelPositionOutsideModelElement,
 	WIDGET_CLASS_NAME
 	WIDGET_CLASS_NAME
 } from '../src/utils';
 } from '../src/utils';
 import UIElement from '@ckeditor/ckeditor5-engine/src/view/uielement';
 import UIElement from '@ckeditor/ckeditor5-engine/src/view/uielement';
@@ -25,6 +27,9 @@ import env from '@ckeditor/ckeditor5-utils/src/env';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import Model from '@ckeditor/ckeditor5-engine/src/model/model';
 import Model from '@ckeditor/ckeditor5-engine/src/model/model';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import Mapper from '@ckeditor/ckeditor5-engine/src/conversion/mapper';
+import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
+import ModelText from '@ckeditor/ckeditor5-engine/src/model/text';
 
 
 describe( 'widget utils', () => {
 describe( 'widget utils', () => {
 	let element, writer, viewDocument;
 	let element, writer, viewDocument;
@@ -146,7 +151,7 @@ describe( 'widget utils', () => {
 		} );
 		} );
 
 
 		it( 'should return false for text node', () => {
 		it( 'should return false for text node', () => {
-			expect( isWidget( new Text( 'p' ) ) ).to.be.false;
+			expect( isWidget( new ViewText( 'p' ) ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 
@@ -449,4 +454,84 @@ describe( 'widget utils', () => {
 			expect( pos.path ).to.deep.equal( [ 3 ] );
 			expect( pos.path ).to.deep.equal( [ 3 ] );
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'viewToModelPositionOutsideModelElement', () => {
+		let mapper, model, modelP, viewP, viewXyz, modelSpan, viewSpan;
+
+		beforeEach( () => {
+			mapper = new Mapper();
+			model = new Model();
+
+			const modelFoo = new ModelText( 'foo' );
+			modelSpan = new ModelElement( 'span' );
+			const modelBar = new ModelText( 'bar' );
+			modelP = new ModelElement( 'p', null, [ modelFoo, modelSpan, modelBar ] );
+
+			const viewFoo = new ViewText( 'foo' );
+			viewXyz = new ViewText( 'xyz' );
+			viewSpan = new ViewElement( 'span', null, viewXyz );
+			const viewBar = new ViewText( 'bar' );
+			viewP = new ViewElement( 'p', null, [ viewFoo, viewSpan, viewBar ] );
+
+			mapper.bindElements( modelP, viewP );
+			mapper.bindElements( modelSpan, viewSpan );
+		} );
+
+		it( 'should map view position that is at the beginning of the view element to a position before the model element', () => {
+			mapper.on( 'viewToModelPosition', viewToModelPositionOutsideModelElement( model, viewElement => viewElement.name == 'span' ) );
+
+			// View:
+			// <p>foo<span>|xyz</span>bar</p>.
+			const viewPosition = new ViewPosition( viewXyz, 0 );
+
+			// Model:
+			// <p>foo|<span></span>bar</p>.
+			const modelPosition = mapper.toModelPosition( viewPosition );
+
+			expect( modelPosition.path ).to.deep.equal( [ 3 ] );
+		} );
+
+		it( 'should map view position that is in the middle of the view element to a position after the model element', () => {
+			mapper.on( 'viewToModelPosition', viewToModelPositionOutsideModelElement( model, viewElement => viewElement.name == 'span' ) );
+
+			// View:
+			// <p>foo<span>x|yz</span>bar</p>.
+			const viewPosition = new ViewPosition( viewXyz, 1 );
+
+			// Model:
+			// <p>foo|<span></span>bar</p>.
+			const modelPosition = mapper.toModelPosition( viewPosition );
+
+			expect( modelPosition.path ).to.deep.equal( [ 4 ] );
+		} );
+
+		it( 'should map view position that is at the end of the view element to a position after the model element', () => {
+			mapper.on( 'viewToModelPosition', viewToModelPositionOutsideModelElement( model, viewElement => viewElement.name == 'span' ) );
+
+			// View:
+			// <p>foo<span>xyz|</span>bar</p>.
+			const viewPosition = new ViewPosition( viewXyz, 3 );
+
+			// Model:
+			// <p>foo<span></span>|bar</p>.
+			const modelPosition = mapper.toModelPosition( viewPosition );
+
+			expect( modelPosition.path ).to.deep.equal( [ 4 ] );
+		} );
+
+		it( 'should not fire if view element is not matched', () => {
+			mapper.on( 'viewToModelPosition', viewToModelPositionOutsideModelElement( model, () => false ) );
+
+			// View:
+			// <p>foo<span>x|yz</span>bar</p>.
+			const viewPosition = new ViewPosition( viewXyz, 1 );
+
+			// Model:
+			// <p>foo<span>x|yz</span>bar</p>.
+			modelSpan._appendChild( new ModelText( 'xyz' ) );
+			const modelPosition = mapper.toModelPosition( viewPosition );
+
+			expect( modelPosition.path ).to.deep.equal( [ 3, 1 ] );
+		} );
+	} );
 } );
 } );