| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module list/indentcommand
- */
- import Command from '@ckeditor/ckeditor5-core/src/command';
- import first from '@ckeditor/ckeditor5-utils/src/first';
- /**
- * The list indent command. It is used by the {@link module:list/list~List list feature}.
- *
- * @extends module:core/command~Command
- */
- export default class IndentCommand extends Command {
- /**
- * Creates an instance of the command.
- *
- * @param {module:core/editor/editor~Editor} editor The editor instance.
- * @param {'forward'|'backward'} indentDirection The direction of indent. If it is equal to `backward`, the command
- * will outdent a list item.
- */
- constructor( editor, indentDirection ) {
- super( editor );
- /**
- * Determines by how much the command will change the list item's indent attribute.
- *
- * @readonly
- * @private
- * @member {Number}
- */
- this._indentBy = indentDirection == 'forward' ? 1 : -1;
- }
- /**
- * @inheritDoc
- */
- refresh() {
- this.isEnabled = this._checkEnabled();
- }
- /**
- * Indents or outdents (depends on the {@link #constructor}'s `indentDirection` parameter) selected list items.
- *
- * @fires execute
- */
- execute() {
- const model = this.editor.model;
- const doc = model.document;
- let itemsToChange = Array.from( doc.selection.getSelectedBlocks() );
- model.change( writer => {
- const lastItem = itemsToChange[ itemsToChange.length - 1 ];
- // Indenting a list item should also indent all the items that are already sub-items of indented item.
- let next = lastItem.nextSibling;
- // Check all items after last indented item, as long as their indent is bigger than indent of that item.
- while ( next && next.name == 'listItem' && next.getAttribute( 'indent' ) > lastItem.getAttribute( 'indent' ) ) {
- itemsToChange.push( next );
- next = next.nextSibling;
- }
- // We need to be sure to keep model in correct state after each small change, because converters
- // bases on that state and assumes that model is correct.
- // Because of that, if the command outdents items, we will outdent them starting from the last item, as
- // it is safer.
- if ( this._indentBy < 0 ) {
- itemsToChange = itemsToChange.reverse();
- }
- for ( const item of itemsToChange ) {
- const indent = item.getAttribute( 'indent' ) + this._indentBy;
- // If indent is lower than 0, it means that the item got outdented when it was not indented.
- // This means that we need to convert that list item to paragraph.
- if ( indent < 0 ) {
- // To keep the model as correct as possible, first rename listItem, then remove attributes,
- // as listItem without attributes is very incorrect and will cause problems in converters.
- // No need to remove attributes, will be removed by post fixer.
- writer.rename( item, 'paragraph' );
- }
- // If indent is >= 0, change the attribute value.
- else {
- writer.setAttribute( 'indent', indent, item );
- }
- }
- // Check whether some of changed list items' type should not be fixed.
- // But first, reverse `itemsToChange` again -- we always want to perform those fixes starting from first item (source-wise).
- if ( this._indentBy < 0 ) {
- itemsToChange = itemsToChange.reverse();
- }
- for ( const item of itemsToChange ) {
- _fixType( item, writer );
- }
- } );
- }
- /**
- * Checks whether the command can be enabled in the current context.
- *
- * @private
- * @returns {Boolean} Whether the command should be enabled.
- */
- _checkEnabled() {
- // Check whether any of position's ancestor is a list item.
- const listItem = first( this.editor.model.document.selection.getSelectedBlocks() );
- // If selection is not in a list item, the command is disabled.
- if ( !listItem || !listItem.is( 'listItem' ) ) {
- return false;
- }
- if ( this._indentBy > 0 ) {
- // Cannot indent first item in it's list. Check if before `listItem` is a list item that is in same list.
- // To be in the same list, the item has to have same attributes and cannot be "split" by an item with lower indent.
- const indent = listItem.getAttribute( 'indent' );
- const type = listItem.getAttribute( 'type' );
- let prev = listItem.previousSibling;
- while ( prev && prev.is( 'listItem' ) && prev.getAttribute( 'indent' ) >= indent ) {
- if ( prev.getAttribute( 'indent' ) == indent ) {
- // The item is on the same level.
- // If it has same type, it means that we found a preceding sibling from the same list.
- // If it does not have same type, it means that `listItem` is on different list (this can happen only
- // on top level lists, though).
- return prev.getAttribute( 'type' ) == type;
- }
- prev = prev.previousSibling;
- }
- // Could not find similar list item, this means that `listItem` is first in its list.
- return false;
- }
- // If we are outdenting it is enough to be in list item. Every list item can always be outdented.
- return true;
- }
- }
- // Fixes type of `item` element after it was indented/outdented. Looks for a sibling of `item` that has the same
- // indent and sets `item`'s type to the same as that sibling.
- function _fixType( item, writer ) {
- // Find a preceding sibling of `item` that is a list item of the same list as `item`.
- const prev = _seekListItem( item, false );
- // If found, fix type.
- if ( prev ) {
- writer.setAttribute( 'type', prev.getAttribute( 'type' ), item );
- return;
- }
- // If not found, find a following sibling of `item` that is a list item of the same list as `item`.
- const next = _seekListItem( item, true );
- // If found, fix type.
- if ( next ) {
- writer.setAttribute( 'type', next.getAttribute( 'type' ), item );
- }
- }
- // Seeks for a list item that has same indent as given `item`. May look through next siblings (`seekForward = true`) or
- // previous siblings (`seekForward = false`). Returns found list item or `null` if item has not been found.
- function _seekListItem( item, seekForward ) {
- let result = item[ seekForward ? 'nextSibling' : 'previousSibling' ];
- // Look for the previous/next sibling that has same indent and is before a list item element with lower indent.
- // If elements are split by an element with lower indent, they are on different lists.
- while ( result && result.is( 'listItem' ) && result.getAttribute( 'indent' ) >= item.getAttribute( 'indent' ) ) {
- if ( result.getAttribute( 'indent' ) == item.getAttribute( 'indent' ) ) {
- // We found sibling that is on the same list.
- return result;
- }
- result = result[ seekForward ? 'nextSibling' : 'previousSibling' ];
- }
- return null;
- }
|