/**
* @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 code-block/outdentcodeblockcommand
*/
import Command from '@ckeditor/ckeditor5-core/src/command';
import {
getLeadingWhiteSpaces,
getIndentOutdentPositions,
isModelSelectionInCodeBlock
} from './utils';
/**
* The code block indentation decrease command plugin.
*
* @extends module:core/command~Command
*/
export default class OutdentCodeBlockCommand extends Command {
constructor( editor ) {
super( editor );
/**
* A sequence of characters removed from the line when the command is executed.
*
* @readonly
* @private
* @member {String}
*/
this._indentSequence = editor.config.get( 'codeBlock.indentSequence' );
}
/**
* @inheritDoc
*/
refresh() {
this.isEnabled = this._checkEnabled();
}
/**
* Executes the command. When the command {@link #isEnabled is enabled}, the indentation of the
* code lines in the selection will be decreased.
*
* @fires execute
*/
execute() {
const editor = this.editor;
const model = editor.model;
model.change( writer => {
const positions = getIndentOutdentPositions( model );
// Outdent all positions, for instance assuming the indent sequence is 4x space (" "):
//
// ^foo -> foo
//
// ^bar -> bar
//
// Also, when there is more than one position:
//
//
// ^foobar
//
// ^bazqux
//
//
// ->
//
//
// foobar
//
// bazqux
//
for ( const position of positions ) {
const range = getLastOutdentableSequenceRange( this.editor.model, position, this._indentSequence );
if ( range ) {
writer.remove( range );
}
}
} );
}
/**
* Checks whether the command can be enabled in the current context.
*
* @private
* @returns {Boolean} Whether the command should be enabled.
*/
_checkEnabled() {
if ( !this._indentSequence ) {
return false;
}
const model = this.editor.model;
if ( !isModelSelectionInCodeBlock( model.document.selection ) ) {
return false;
}
// Outdent command can execute only when there is an indent character sequence
// in some of the lines.
return getIndentOutdentPositions( model ).some( position => {
return getLastOutdentableSequenceRange( model, position, this._indentSequence );
} );
}
}
// For a position coming from `getIndentOutdentPositions()`, it returns the range representing
// the last occurrence of the indent sequence among the leading whitespaces of the code line the
// position represents.
//
// For instance, assuming the indent sequence is 4x space (" "):
//
// foo^ -> null
// foo^bar -> null
// ^foo -> null
// ^foo -> [ ]foo
// ^foo bar -> [ ]foo bar
//
// @param {} model
// @param {} position
// @param {String} sequence
// @returns {|null}
function getLastOutdentableSequenceRange( model, position, sequence ) {
// Positions start before each text node (code line). Get the node corresponding to the position.
const nodeAtPosition = getCodeLineTextNodeAtPosition( position );
if ( !nodeAtPosition ) {
return null;
}
const leadingWhiteSpaces = getLeadingWhiteSpaces( nodeAtPosition );
const lastIndexOfSequence = leadingWhiteSpaces.lastIndexOf( sequence );
// For instance, assuming the indent sequence is 4x space (" "):
//
// ^foo -> null
//
if ( lastIndexOfSequence + sequence.length !== leadingWhiteSpaces.length ) {
return null;
}
// For instance, assuming the indent sequence is 4x space (" "):
//
// ^foo -> null
//
if ( lastIndexOfSequence === -1 ) {
return null;
}
const { parent, startOffset } = nodeAtPosition;
// Create a range that contains the **last** indent sequence among the leading whitespaces
// of the line.
//
// For instance, assuming the indent sequence is 4x space (" "):
//
// ^foo -> [ ]foo
//
return model.createRange(
model.createPositionAt( parent, startOffset + lastIndexOfSequence ),
model.createPositionAt( parent, startOffset + lastIndexOfSequence + sequence.length )
);
}
function getCodeLineTextNodeAtPosition( position ) {
// Positions start before each text node (code line). Get the node corresponding to the position.
let nodeAtPosition = position.parent.getChild( position.index );
// foo^
// foo^bar
if ( !nodeAtPosition || nodeAtPosition.is( 'element', 'softBreak' ) ) {
nodeAtPosition = position.nodeBefore;
}
// ^
// foo^bar
if ( !nodeAtPosition || nodeAtPosition.is( 'element', 'softBreak' ) ) {
return null;
}
return nodeAtPosition;
}