| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- /**
- * @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 block-quote/blockquotecommand
- */
- import Command from '@ckeditor/ckeditor5-core/src/command';
- import first from '@ckeditor/ckeditor5-utils/src/first';
- /**
- * The block quote command plugin.
- *
- * @extends module:core/command~Command
- */
- export default class BlockQuoteCommand extends Command {
- /**
- * Whether the selection starts in a block quote.
- *
- * @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 block quotes within
- * the selection will be removed. If it is off, all selected blocks will be wrapped with
- * a block quote.
- *
- * @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 block quote,
- * otherwise the command will remove the block quote. If not set, the command will act basing on its current value.
- */
- execute( options = {} ) {
- const model = this.editor.model;
- const schema = model.schema;
- 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._removeQuote( writer, blocks.filter( findQuote ) );
- } else {
- const blocksToQuote = blocks.filter( block => {
- // Already quoted blocks needs to be considered while quoting too
- // in order to reuse their <bQ> elements.
- return findQuote( block ) || checkCanBeQuoted( schema, block );
- } );
- this._applyQuote( writer, blocksToQuote );
- }
- } );
- }
- /**
- * 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() );
- // In the current implementation, the block quote must be an immediate parent of a block element.
- return !!( firstBlock && findQuote( firstBlock ) );
- }
- /**
- * 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 checkCanBeQuoted( schema, firstBlock );
- }
- /**
- * Removes the quote from given blocks.
- *
- * If blocks which are supposed to be "unquoted" are in the middle of a quote,
- * start it or end it, then the quote will be split (if needed) and the blocks
- * will be moved out of it, so other quoted blocks remained quoted.
- *
- * @private
- * @param {module:engine/model/writer~Writer} writer
- * @param {Array.<module:engine/model/element~Element>} blocks
- */
- _removeQuote( writer, blocks ) {
- // Unquote all groups of block. Iterate in the reverse order to not break following ranges.
- getRangesOfBlockGroups( writer, blocks ).reverse().forEach( groupRange => {
- if ( groupRange.start.isAtStart && groupRange.end.isAtEnd ) {
- writer.unwrap( groupRange.start.parent );
- return;
- }
- // The group of blocks are at the beginning of an <bQ> so let's move them left (out of the <bQ>).
- if ( groupRange.start.isAtStart ) {
- const positionBefore = writer.createPositionBefore( groupRange.start.parent );
- writer.move( groupRange, positionBefore );
- return;
- }
- // The blocks are in the middle of an <bQ> so we need to split the <bQ> after the last block
- // so we move the items there.
- if ( !groupRange.end.isAtEnd ) {
- writer.split( groupRange.end );
- }
- // Now we are sure that groupRange.end.isAtEnd is true, so let's move the blocks right.
- const positionAfter = writer.createPositionAfter( groupRange.end.parent );
- writer.move( groupRange, positionAfter );
- } );
- }
- /**
- * Applies the quote to given blocks.
- *
- * @private
- * @param {module:engine/model/writer~Writer} writer
- * @param {Array.<module:engine/model/element~Element>} blocks
- */
- _applyQuote( writer, blocks ) {
- const quotesToMerge = [];
- // Quote all groups of block. Iterate in the reverse order to not break following ranges.
- getRangesOfBlockGroups( writer, blocks ).reverse().forEach( groupRange => {
- let quote = findQuote( groupRange.start );
- if ( !quote ) {
- quote = writer.createElement( 'blockQuote' );
- writer.wrap( groupRange, quote );
- }
- quotesToMerge.push( quote );
- } );
- // Merge subsequent <bQ> elements. Reverse the order again because this time we want to go through
- // the <bQ> elements in the source order (due to how merge works – it moves the right element's content
- // to the first element and removes the right one. Since we may need to merge a couple of subsequent `<bQ>` elements
- // we want to keep the reference to the first (furthest left) one.
- quotesToMerge.reverse().reduce( ( currentQuote, nextQuote ) => {
- if ( currentQuote.nextSibling == nextQuote ) {
- writer.merge( writer.createPositionAfter( currentQuote ) );
- return currentQuote;
- }
- return nextQuote;
- } );
- }
- }
- function findQuote( elementOrPosition ) {
- return elementOrPosition.parent.name == 'blockQuote' ? elementOrPosition.parent : null;
- }
- // Returns a minimal array of ranges containing groups of subsequent blocks.
- //
- // content: abcdefgh
- // blocks: [ a, b, d, f, g, h ]
- // output ranges: [ab]c[d]e[fgh]
- //
- // @param {Array.<module:engine/model/element~Element>} blocks
- // @returns {Array.<module:engine/model/range~Range>}
- function getRangesOfBlockGroups( writer, blocks ) {
- let startPosition;
- let i = 0;
- const ranges = [];
- while ( i < blocks.length ) {
- const block = blocks[ i ];
- const nextBlock = blocks[ i + 1 ];
- if ( !startPosition ) {
- startPosition = writer.createPositionBefore( block );
- }
- if ( !nextBlock || block.nextSibling != nextBlock ) {
- ranges.push( writer.createRange( startPosition, writer.createPositionAfter( block ) ) );
- startPosition = null;
- }
- i++;
- }
- return ranges;
- }
- // Checks whether <bQ> can wrap the block.
- function checkCanBeQuoted( schema, block ) {
- // TMP will be replaced with schema.checkWrap().
- const isBQAllowed = schema.checkChild( block.parent, 'blockQuote' );
- const isBlockAllowedInBQ = schema.checkChild( [ '$root', 'blockQuote' ], block );
- return isBQAllowed && isBlockAllowedInBQ;
- }
|