Forráskód Böngészése

Merge pull request #7237 from ckeditor/i/6775

Internal (widget): Integrated the `WidgetTypeAround` plugin with the read–only mode. Closes #6775.

Internal (widget): Integrated the `WidgetTypeAround` plugin with the `RestrictedEditingMode` plugin (see #6775).

Feature (paragraph): Implemented the `InsertParagraphCommand` registered as `'insertParagraph'` in the editor. Closes #6823. Closes #7229.

Docs (engine): Improved `Model#insertContent` API documentation (see #6775).
Kuba Niegowski 5 éve
szülő
commit
126701895d

+ 18 - 3
packages/ckeditor5-engine/src/model/model.js

@@ -370,13 +370,28 @@ export default class Model {
 	 *			editor.model.insertContent( writer.createText( 'x' ), writer.createPositionAt( doc.getRoot(), 2 ) );
 	 *		} );
 	 *
-	 * If an instance of {@link module:engine/model/selection~Selection} is passed as `selectable`
-	 * it will be moved to the target position (where the document selection should be moved after the insertion).
+	 * If you want the document selection to be moved to the inserted content, use the
+	 * {@link module:engine/model/writer~Writer#setSelection `setSelection()`} method of the writer after inserting
+	 * the content:
 	 *
 	 *		editor.model.change( writer => {
-	 *			// Insert text replacing the given selection instance.
+	 *			const paragraph = writer.createElement( 'paragraph' );
+	 *
+	 *			// Insert an empty paragraph at the beginning of the root.
+	 *			editor.model.insertContent( paragraph, writer.createPositionAt( editor.model.document.getRoot(), 0 ) );
+	 *
+	 *			// Move the document selection to the inserted paragraph.
+	 *			writer.setSelection( paragraph, 'in' );
+	 *		} );
+	 *
+	 * If an instance of the {@link module:engine/model/selection~Selection model selection} is passed as `selectable`,
+	 * the new content will be inserted at the passed selection (instead of document selection):
+	 *
+	 *		editor.model.change( writer => {
+	 *			// Create a selection in a paragraph that will be used as a place of insertion.
 	 *			const selection = writer.createSelection( paragraph, 'in' );
 	 *
+	 *			// Insert the new text at the created selection.
 	 *			editor.model.insertContent( writer.createText( 'x' ), selection );
 	 *
 	 *			// insertContent() modifies the passed selection instance so it can be used to set the document selection.

+ 0 - 1
packages/ckeditor5-highlight/src/highlightcommand.js

@@ -44,7 +44,6 @@ export default class HighlightCommand extends Command {
 	/**
 	 * Executes the command.
 	 *
-	 * @protected
 	 * @param {Object} [options] Options for the executed command.
 	 * @param {String} [options.value] The value to apply.
 	 *

+ 49 - 0
packages/ckeditor5-paragraph/src/insertparagraphcommand.js

@@ -0,0 +1,49 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module paragraph/insertparagraphcommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+/**
+ * The insert paragraph command. It inserts a new paragraph at a specific
+ * {@link module:engine/model/position~Position document position}.
+ *
+ *		// Insert a new paragraph before an element in the document.
+ *		editor.execute( 'insertParagraph', {
+ *			position: editor.model.createPositionBefore( element )
+ *		} );
+ *
+ * **Note**: This command moves the selection to the inserted paragraph.
+ *
+ * @extends module:core/command~Command
+ */
+export default class InsertParagraphCommand extends Command {
+	/**
+	 * Executes the command.
+	 *
+	 * @param {Object} options Options for the executed command.
+	 * @param {module:engine/model/position~Position} options.position The model position at which
+	 * the new paragraph will be inserted.
+	 * @fires execute
+	 */
+	execute( options ) {
+		const model = this.editor.model;
+
+		if ( !model.schema.checkChild( options.position, 'paragraph' ) ) {
+			return;
+		}
+
+		model.change( writer => {
+			const paragraph = writer.createElement( 'paragraph' );
+
+			model.insertContent( paragraph, options.position );
+
+			writer.setSelection( paragraph, 'in' );
+		} );
+	}
+}

+ 9 - 0
packages/ckeditor5-paragraph/src/paragraph.js

@@ -8,6 +8,7 @@
  */
 
 import ParagraphCommand from './paragraphcommand';
+import InsertParagraphCommand from './insertparagraphcommand';
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
@@ -16,6 +17,13 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  *
  * It introduces the `<paragraph>` element in the model which renders as a `<p>` element in the DOM and data.
  *
+ * It also brings two editors commands:
+ *
+ * * The {@link module:paragraph/paragraphcommand~ParagraphCommand `'paragraph'`} command that converts all
+ * blocks in the model selection into paragraphs.
+ * * The {@link module:paragraph/insertparagraphcommand~InsertParagraphCommand `'insertParagraph'`} command
+ * that inserts a new paragraph at a specified location in the model.
+ *
  * @extends module:core/plugin~Plugin
  */
 export default class Paragraph extends Plugin {
@@ -35,6 +43,7 @@ export default class Paragraph extends Plugin {
 		const data = editor.data;
 
 		editor.commands.add( 'paragraph', new ParagraphCommand( editor ) );
+		editor.commands.add( 'insertParagraph', new InsertParagraphCommand( editor ) );
 
 		// Schema.
 		model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );

+ 101 - 0
packages/ckeditor5-paragraph/tests/insertparagraphcommand.js

@@ -0,0 +1,101 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import InsertParagraphCommand from '../src/insertparagraphcommand';
+
+import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'InsertParagraphCommand', () => {
+	let editor, model, document, command, root, schema;
+
+	beforeEach( () => {
+		return ModelTestEditor.create().then( newEditor => {
+			editor = newEditor;
+			model = editor.model;
+			document = model.document;
+			schema = model.schema;
+			command = new InsertParagraphCommand( editor );
+			root = document.getRoot();
+
+			editor.commands.add( 'insertParagraph', command );
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			schema.register( 'heading1', { inheritAllFrom: '$block', allowIn: 'headersOnly' } );
+			schema.register( 'headersOnly', { inheritAllFrom: '$block' } );
+		} );
+	} );
+
+	afterEach( () => {
+		command.destroy();
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should insert a paragraph at a specific document position and anchor the selection inside of it', () => {
+			setData( model, '<heading1>foo[]</heading1>' );
+
+			command.execute( {
+				position: model.createPositionBefore( root.getChild( 0 ) )
+			} );
+
+			expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph><heading1>foo</heading1>' );
+		} );
+
+		it( 'should do nothing if the paragraph is not allowed at the provided position', () => {
+			setData( model, '<headersOnly><heading1>foo[]</heading1></headersOnly>' );
+
+			command.execute( {
+				position: model.createPositionBefore( root.getChild( 0 ).getChild( 0 ) )
+			} );
+
+			command.execute( {
+				position: model.createPositionAfter( root.getChild( 0 ).getChild( 0 ) )
+			} );
+
+			expect( getData( model ) ).to.equal( '<headersOnly><heading1>foo[]</heading1></headersOnly>' );
+		} );
+
+		describe( 'interation with existing paragraphs in the content', () => {
+			it( 'should insert a paragraph before another paragraph', () => {
+				setData( model, '<paragraph>foo[]</paragraph>' );
+
+				command.execute( {
+					position: model.createPositionBefore( root.getChild( 0 ) )
+				} );
+
+				expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph><paragraph>foo</paragraph>' );
+			} );
+
+			it( 'should insert a paragraph after another paragraph', () => {
+				setData( model, '<paragraph>foo[]</paragraph>' );
+
+				command.execute( {
+					position: model.createPositionAfter( root.getChild( 0 ) )
+				} );
+
+				expect( getData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>[]</paragraph>' );
+			} );
+
+			it( 'should not merge with a paragraph that precedes the position at which a new paragraph is inserted', () => {
+				setData( model, '<paragraph>bar</paragraph><heading1>foo[]</heading1>' );
+
+				command.execute( {
+					position: model.createPositionBefore( root.getChild( 1 ) )
+				} );
+
+				expect( getData( model ) ).to.equal( '<paragraph>bar</paragraph><paragraph>[]</paragraph><heading1>foo</heading1>' );
+			} );
+
+			it( 'should not merge with a paragraph that follows the position at which a new paragraph is inserted', () => {
+				setData( model, '<heading1>foo[]</heading1><paragraph>bar</paragraph>' );
+
+				command.execute( {
+					position: model.createPositionAfter( root.getChild( 0 ) )
+				} );
+
+				expect( getData( model ) ).to.equal( '<heading1>foo</heading1><paragraph>[]</paragraph><paragraph>bar</paragraph>' );
+			} );
+		} );
+	} );
+} );

+ 7 - 2
packages/ckeditor5-paragraph/tests/paragraph.js

@@ -5,6 +5,7 @@
 
 import Paragraph from '../src/paragraph';
 import ParagraphCommand from '../src/paragraphcommand';
+import InsertParagraphCommand from '../src/insertparagraphcommand';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import {
 	getData as getModelData,
@@ -437,9 +438,13 @@ describe( 'Paragraph feature', () => {
 		} );
 	} );
 
-	describe( 'command', () => {
-		it( 'should be set in the editor', () => {
+	describe( 'commands', () => {
+		it( '"paragraph" command should be registered in the editor', () => {
 			expect( editor.commands.get( 'paragraph' ) ).to.be.instanceof( ParagraphCommand );
 		} );
+
+		it( '"insertParagraph" command should be registered in the editor', () => {
+			expect( editor.commands.get( 'insertParagraph' ) ).to.be.instanceof( InsertParagraphCommand );
+		} );
 	} );
 } );

+ 6 - 2
packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widget.css

@@ -129,8 +129,12 @@
 
 	&:not(.ck-widget_selected) {
 		/* Disable visual effects of hover/active widget when CKEditor is in readOnly mode.
-		See: https://github.com/ckeditor/ckeditor5/issues/1261 */
-		--ck-widget-outline-thickness: 0;
+		 * See: https://github.com/ckeditor/ckeditor5/issues/1261
+		 *
+		 * Leave the unit because this custom property is used in calc() by other features.
+		 * See: https://github.com/ckeditor/ckeditor5/issues/6775
+		 */
+		--ck-widget-outline-thickness: 0px;
 	}
 
 	&.ck-widget_with-selection-handle {

+ 37 - 10
packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js

@@ -90,6 +90,9 @@ export default class WidgetTypeAround extends Plugin {
 		this._enableTypeAroundUIInjection();
 		this._enableDetectionOfTypeAroundWidgets();
 		this._enableInsertingParagraphsOnButtonClick();
+
+		// TODO: This is a quick fix and it should be removed the proper integration arrives.
+		this._enableTemporaryTrackChangesIntegration();
 	}
 
 	/**
@@ -105,21 +108,17 @@ export default class WidgetTypeAround extends Plugin {
 	_insertParagraph( widgetViewElement, position ) {
 		const editor = this.editor;
 		const editingView = editor.editing.view;
-		let viewPosition;
+		const widgetModelElement = editor.editing.mapper.toModelElement( widgetViewElement );
+		let modelPosition;
 
 		if ( position === 'before' ) {
-			viewPosition = editingView.createPositionBefore( widgetViewElement );
+			modelPosition = editor.model.createPositionBefore( widgetModelElement );
 		} else {
-			viewPosition = editingView.createPositionAfter( widgetViewElement );
+			modelPosition = editor.model.createPositionAfter( widgetModelElement );
 		}
 
-		const modelPosition = editor.editing.mapper.toModelPosition( viewPosition );
-
-		editor.model.change( writer => {
-			const paragraph = writer.createElement( 'paragraph' );
-
-			writer.insert( paragraph, modelPosition );
-			writer.setSelection( paragraph, 0 );
+		editor.execute( 'insertParagraph', {
+			position: modelPosition
 		} );
 
 		editingView.focus();
@@ -222,6 +221,34 @@ export default class WidgetTypeAround extends Plugin {
 			evt.stop();
 		} );
 	}
+
+	/**
+	 * A quick fix for the integration with features requiring custom handling of the `insertParagraph`
+	 * command such as Track Changes. When the `insertParagraph` command is disabled, this fix adds
+	 * a CSS class to editor roots that makes the UI disappear.
+	 *
+	 * TODO: This is a quick fix and it should be replaced by a proper integration.
+	 *
+	 * @private
+	 */
+	_enableTemporaryTrackChangesIntegration() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+		const insertParagraphCommand = this.editor.commands.get( 'insertParagraph' );
+		const className = 'ck-widget_type-around_temp-disabled';
+
+		this.listenTo( insertParagraphCommand, 'change:isEnabled', ( evt, name, isEnabled ) => {
+			editingView.change( writer => {
+				for ( const root of editingView.document.roots ) {
+					if ( isEnabled ) {
+						writer.removeClass( className, root );
+					} else {
+						writer.addClass( className, root );
+					}
+				}
+			} );
+		} );
+	}
 }
 
 // Injects the type around UI into a view widget instance.

+ 2 - 0
packages/ckeditor5-widget/tests/manual/type-around.html

@@ -21,6 +21,8 @@
 		}
 	</style>
 </head>
+<button id="toggleReadOnly" type="button">Toggle read–only mode</button>
+<hr />
 <div id="editor">
 	<figure class="image"><img src="sample.jpg" alt="bar">
 		<figcaption>Caption</figcaption>

+ 4 - 0
packages/ckeditor5-widget/tests/manual/type-around.js

@@ -90,6 +90,10 @@ class InlineWidget extends Plugin {
 	}
 }
 
+document.querySelector( '#toggleReadOnly' ).addEventListener( 'click', () => {
+	window.editor.isReadOnly = !window.editor.isReadOnly;
+} );
+
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ ArticlePluginSet, HorizontalLine, InlineWidget, MediaEmbed ],

+ 39 - 2
packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js

@@ -54,19 +54,41 @@ describe( 'WidgetTypeAround', () => {
 	} );
 
 	describe( '_insertParagraph()', () => {
-		it( 'should insert a paragraph with a selection before a widget when position is "before"', () => {
+		let executeSpy;
+
+		beforeEach( () => {
+			executeSpy = sinon.spy( editor, 'execute' );
+		} );
+
+		it( 'should execute the "insertParagraph" command when inserting a paragraph before the widget', () => {
 			setModelData( editor.model, '<blockWidget></blockWidget>' );
 
 			plugin._insertParagraph( viewRoot.getChild( 0 ), 'before' );
 
+			const spyExecutePosition = executeSpy.firstCall.args[ 1 ].position;
+			const positionBeforeWidget = editor.model.createPositionBefore( editor.model.document.getRoot().getChild( 0 ) );
+
+			sinon.assert.calledOnce( executeSpy );
+			sinon.assert.calledWith( executeSpy, 'insertParagraph' );
+
+			expect( spyExecutePosition.isEqual( positionBeforeWidget ) ).to.be.true;
+
 			expect( getModelData( editor.model ) ).to.equal( '<paragraph>[]</paragraph><blockWidget></blockWidget>' );
 		} );
 
-		it( 'should insert a paragraph with a selection after a widget when position is "after"', () => {
+		it( 'should execute the "insertParagraph" command when inserting a paragraph after the widget', () => {
 			setModelData( editor.model, '<blockWidget></blockWidget>' );
 
 			plugin._insertParagraph( viewRoot.getChild( 0 ), 'after' );
 
+			const spyExecutePosition = executeSpy.firstCall.args[ 1 ].position;
+			const positionAfterWidget = editor.model.createPositionAfter( editor.model.document.getRoot().getChild( 0 ) );
+
+			sinon.assert.calledOnce( executeSpy );
+			sinon.assert.calledWith( executeSpy, 'insertParagraph' );
+
+			expect( spyExecutePosition.isEqual( positionAfterWidget ) ).to.be.true;
+
 			expect( getModelData( editor.model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]</paragraph>' );
 		} );
 
@@ -290,6 +312,21 @@ describe( 'WidgetTypeAround', () => {
 		} );
 	} );
 
+	describe( '(TEMP) "Integration" with Track Changes using CSS class on roots', () => {
+		it( 'should toggle the CSS class when the InsertParagraphCommand#isEnabled changes', () => {
+			const command = editor.commands.get( 'insertParagraph' );
+
+			expect( command.isEnabled ).to.be.true;
+			expect( viewRoot.hasClass( 'ck-widget_type-around_temp-disabled' ) ).to.be.false;
+
+			command.isEnabled = false;
+			expect( viewRoot.hasClass( 'ck-widget_type-around_temp-disabled' ) ).to.be.true;
+
+			command.isEnabled = true;
+			expect( viewRoot.hasClass( 'ck-widget_type-around_temp-disabled' ) ).to.be.false;
+		} );
+	} );
+
 	function assertIsTypeAroundBefore( viewWidget, expected ) {
 		expect( viewWidget.hasClass( 'ck-widget_can-type-around_before' ) ).to.equal( expected );
 	}

+ 23 - 0
packages/ckeditor5-widget/theme/widgettypearound.css

@@ -65,3 +65,26 @@
 		}
 	}
 }
+
+/*
+ * Integration with the read-only mode of the editor.
+ */
+.ck.ck-editor__editable.ck-read-only .ck-widget__type-around {
+	display: none;
+}
+
+/*
+ * Integration with the restricted editing mode (feature) of the editor.
+ */
+.ck.ck-editor__editable.ck-restricted-editing_mode_restricted .ck-widget__type-around {
+	display: none;
+}
+
+/*
+ * A quick fix for integration with track changes.
+ *
+ * TODO: To be removed when the proper integration arrives.
+ */
+.ck.ck-editor__editable.ck-widget_type-around_temp-disabled .ck-widget__type-around {
+	display: none;
+}