ソースを参照

Add more whistles and bells to the reconversion guide demo.

Maciej Gołaszewski 5 年 前
コミット
dd9862405e

+ 10 - 2
packages/ckeditor5-engine/docs/_snippets/framework/build-element-reconversion-source.js

@@ -6,9 +6,17 @@
 /* globals window */
 
 import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
-
-import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import { toWidget, toWidgetEditable, findOptimalInsertionPosition } from '@ckeditor/ckeditor5-widget/src/utils';
+import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 
 window.ClassicEditor = ClassicEditor;
+window.Plugin = Plugin;
+window.ButtonView = ButtonView;
+window.Command = Command;
 window.toWidget = toWidget;
 window.toWidgetEditable = toWidgetEditable;
+window.createElement = createElement;
+window.findOptimalInsertionPosition = findOptimalInsertionPosition;

+ 147 - 24
packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.js

@@ -3,11 +3,16 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals ClassicEditor, toWidget, toWidgetEditable, console, window, prompt, document */
+/* globals
+	ClassicEditor, Plugin, ButtonView, Command, toWidget, toWidgetEditable, createElement, findOptimalInsertionPosition,
+	console, window, prompt, document
+*/
 
 import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
-import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 
+/**
+ * Helper for extracting side card type from a view element based on its CSS class.
+ */
 const getTypeFromViewElement = viewElement => {
 	for ( const type of [ 'info', 'warning' ] ) {
 		if ( viewElement.hasClass( `side-card-${ type }` ) ) {
@@ -18,6 +23,9 @@ const getTypeFromViewElement = viewElement => {
 	return 'info';
 };
 
+/**
+ * Single upcast converter to <sideCard/> element with all its attributes.
+ */
 const upcastCard = ( viewElement, { writer } ) => {
 	const sideCard = writer.createElement( 'sideCard' );
 
@@ -35,7 +43,10 @@ const upcastCard = ( viewElement, { writer } ) => {
 	return sideCard;
 };
 
-function addActionButton( text, callback, domElement, editor ) {
+/**
+ * Helper for creating DOM button with an editor callback.
+ */
+const addActionButton = ( text, callback, domElement, editor ) => {
 	const domDocument = domElement.ownerDocument;
 
 	const button = createElement( domDocument, 'button', {}, [ text ] );
@@ -47,9 +58,15 @@ function addActionButton( text, callback, domElement, editor ) {
 	domElement.appendChild( button );
 
 	return button;
-}
+};
 
-const renderActionsView = ( editor, modelElement ) => function( domElement ) {
+/**
+ * Helper function that creates card editing UI inside the card.
+ */
+const createActionsView = ( editor, modelElement ) => function( domElement ) {
+	//
+	// Set URL action button.
+	//
 	addActionButton( 'Set URL', writer => {
 		// eslint-disable-next-line no-alert
 		const newURL = prompt( 'Set URL', modelElement.getAttribute( 'cardURL' ) || '' );
@@ -60,33 +77,50 @@ const renderActionsView = ( editor, modelElement ) => function( domElement ) {
 	const currentType = modelElement.getAttribute( 'cardType' );
 	const newType = currentType === 'info' ? 'warning' : 'info';
 
+	//
+	// Change card action button.
+	//
 	addActionButton( 'Change type', writer => {
 		writer.setAttribute( 'cardType', newType, modelElement );
 	}, domElement, editor );
 
 	const childCount = modelElement.childCount;
 
+	//
+	// Add content section to a card action button.
+	//
 	const addButton = addActionButton( 'Add section', writer => {
 		writer.insertElement( 'sideCardSection', modelElement, 'end' );
 	}, domElement, editor );
 
+	// Disable the button so only 1-3 content boxes are in the card (there always will be a title).
 	if ( childCount > 4 ) {
 		addButton.setAttribute( 'disabled', 'disabled' );
 	}
 
+	//
+	// Remove content section from a card action button.
+	//
 	const removeButton = addActionButton( 'Remove section', writer => {
 		writer.remove( modelElement.getChild( childCount - 1 ) );
 	}, domElement, editor );
 
+	// Disable the button so only 1-3 content boxes are in the card (there always will be a title).
 	if ( childCount < 3 ) {
 		removeButton.setAttribute( 'disabled', 'disabled' );
 	}
 };
 
-const downcastSideCard = editor => {
+/**
+ * The downcast converter for <sideCard/> element.
+ *
+ * It returns a full view structure based on the current state of the model element.
+ */
+const downcastSideCard = ( editor, { asWidget } ) => {
 	return ( modelElement, { writer, consumable, mapper } ) => {
 		const type = modelElement.getAttribute( 'cardType' ) || 'info';
 
+		// Main view element for the side card.
 		const sideCardView = writer.createContainerElement( 'aside', {
 			class: `side-card side-card-${ type }`
 		} );
@@ -107,7 +141,9 @@ const downcastSideCard = editor => {
 			mapper.bindElements( child, childView );
 
 			// Make it an editable part of the widget.
-			toWidgetEditable( childView, writer );
+			if ( asWidget ) {
+				toWidgetEditable( childView, writer );
+			}
 
 			writer.insert( writer.createPositionAt( sideCardView, 'end' ), childView );
 		}
@@ -125,26 +161,76 @@ const downcastSideCard = editor => {
 			writer.insert( writer.createPositionAt( sideCardView, 'end' ), urlBox );
 		}
 
-		// Inner element used to render simple UI that allows to change side card's attributes.
-		const actionsView = writer.createRawElement( 'div', {
-			class: 'side-card-actions',
-			contenteditable: 'false', 			// Prevent editing of the element:
-			'data-cke-ignore-events': 'true'	// Allows using custom UI elements inside editing view.
-		}, renderActionsView( editor, modelElement ) ); // See the full code for details.
+		// Inner element used to render a simple UI that allows to change side card's attributes.
+		// It will only be needed in the editing view inside a widgetized element.
+		// The data output should not contain this section.
+		if ( asWidget ) {
+			const actionsView = writer.createRawElement( 'div', {
+				class: 'side-card-actions',
+				contenteditable: 'false', 			// Prevent editing of the element:
+				'data-cke-ignore-events': 'true'	// Allows using custom UI elements inside editing view.
+			}, createActionsView( editor, modelElement ) ); // See the full code for details.
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), actionsView );
 
-		writer.insert( writer.createPositionAt( sideCardView, 'end' ), actionsView );
+			toWidget( sideCardView, writer, { widgetLabel: 'Side card' } );
+		}
 
-		return toWidget( sideCardView, writer, { widgetLabel: 'Side card' } );
+		return sideCardView;
 	};
 };
 
-class ComplexBox {
+class InsertCardCommand extends Command {
+	/**
+	 * Refresh uses schema definition to checks if a sideCard can be inserted in the current selection.
+	 */
+	refresh() {
+		const model = this.editor.model;
+		const validParent = findOptimalInsertionPosition( model.document.selection, model );
+
+		this.isEnabled = model.schema.checkChild( validParent, 'sideCard' );
+	}
+
+	/**
+	 * Creates full side card element with all required children and attributes.
+	 */
+	execute() {
+		const model = this.editor.model;
+		const selection = model.document.selection;
+
+		const insertPosition = findOptimalInsertionPosition( selection, model );
+
+		model.change( writer => {
+			const sideCard = writer.createElement( 'sideCard', { cardType: 'info' } );
+			const title = writer.createElement( 'sideCardTitle' );
+			const section = writer.createElement( 'sideCardSection' );
+			const paragraph = writer.createElement( 'paragraph' );
+
+			writer.insert( title, sideCard, 0 );
+			writer.insert( section, sideCard, 1 );
+			writer.insert( paragraph, section, 0 );
+
+			model.insertContent( sideCard, insertPosition );
+
+			writer.setSelection( writer.createPositionAt( title, 0 ) );
+		} );
+	}
+}
+
+class ComplexBox extends Plugin {
 	constructor( editor ) {
-		this._defineSchema( editor );
-		this._defineConversion( editor );
+		super( editor );
+
+		this._defineSchema();
+		this._defineConversion();
+
+		editor.commands.add( 'insertCard', new InsertCardCommand( editor ) );
+
+		this._defineUI();
 	}
 
-	_defineConversion( editor ) {
+	_defineConversion() {
+		const editor = this.editor;
 		const conversion = editor.conversion;
 
 		conversion.for( 'upcast' )
@@ -162,18 +248,23 @@ class ComplexBox {
 			} );
 
 		// The downcast conversion must be split as you need a widget in the editing pipeline.
-		conversion.for( 'downcast' ).elementToElement( {
+		conversion.for( 'editingDowncast' ).elementToElement( {
 			model: 'sideCard',
-			view: downcastSideCard( editor ),
+			view: downcastSideCard( editor, { asWidget: true } ),
 			triggerBy: {
 				attributes: [ 'cardType', 'cardURL' ],
 				children: [ 'sideCardSection' ]
 			}
 		} );
+		// The data downcast is always executed from the current model stat, so the `triggerBy` will take no effect.
+		conversion.for( 'dataDowncast' ).elementToElement( {
+			model: 'sideCard',
+			view: downcastSideCard( editor, { asWidget: false } )
+		} );
 	}
 
-	_defineSchema( editor ) {
-		const schema = editor.model.schema;
+	_defineSchema() {
+		const schema = this.editor.model.schema;
 
 		// The main element with attributes for type and URL:
 		schema.register( 'sideCard', {
@@ -181,6 +272,12 @@ class ComplexBox {
 			isObject: true,
 			allowAttributes: [ 'cardType', 'cardURL' ]
 		} );
+		// Disallow side card nesting.
+		schema.addChildCheck( ( context, childDefinition ) => {
+			if ( [ ...context.getNames() ].includes( 'sideCard' ) && childDefinition.name === 'sideCard' ) {
+				return false;
+			}
+		} );
 
 		// A text-only title.
 		schema.register( 'sideCardTitle', {
@@ -203,6 +300,32 @@ class ComplexBox {
 			allowContentOf: '$root'
 		} );
 	}
+
+	_defineUI() {
+		const editor = this.editor;
+
+		// Defines a simple text button.
+		editor.ui.componentFactory.add( 'insertCard', locale => {
+			const button = new ButtonView( locale );
+
+			const command = editor.commands.get( 'insertCard' );
+
+			button.set( {
+				withText: true,
+				icon: false,
+				label: 'Insert card'
+			} );
+
+			button.bind( 'isEnabled' ).to( command );
+
+			button.on( 'execute', () => {
+				editor.execute( 'insertCard' );
+				editor.editing.view.focus();
+			} );
+
+			return button;
+		} );
+	}
 }
 
 ClassicEditor
@@ -220,7 +343,7 @@ ClassicEditor
 			]
 		},
 		toolbar: {
-			items: [ 'heading', '|', 'bold', 'italic' ],
+			items: [ 'heading', '|', 'bold', 'italic', '|', 'insertCard' ],
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		}
 	} )