File: //home/arjun/projects/buyercall/node_modules/@ckeditor/ckeditor5-list/src/list/utils.js
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module list/list/utils
*/
import { TreeWalker, getFillerOffset } from 'ckeditor5/src/engine';
import { ButtonView } from 'ckeditor5/src/ui';
/**
* Creates a list item {@link module:engine/view/containerelement~ContainerElement}.
*
* @param {module:engine/view/downcastwriter~DowncastWriter} writer The writer instance.
* @returns {module:engine/view/containerelement~ContainerElement}
*/
export function createViewListItemElement( writer ) {
const viewItem = writer.createContainerElement( 'li' );
viewItem.getFillerOffset = getListItemFillerOffset;
return viewItem;
}
/**
* Helper function that creates a `<ul><li></li></ul>` or (`<ol>`) structure out of the given `modelItem` model `listItem` element.
* Then, it binds the created view list item (`<li>`) with the model `listItem` element.
* The function then returns the created view list item (`<li>`).
*
* @param {module:engine/model/item~Item} modelItem Model list item.
* @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface.
* @returns {module:engine/view/containerelement~ContainerElement} View list element.
*/
export function generateLiInUl( modelItem, conversionApi ) {
const mapper = conversionApi.mapper;
const viewWriter = conversionApi.writer;
const listType = modelItem.getAttribute( 'listType' ) == 'numbered' ? 'ol' : 'ul';
const viewItem = createViewListItemElement( viewWriter );
const viewList = viewWriter.createContainerElement( listType, null );
viewWriter.insert( viewWriter.createPositionAt( viewList, 0 ), viewItem );
mapper.bindElements( modelItem, viewItem );
return viewItem;
}
/**
* Helper function that inserts a view list at a correct place and merges it with its siblings.
* It takes a model list item element (`modelItem`) and a corresponding view list item element (`injectedItem`). The view list item
* should be in a view list element (`<ul>` or `<ol>`) and should be its only child.
* See comments below to better understand the algorithm.
*
* @param {module:engine/view/item~Item} modelItem Model list item.
* @param {module:engine/view/containerelement~ContainerElement} injectedItem
* @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface.
* @param {module:engine/model/model~Model} model The model instance.
*/
export function injectViewList( modelItem, injectedItem, conversionApi, model ) {
const injectedList = injectedItem.parent;
const mapper = conversionApi.mapper;
const viewWriter = conversionApi.writer;
// The position where the view list will be inserted.
let insertPosition = mapper.toViewPosition( model.createPositionBefore( modelItem ) );
// 1. Find the previous list item that has the same or smaller indent. Basically we are looking for the first model item
// that is a "parent" or "sibling" of the injected model item.
// If there is no such list item, it means that the injected list item is the first item in "its list".
const refItem = getSiblingListItem( modelItem.previousSibling, {
sameIndent: true,
smallerIndent: true,
listIndent: modelItem.getAttribute( 'listIndent' )
} );
const prevItem = modelItem.previousSibling;
if ( refItem && refItem.getAttribute( 'listIndent' ) == modelItem.getAttribute( 'listIndent' ) ) {
// There is a list item with the same indent - we found the same-level sibling.
// Break the list after it. The inserted view item will be added in the broken space.
const viewItem = mapper.toViewElement( refItem );
insertPosition = viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );
} else {
// There is no list item with the same indent. Check the previous model item.
if ( prevItem && prevItem.name == 'listItem' ) {
// If it is a list item, it has to have a lower indent.
// It means that the inserted item should be added to it as its nested item.
insertPosition = mapper.toViewPosition( model.createPositionAt( prevItem, 'end' ) );
// There could be some not mapped elements (eg. span in to-do list) but we need to insert
// a nested list directly inside the li element.
const mappedViewAncestor = mapper.findMappedViewAncestor( insertPosition );
const nestedList = findNestedList( mappedViewAncestor );
// If there already is some nested list, then use it's position.
if ( nestedList ) {
insertPosition = viewWriter.createPositionBefore( nestedList );
} else {
// Else just put new list on the end of list item content.
insertPosition = viewWriter.createPositionAt( mappedViewAncestor, 'end' );
}
} else {
// The previous item is not a list item (or does not exist at all).
// Just map the position and insert the view item at the mapped position.
insertPosition = mapper.toViewPosition( model.createPositionBefore( modelItem ) );
}
}
insertPosition = positionAfterUiElements( insertPosition );
// Insert the view item.
viewWriter.insert( insertPosition, injectedList );
// 2. Handle possible children of the injected model item.
if ( prevItem && prevItem.name == 'listItem' ) {
const prevView = mapper.toViewElement( prevItem );
const walkerBoundaries = viewWriter.createRange( viewWriter.createPositionAt( prevView, 0 ), insertPosition );
const walker = walkerBoundaries.getWalker( { ignoreElementEnd: true } );
for ( const value of walker ) {
if ( value.item.is( 'element', 'li' ) ) {
const breakPosition = viewWriter.breakContainer( viewWriter.createPositionBefore( value.item ) );
const viewList = value.item.parent;
const targetPosition = viewWriter.createPositionAt( injectedItem, 'end' );
mergeViewLists( viewWriter, targetPosition.nodeBefore, targetPosition.nodeAfter );
viewWriter.move( viewWriter.createRangeOn( viewList ), targetPosition );
walker.position = breakPosition;
}
}
} else {
const nextViewList = injectedList.nextSibling;
if ( nextViewList && ( nextViewList.is( 'element', 'ul' ) || nextViewList.is( 'element', 'ol' ) ) ) {
let lastSubChild = null;
for ( const child of nextViewList.getChildren() ) {
const modelChild = mapper.toModelElement( child );
if ( modelChild && modelChild.getAttribute( 'listIndent' ) > modelItem.getAttribute( 'listIndent' ) ) {
lastSubChild = child;
} else {
break;
}
}
if ( lastSubChild ) {
viewWriter.breakContainer( viewWriter.createPositionAfter( lastSubChild ) );
viewWriter.move( viewWriter.createRangeOn( lastSubChild.parent ), viewWriter.createPositionAt( injectedItem, 'end' ) );
}
}
}
// Merge the inserted view list with its possible neighbor lists.
mergeViewLists( viewWriter, injectedList, injectedList.nextSibling );
mergeViewLists( viewWriter, injectedList.previousSibling, injectedList );
}
/**
* Helper function that takes two parameters that are expected to be view list elements, and merges them.
* The merge happens only if both parameters are list elements of the same type (the same element name and the same class attributes).
*
* @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter The writer instance.
* @param {module:engine/view/item~Item} firstList The first element to compare.
* @param {module:engine/view/item~Item} secondList The second element to compare.
* @returns {module:engine/view/position~Position|null} The position after merge or `null` when there was no merge.
*/
export function mergeViewLists( viewWriter, firstList, secondList ) {
// Check if two lists are going to be merged.
if ( !firstList || !secondList || ( firstList.name != 'ul' && firstList.name != 'ol' ) ) {
return null;
}
// Both parameters are list elements, so compare types now.
if ( firstList.name != secondList.name || firstList.getAttribute( 'class' ) !== secondList.getAttribute( 'class' ) ) {
return null;
}
return viewWriter.mergeContainers( viewWriter.createPositionAfter( firstList ) );
}
/**
* Helper function that for a given `view.Position`, returns a `view.Position` that is after all `view.UIElement`s that
* are after the given position.
*
* For example:
* `<container:p>foo^<ui:span></ui:span><ui:span></ui:span>bar</container:p>`
* For position ^, the position before "bar" will be returned.
*
* @param {module:engine/view/position~Position} viewPosition
* @returns {module:engine/view/position~Position}
*/
export function positionAfterUiElements( viewPosition ) {
return viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
}
/**
* Helper function that searches for a previous list item sibling of a given model item that meets the given criteria
* passed by the options object.
*
* @param {module:engine/model/item~Item} modelItem
* @param {Object} options Search criteria.
* @param {Boolean} [options.sameIndent=false] Whether the sought sibling should have the same indentation.
* @param {Boolean} [options.smallerIndent=false] Whether the sought sibling should have a smaller indentation.
* @param {Number} [options.listIndent] The reference indentation.
* @param {'forward'|'backward'} [options.direction='backward'] Walking direction.
* @returns {module:engine/model/item~Item|null}
*/
export function getSiblingListItem( modelItem, options ) {
const sameIndent = !!options.sameIndent;
const smallerIndent = !!options.smallerIndent;
const indent = options.listIndent;
let item = modelItem;
while ( item && item.name == 'listItem' ) {
const itemIndent = item.getAttribute( 'listIndent' );
if ( ( sameIndent && indent == itemIndent ) || ( smallerIndent && indent > itemIndent ) ) {
return item;
}
if ( options.direction === 'forward' ) {
item = item.nextSibling;
} else {
item = item.previousSibling;
}
}
return null;
}
/**
* Helper method for creating a UI button and linking it with an appropriate command.
*
* @private
* @param {module:core/editor/editor~Editor} editor The editor instance to which the UI component will be added.
* @param {String} commandName The name of the command.
* @param {String} label The button label.
* @param {String} icon The source of the icon.
*/
export function createUIComponent( editor, commandName, label, icon ) {
editor.ui.componentFactory.add( commandName, locale => {
const command = editor.commands.get( commandName );
const buttonView = new ButtonView( locale );
buttonView.set( {
label,
icon,
tooltip: true,
isToggleable: true
} );
// Bind button model to command.
buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
// Execute command.
buttonView.on( 'execute', () => {
editor.execute( commandName );
editor.editing.view.focus();
} );
return buttonView;
} );
}
/**
* Returns a first list view element that is direct child of the given view element.
*
* @param {module:engine/view/element~Element} viewElement
* @return {module:engine/view/element~Element|null}
*/
export function findNestedList( viewElement ) {
for ( const node of viewElement.getChildren() ) {
if ( node.name == 'ul' || node.name == 'ol' ) {
return node;
}
}
return null;
}
/**
* Returns an array with all `listItem` elements that represent the same list.
*
* It means that values of `listIndent`, `listType`, `listStyle`, `listReversed` and `listStart` for all items are equal.
*
* @param {module:engine/model/position~Position} position Starting position.
* @param {'forward'|'backward'} direction Walking direction.
* @returns {Array.<module:engine/model/element~Element>}
*/
export function getSiblingNodes( position, direction ) {
const items = [];
const listItem = position.parent;
const walkerOptions = {
ignoreElementEnd: true,
startPosition: position,
shallow: true,
direction
};
const limitIndent = listItem.getAttribute( 'listIndent' );
const nodes = [ ...new TreeWalker( walkerOptions ) ]
.filter( value => value.item.is( 'element' ) )
.map( value => value.item );
for ( const element of nodes ) {
// If found something else than `listItem`, we're out of the list scope.
if ( !element.is( 'element', 'listItem' ) ) {
break;
}
// If current parsed item has lower indent that element that the element that was a starting point,
// it means we left a nested list. Abort searching items.
//
// ■ List item 1. [listIndent=0]
// ○ List item 2.[] [listIndent=1], limitIndent = 1,
// ○ List item 3. [listIndent=1]
// ■ List item 4. [listIndent=0]
//
// Abort searching when leave nested list.
if ( element.getAttribute( 'listIndent' ) < limitIndent ) {
break;
}
// ■ List item 1.[] [listIndent=0] limitIndent = 0,
// ○ List item 2. [listIndent=1]
// ○ List item 3. [listIndent=1]
// ■ List item 4. [listIndent=0]
//
// Ignore nested lists.
if ( element.getAttribute( 'listIndent' ) > limitIndent ) {
continue;
}
// ■ List item 1.[] [listType=bulleted]
// 1. List item 2. [listType=numbered]
// 2.List item 3. [listType=numbered]
//
// Abort searching when found a different kind of a list.
if ( element.getAttribute( 'listType' ) !== listItem.getAttribute( 'listType' ) ) {
break;
}
// ■ List item 1.[] [listType=bulleted]
// ■ List item 2. [listType=bulleted]
// ○ List item 3. [listType=bulleted]
// ○ List item 4. [listType=bulleted]
//
// Abort searching when found a different list style,
if ( element.getAttribute( 'listStyle' ) !== listItem.getAttribute( 'listStyle' ) ) {
break;
}
// ... different direction
if ( element.getAttribute( 'listReversed' ) !== listItem.getAttribute( 'listReversed' ) ) {
break;
}
// ... and different start index
if ( element.getAttribute( 'listStart' ) !== listItem.getAttribute( 'listStart' ) ) {
break;
}
if ( direction === 'backward' ) {
items.unshift( element );
} else {
items.push( element );
}
}
return items;
}
/**
* Returns an array with all `listItem` elements in the model selection.
*
* It returns all the items even if only a part of the list is selected, including items that belong to nested lists.
* If no list is selected, it returns an empty array.
* The order of the elements is not specified.
*
* @protected
* @param {module:engine/model/model~Model} model
* @returns {Array.<module:engine/model/element~Element>}
*/
export function getSelectedListItems( model ) {
const document = model.document;
// For all selected blocks find all list items that are being selected
// and update the `listStyle` attribute in those lists.
let listItems = [ ...document.selection.getSelectedBlocks() ]
.filter( element => element.is( 'element', 'listItem' ) )
.map( element => {
const position = model.change( writer => writer.createPositionAt( element, 0 ) );
return [
...getSiblingNodes( position, 'backward' ),
...getSiblingNodes( position, 'forward' )
];
} )
.flat();
// Since `getSelectedBlocks()` can return items that belong to the same list, and
// `getSiblingNodes()` returns the entire list, we need to remove duplicated items.
listItems = [ ...new Set( listItems ) ];
return listItems;
}
const BULLETED_LIST_STYLE_TYPES = [ 'disc', 'circle', 'square' ];
// There's a lot of them (https://www.w3.org/TR/css-counter-styles-3/#typedef-counter-style).
// Let's support only those that can be selected by ListPropertiesUI.
const NUMBERED_LIST_STYLE_TYPES = [
'decimal',
'decimal-leading-zero',
'lower-roman',
'upper-roman',
'lower-latin',
'upper-latin'
];
/**
* Checks whether the given list-style-type is supported by numbered or bulleted list.
*
* @param {String} listStyleType
* @returns {'bulleted'|'numbered'|null}
*/
export function getListTypeFromListStyleType( listStyleType ) {
if ( BULLETED_LIST_STYLE_TYPES.includes( listStyleType ) ) {
return 'bulleted';
}
if ( NUMBERED_LIST_STYLE_TYPES.includes( listStyleType ) ) {
return 'numbered';
}
return null;
}
// Implementation of getFillerOffset for view list item element.
//
// @returns {Number|null} Block filler offset or `null` if block filler is not needed.
function getListItemFillerOffset() {
const hasOnlyLists = !this.isEmpty && ( this.getChild( 0 ).name == 'ul' || this.getChild( 0 ).name == 'ol' );
if ( this.isEmpty || hasOnlyLists ) {
return 0;
}
return getFillerOffset.call( this );
}