/** * @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 indent/indentblock */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import IndentBlockCommand from './indentblockcommand'; import IndentUsingOffset from './indentcommandbehavior/indentusingoffset'; import IndentUsingClasses from './indentcommandbehavior/indentusingclasses'; /** * The block indentation feature. * * It registers the `'indentBlock'` and `'outdentBlock'` commands. * * If the plugin {@link module:indent/indent~Indent} is defined, it also attaches the `'indentBlock'` and `'outdentBlock'` commands to * the `'indent'` and `'outdent'` commands. * * @extends module:core/plugin~Plugin */ export default class IndentBlock extends Plugin { /** * @inheritDoc */ constructor( editor ) { super( editor ); editor.config.define( 'indentBlock', { offset: 40, unit: 'px' } ); } /** * @inheritDoc */ static get pluginName() { return 'IndentBlock'; } /** * @inheritDoc */ init() { const editor = this.editor; const schema = editor.model.schema; const conversion = editor.conversion; const configuration = editor.config.get( 'indentBlock' ); // Enable block indentation by default in paragraph and default headings. const knownElements = [ 'paragraph', 'heading1', 'heading2', 'heading3', 'heading4', 'heading5', 'heading6' ]; knownElements.forEach( elementName => { if ( schema.isRegistered( elementName ) ) { schema.extend( elementName, { allowAttributes: 'blockIndent' } ); } } ); const useOffsetConfig = !configuration.classes || !configuration.classes.length; const indentConfig = Object.assign( { direction: 'forward' }, configuration ); const outdentConfig = Object.assign( { direction: 'backward' }, configuration ); if ( useOffsetConfig ) { this._setupConversionUsingOffset( conversion ); editor.commands.add( 'indentBlock', new IndentBlockCommand( editor, new IndentUsingOffset( indentConfig ) ) ); editor.commands.add( 'outdentBlock', new IndentBlockCommand( editor, new IndentUsingOffset( outdentConfig ) ) ); } else { this._setupConversionUsingClasses( configuration.classes ); editor.commands.add( 'indentBlock', new IndentBlockCommand( editor, new IndentUsingClasses( indentConfig ) ) ); editor.commands.add( 'outdentBlock', new IndentBlockCommand( editor, new IndentUsingClasses( outdentConfig ) ) ); } } /** * @inheritDoc */ afterInit() { const editor = this.editor; const indentCommand = editor.commands.get( 'indent' ); const outdentCommand = editor.commands.get( 'outdent' ); indentCommand.registerChildCommand( editor.commands.get( 'indentBlock' ) ); outdentCommand.registerChildCommand( editor.commands.get( 'outdentBlock' ) ); } /** * Setups conversion for using offset indents. * * @private */ _setupConversionUsingOffset() { const conversion = this.editor.conversion; const locale = this.editor.locale; const marginProperty = locale.contentLanguageDirection === 'rtl' ? 'margin-right' : 'margin-left'; conversion.for( 'upcast' ).attributeToAttribute( { view: { styles: { [ marginProperty ]: /[\s\S]+/ } }, model: { key: 'blockIndent', value: viewElement => viewElement.getStyle( marginProperty ) } } ); // The margin shorthand should also work. conversion.for( 'upcast' ).attributeToAttribute( { view: { styles: { 'margin': /[\s\S]+/ } }, model: { key: 'blockIndent', value: viewElement => normalizeToMarginSideStyle( viewElement.getStyle( 'margin' ), marginProperty ) } } ); conversion.for( 'downcast' ).attributeToAttribute( { model: 'blockIndent', view: modelAttributeValue => { return { key: 'style', value: { [ marginProperty ]: modelAttributeValue } }; } } ); } /** * Setups conversion for using classes. * * @param {Array.} classes * @private */ _setupConversionUsingClasses( classes ) { const definition = { model: { key: 'blockIndent', values: [] }, view: {} }; for ( const className of classes ) { definition.model.values.push( className ); definition.view[ className ] = { key: 'class', value: [ className ] }; } this.editor.conversion.attributeToAttribute( definition ); } } // Normalizes the margin shorthand value to the value of margin-left or margin-right CSS property. // // As such it will return: // // - '1em' -> '1em' // - '2px 1em' -> '1em' // - '2px 1em 3px' -> '1em' // - '2px 10px 3px 1em' // -> '1em' (side "margin-left") // -> '10px' (side "margin-right") // // @param {String} marginStyleValue Margin style value. // @param {String} side "margin-left" or "margin-right" depending on which margin should be returned. // @returns {String} Extracted value of margin-left or margin-right. function normalizeToMarginSideStyle( marginStyleValue, side ) { // Splits the margin shorthand, ie margin: 2em 4em. const marginEntries = marginStyleValue.split( ' ' ); let marginValue; // If only one value defined, ie: `margin: 1px`. marginValue = marginEntries[ 0 ]; // If only two values defined, ie: `margin: 1px 2px`. if ( marginEntries[ 1 ] ) { marginValue = marginEntries[ 1 ]; } // If four values defined, ie: `margin: 1px 2px 3px 4px`. if ( marginEntries[ 3 ] ) { if ( side === 'margin-left' ) { marginValue = marginEntries[ 3 ]; } else { marginValue = marginEntries[ 1 ]; } } return marginValue; } /** * The configuration of the {@link module:indent/indentblock~IndentBlock block indentation feature}. * * Read more in {@link module:indent/indentblock~IndentBlockConfig}. * * @member {module:indent/indentblock~IndentBlockConfig} module:core/editor/editorconfig~EditorConfig#indentBlock */ /** * The configuration of the block indentation feature. * * If no {@link module:indent/indentblock~IndentBlockConfig#classes} are set, the block indentation feature will use * {@link module:indent/indentblock~IndentBlockConfig#offset} and {@link module:indent/indentblock~IndentBlockConfig#unit} to * create indentation steps. * * ClassicEditor * .create( editorElement, { * indentBlock: { * offset: 2, * unit: 'em' * } * } ) * .then( ... ) * .catch( ... ); * * Alternatively, the block indentation feature may set one of defined {@link module:indent/indentblock~IndentBlockConfig#classes} as * indentation steps: * * ClassicEditor * .create( editorElement, { * indentBlock: { * classes: [ * 'indent-a', // The first step - smallest indentation. * 'indent-b', * 'indent-c', * 'indent-d', * 'indent-e' // The last step - biggest indentation. * ] * } * } ) * .then( ... ) * .catch( ... ); * * In the example above only 5 indentation steps will be available. * * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. * * @interface IndentBlockConfig */ /** * The size of indentation {@link module:indent/indentblock~IndentBlockConfig#unit units} for each indentation step. * * @default 40 * @member {Number} module:indent/indentblock~IndentBlockConfig#offset */ /** * The unit used for indentation {@link module:indent/indentblock~IndentBlockConfig#offset}. * * @default 'px' * @member {String} module:indent/indentblock~IndentBlockConfig#unit */ /** * An optional list of classes to use for indenting the editor content. If not set or set to an empty array, no classes will be used. * The {@link module:indent/indentblock~IndentBlockConfig#unit `indentBlock.unit`} and * {@link module:indent/indentblock~IndentBlockConfig#offset `indentBlock.offset`} properties will be used instead. * * @default undefined * @member {Array.|undefined} module:indent/indentblock~IndentBlockConfig#classes */