src/listediting.js
/**
* @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 list/listediting
*/
import ListCommand from './listcommand';
import IndentCommand from './indentcommand';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import {
cleanList,
cleanListItem,
modelViewInsertion,
modelViewChangeType,
modelViewMergeAfterChangeType,
modelViewMergeAfter,
modelViewRemove,
modelViewSplitOnInsert,
modelViewChangeIndent,
modelChangePostFixer,
modelIndentPasteFixer,
viewModelConverter,
modelToViewPosition,
viewToModelPosition
} from './converters';
/**
* The engine of the list feature. It handles creating, editing and removing lists and list items.
*
* It registers the `'numberedList'`, `'bulletedList'`, `'indentList'` and `'outdentList'` commands.
*
* @extends module:core/plugin~Plugin
*/
export default class ListEditing extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'ListEditing';
}
/**
* @inheritDoc
*/
static get requires() {
return [ Paragraph ];
}
/**
* @inheritDoc
*/
init() {
const editor = this.editor;
// Schema.
// Note: in case `$block` will ever be allowed in `listItem`, keep in mind that this feature
// uses `Selection#getSelectedBlocks()` without any additional processing to obtain all selected list items.
// If there are blocks allowed inside list item, algorithms using `getSelectedBlocks()` will have to be modified.
editor.model.schema.register( 'listItem', {
inheritAllFrom: '$block',
allowAttributes: [ 'listType', 'listIndent' ]
} );
// Converters.
const data = editor.data;
const editing = editor.editing;
editor.model.document.registerPostFixer( writer => modelChangePostFixer( editor.model, writer ) );
editing.mapper.registerViewToModelLength( 'li', getViewListItemLength );
data.mapper.registerViewToModelLength( 'li', getViewListItemLength );
editing.mapper.on( 'modelToViewPosition', modelToViewPosition( editing.view ) );
editing.mapper.on( 'viewToModelPosition', viewToModelPosition( editor.model ) );
data.mapper.on( 'modelToViewPosition', modelToViewPosition( editing.view ) );
editor.conversion.for( 'editingDowncast' )
.add( dispatcher => {
dispatcher.on( 'insert', modelViewSplitOnInsert, { priority: 'high' } );
dispatcher.on( 'insert:listItem', modelViewInsertion( editor.model ) );
dispatcher.on( 'attribute:listType:listItem', modelViewChangeType, { priority: 'high' } );
dispatcher.on( 'attribute:listType:listItem', modelViewMergeAfterChangeType, { priority: 'low' } );
dispatcher.on( 'attribute:listIndent:listItem', modelViewChangeIndent( editor.model ) );
dispatcher.on( 'remove:listItem', modelViewRemove( editor.model ) );
dispatcher.on( 'remove', modelViewMergeAfter, { priority: 'low' } );
} );
editor.conversion.for( 'dataDowncast' )
.add( dispatcher => {
dispatcher.on( 'insert', modelViewSplitOnInsert, { priority: 'high' } );
dispatcher.on( 'insert:listItem', modelViewInsertion( editor.model ) );
} );
editor.conversion.for( 'upcast' )
.add( dispatcher => {
dispatcher.on( 'element:ul', cleanList, { priority: 'high' } );
dispatcher.on( 'element:ol', cleanList, { priority: 'high' } );
dispatcher.on( 'element:li', cleanListItem, { priority: 'high' } );
dispatcher.on( 'element:li', viewModelConverter );
} );
// Fix indentation of pasted items.
editor.model.on( 'insertContent', modelIndentPasteFixer, { priority: 'high' } );
// Register commands for numbered and bulleted list.
editor.commands.add( 'numberedList', new ListCommand( editor, 'numbered' ) );
editor.commands.add( 'bulletedList', new ListCommand( editor, 'bulleted' ) );
// Register commands for indenting.
editor.commands.add( 'indentList', new IndentCommand( editor, 'forward' ) );
editor.commands.add( 'outdentList', new IndentCommand( editor, 'backward' ) );
const viewDocument = editing.view.document;
// Overwrite default Enter key behavior.
// If Enter key is pressed with selection collapsed in empty list item, outdent it instead of breaking it.
this.listenTo( viewDocument, 'enter', ( evt, data ) => {
const doc = this.editor.model.document;
const positionParent = doc.selection.getLastPosition().parent;
if ( doc.selection.isCollapsed && positionParent.name == 'listItem' && positionParent.isEmpty ) {
this.editor.execute( 'outdentList' );
data.preventDefault();
evt.stop();
}
} );
// Overwrite default Backspace key behavior.
// If Backspace key is pressed with selection collapsed on first position in first list item, outdent it. #83
this.listenTo( viewDocument, 'delete', ( evt, data ) => {
// Check conditions from those that require less computations like those immediately available.
if ( data.direction !== 'backward' ) {
return;
}
const selection = this.editor.model.document.selection;
if ( !selection.isCollapsed ) {
return;
}
const firstPosition = selection.getFirstPosition();
if ( !firstPosition.isAtStart ) {
return;
}
const positionParent = firstPosition.parent;
if ( positionParent.name !== 'listItem' ) {
return;
}
const previousIsAListItem = positionParent.previousSibling && positionParent.previousSibling.name === 'listItem';
if ( previousIsAListItem ) {
return;
}
this.editor.execute( 'outdentList' );
data.preventDefault();
evt.stop();
}, { priority: 'high' } );
const getCommandExecuter = commandName => {
return ( data, cancel ) => {
const command = this.editor.commands.get( commandName );
if ( command.isEnabled ) {
this.editor.execute( commandName );
cancel();
}
};
};
editor.keystrokes.set( 'Tab', getCommandExecuter( 'indentList' ) );
editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( 'outdentList' ) );
}
/**
* @inheritDoc
*/
afterInit() {
const commands = this.editor.commands;
const indent = commands.get( 'indent' );
const outdent = commands.get( 'outdent' );
if ( indent ) {
indent.registerChildCommand( commands.get( 'indentList' ) );
}
if ( outdent ) {
outdent.registerChildCommand( commands.get( 'outdentList' ) );
}
}
}
function getViewListItemLength( element ) {
let length = 1;
for ( const child of element.getChildren() ) {
if ( child.name == 'ul' || child.name == 'ol' ) {
for ( const item of child.getChildren() ) {
length += getViewListItemLength( item );
}
}
}
return length;
}