Browse Source

Initial code block implementation.

Oskar Wróbel 6 years ago
parent
commit
7b78ff8189

+ 38 - 0
packages/ckeditor5-code-block/src/codeblock.js

@@ -0,0 +1,38 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module code-block/codeblock
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import CodeBlockEditing from './codeblockediting';
+import CodeBlockUI from './codeblockqui';
+
+/**
+ * The code block plugin.
+ *
+ * For more information about this feature check the {@glink api/code-block package page}.
+ *
+ * This is a "glue" plugin which loads the {@link module:code-block/codeblockediting~CodeBlockEditing code block editing feature}
+ * and {@link module:code-block/codeblockui~CodeBlockUI code block UI feature}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class CodeBlock extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ CodeBlockEditing, CodeBlockUI ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'CodeBlock';
+	}
+}

+ 160 - 0
packages/ckeditor5-code-block/src/codeblockcommand.js

@@ -0,0 +1,160 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module code-block/codeblockcommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+import first from '@ckeditor/ckeditor5-utils/src/first';
+
+/**
+ * The code block command plugin.
+ *
+ * @extends module:core/command~Command
+ */
+export default class CodeBlockCommand extends Command {
+	/**
+	 * Whether the selection starts in a code block.
+	 *
+	 * @observable
+	 * @readonly
+	 * @member {Boolean} #value
+	 */
+
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		this.value = this._getValue();
+		this.isEnabled = this._checkEnabled();
+	}
+
+	/**
+	 * Executes the command. When the command {@link #value is on}, all top-most code blocks within
+	 * the selection will be removed. If it is off, all selected blocks will be flattened and
+	 * wrapped by a code block.
+	 *
+	 * @fires execute
+	 * @param {Object} [options] Command options.
+	 * @param {Boolean} [options.forceValue] If set, it will force the command behavior. If `true`, the command will apply a code block,
+	 * otherwise the command will remove the code block. If not set, the command will act basing on its current value.
+	 */
+	execute( options = {} ) {
+		const model = this.editor.model;
+		const selection = model.document.selection;
+
+		const blocks = Array.from( selection.getSelectedBlocks() );
+		const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue;
+
+		model.change( writer => {
+			if ( value ) {
+				this._applyCodeBlock( writer, blocks );
+			} else {
+				this._removeCodeBlock( writer, blocks );
+			}
+		} );
+	}
+
+	/**
+	 * Checks the command's {@link #value}.
+	 *
+	 * @private
+	 * @returns {Boolean} The current value.
+	 */
+	_getValue() {
+		const selection = this.editor.model.document.selection;
+		const firstBlock = first( selection.getSelectedBlocks() );
+
+		return !!( firstBlock && firstBlock.is( 'codeBlock' ) );
+	}
+
+	/**
+	 * Checks whether the command can be enabled in the current context.
+	 *
+	 * @private
+	 * @returns {Boolean} Whether the command should be enabled.
+	 */
+	_checkEnabled() {
+		if ( this.value ) {
+			return true;
+		}
+
+		const selection = this.editor.model.document.selection;
+		const schema = this.editor.model.schema;
+
+		const firstBlock = first( selection.getSelectedBlocks() );
+
+		if ( !firstBlock ) {
+			return false;
+		}
+
+		return canBeCodeBlock( schema, firstBlock );
+	}
+
+	/**
+	 * @private
+	 * @param {module:engine/model/writer~Writer} writer
+	 * @param {Array.<module:engine/model/element~Element>} blocks
+	 */
+	_applyCodeBlock( writer, blocks ) {
+		const schema = this.editor.model.schema;
+		const allowedBlock = blocks.filter( block => canBeCodeBlock( schema, block ) );
+
+		for ( const block of allowedBlock ) {
+			writer.rename( block, 'codeBlock' );
+		}
+
+		allowedBlock.reverse().forEach( ( currentBlock, i ) => {
+			const nextBlock = allowedBlock[ i + 1 ];
+
+			if ( currentBlock.previousSibling === nextBlock ) {
+				writer.appendElement( 'softBreak', nextBlock );
+				writer.merge( writer.createPositionBefore( currentBlock ) );
+			}
+		} );
+	}
+
+	/**
+	 * @private
+	 * @param {module:engine/model/writer~Writer} writer
+	 * @param {Array.<module:engine/model/element~Element>} blocks
+	 */
+	_removeCodeBlock( writer, blocks ) {
+		const codeBlocks = blocks.filter( block => block.is( 'codeBlock' ) );
+
+		for ( const block of codeBlocks ) {
+			const range = writer.createRangeOn( block );
+
+			for ( const item of Array.from( range.getItems() ).reverse() ) {
+				if ( item.is( 'softBreak' ) && item.parent.is( 'codeBlock' ) ) {
+					const { position } = writer.split( writer.createPositionBefore( item ) );
+
+					writer.rename( position.nodeAfter, 'paragraph' );
+					writer.remove( item );
+				}
+			}
+
+			writer.rename( block, 'paragraph' );
+		}
+	}
+}
+
+function canBeCodeBlock( schema, element ) {
+	if ( element.is( 'rootElement' ) || schema.isLimit( element ) ) {
+		return false;
+	}
+
+	if ( !schema.checkChild( element.parent, 'codeBlock' ) ) {
+		return false;
+	}
+
+	if ( !schema.isBlock( element ) && !schema.checkChild( element, '$text' ) ) {
+		return false;
+	}
+
+	return true;
+}

+ 71 - 0
packages/ckeditor5-code-block/src/codeblockediting.js

@@ -0,0 +1,71 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module code-block/codeblockediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import CodeBlockCommand from './codeblockcommand';
+
+/**
+ * The editing part of the code block feature.
+ *
+ * Introduces the `'codeBlock'` command and the `'codeBlock'` model element.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class CodeBlockEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const schema = editor.model.schema;
+
+		// Command.
+		editor.commands.add( 'codeBlock', new CodeBlockCommand( editor ) );
+
+		// Schema.
+		schema.register( 'codeBlock', { inheritAllFrom: '$block' } );
+
+		// Disallow codeBlock in codeBlock.
+		schema.addChildCheck( ( context, childDef ) => {
+			if ( context.endsWith( 'codeBlock' ) && childDef.name == 'codeBlock' ) {
+				return false;
+			}
+		} );
+
+		// Disallow all attributes in `codeBlock`.
+		schema.addAttributeCheck( context => {
+			if ( context.endsWith( 'codeBlock' ) || context.endsWith( 'codeBlock $text' ) ) {
+				return false;
+			}
+		} );
+
+		// Conversion.
+		editor.conversion.elementToElement( { model: 'codeBlock', view: 'pre' } );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	afterInit() {
+		const editor = this.editor;
+		const viewDoc = editor.editing.view.document;
+
+		this.listenTo( viewDoc, 'enter', ( evt, data ) => {
+			const doc = editor.model.document;
+			const positionParent = doc.selection.getLastPosition().parent;
+
+			if ( positionParent.is( 'codeBlock' ) ) {
+				editor.execute( 'shiftEnter' );
+				data.preventDefault();
+				evt.stop();
+			}
+		} );
+	}
+}

+ 51 - 0
packages/ckeditor5-code-block/src/codeblockqui.js

@@ -0,0 +1,51 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module code-block/codeblockui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import codeBlockIcon from '@ckeditor/ckeditor5-core/theme/icons/quote.svg';
+import '../theme/codeblock.css';
+
+/**
+ * The code block UI plugin.
+ *
+ * It introduces the `'codeBlock'` button.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class CodeBlockUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
+
+		editor.ui.componentFactory.add( 'codeBlock', locale => {
+			const command = editor.commands.get( 'codeBlock' );
+			const buttonView = new ButtonView( locale );
+
+			buttonView.set( {
+				label: t( 'Code block' ),
+				icon: codeBlockIcon,
+				tooltip: true,
+				isToggleable: true
+			} );
+
+			// Bind button model to command.
+			buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
+
+			// Execute command.
+			this.listenTo( buttonView, 'execute', () => editor.execute( 'codeBlock' ) );
+
+			return buttonView;
+		} );
+	}
+}

+ 7 - 0
packages/ckeditor5-code-block/theme/codeblock.css

@@ -0,0 +1,7 @@
+.ck-content pre {
+	padding: 8px 13px;
+	color: #353535;
+	background: #f1f1f1;
+	border: solid 1px var(--ck-color-base-border);
+	border-radius: var(--ck-border-radius);
+}