@ckeditor/ckeditor5-list
Advanced tools
Comparing version 12.0.4 to 12.1.0
Changelog | ||
========= | ||
## [12.1.0](https://github.com/ckeditor/ckeditor5-list/compare/v12.0.4...v12.1.0) (2019-08-26) | ||
### Features | ||
* Introduced to-do lists. Closes [ckeditor/ckeditor5#1434](https://github.com/ckeditor/ckeditor5/issues/1434). ([56a7a7a](https://github.com/ckeditor/ckeditor5-list/commit/56a7a7a)) | ||
### Bug fixes | ||
* The UI buttons should be marked as toggleable for better assistive technologies support (see [ckeditor/ckeditor5#1403](https://github.com/ckeditor/ckeditor5/issues/1403)). ([bb12325](https://github.com/ckeditor/ckeditor5-list/commit/bb12325)) | ||
### Other changes | ||
* The issue tracker for this package was moved to https://github.com/ckeditor/ckeditor5/issues. See [ckeditor/ckeditor5#1988](https://github.com/ckeditor/ckeditor5/issues/1988). ([5507ac6](https://github.com/ckeditor/ckeditor5-list/commit/5507ac6)) | ||
* Updated translations. ([10e296d](https://github.com/ckeditor/ckeditor5-list/commit/10e296d)) | ||
## [12.0.4](https://github.com/ckeditor/ckeditor5-list/compare/v12.0.3...v12.0.4) (2019-07-10) | ||
@@ -16,3 +32,3 @@ | ||
The `@ckeditor/ckeditor5-indent` feature introduces the "indent" and "outdent" buttons which can be used to manipulate lists and other blocks. | ||
* Updated translations. ([6c4b520](https://github.com/ckeditor/ckeditor5-list/commit/6c4b520)) | ||
* Updated translations. ([6c4b520](https://github.com/ckeditor/ckeditor5-list/commit/6c4b520)) | ||
@@ -24,3 +40,3 @@ | ||
* Updated translations. ([b7f3abc](https://github.com/ckeditor/ckeditor5-list/commit/b7f3abc)) | ||
* Updated translations. ([b7f3abc](https://github.com/ckeditor/ckeditor5-list/commit/b7f3abc)) | ||
@@ -32,3 +48,3 @@ | ||
* Updated translations. ([d595449](https://github.com/ckeditor/ckeditor5-list/commit/d595449)) | ||
* Updated translations. ([d595449](https://github.com/ckeditor/ckeditor5-list/commit/d595449)) | ||
@@ -35,0 +51,0 @@ |
{ | ||
"Numbered List": "Toolbar button tooltip for the Numbered List feature.", | ||
"Bulleted List": "Toolbar button tooltip for the Bulleted List feature." | ||
} | ||
"Bulleted List": "Toolbar button tooltip for the Bulleted List feature.", | ||
"To-do List": "Toolbar button tooltip for the To-do List feature." | ||
} |
{ | ||
"name": "@ckeditor/ckeditor5-list", | ||
"version": "12.0.4", | ||
"version": "12.1.0", | ||
"description": "Ordered and unordered lists feature to CKEditor 5.", | ||
@@ -13,19 +13,21 @@ "keywords": [ | ||
"dependencies": { | ||
"@ckeditor/ckeditor5-core": "^12.2.1", | ||
"@ckeditor/ckeditor5-engine": "^13.2.1", | ||
"@ckeditor/ckeditor5-paragraph": "^11.0.4", | ||
"@ckeditor/ckeditor5-ui": "^13.0.2", | ||
"@ckeditor/ckeditor5-utils": "^13.0.1" | ||
"@ckeditor/ckeditor5-core": "^12.3.0", | ||
"@ckeditor/ckeditor5-engine": "^14.0.0", | ||
"@ckeditor/ckeditor5-paragraph": "^11.0.5", | ||
"@ckeditor/ckeditor5-ui": "^14.0.0", | ||
"@ckeditor/ckeditor5-utils": "^14.0.0" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-basic-styles": "^11.1.3", | ||
"@ckeditor/ckeditor5-block-quote": "^11.1.2", | ||
"@ckeditor/ckeditor5-clipboard": "^12.0.1", | ||
"@ckeditor/ckeditor5-editor-classic": "^12.1.3", | ||
"@ckeditor/ckeditor5-enter": "^11.0.4", | ||
"@ckeditor/ckeditor5-heading": "^11.0.4", | ||
"@ckeditor/ckeditor5-indent": "^10.0.1", | ||
"@ckeditor/ckeditor5-link": "^11.1.1", | ||
"@ckeditor/ckeditor5-typing": "^12.1.1", | ||
"@ckeditor/ckeditor5-undo": "^11.0.4", | ||
"@ckeditor/ckeditor5-basic-styles": "^11.1.4", | ||
"@ckeditor/ckeditor5-block-quote": "^11.1.3", | ||
"@ckeditor/ckeditor5-clipboard": "^12.0.2", | ||
"@ckeditor/ckeditor5-editor-classic": "^12.1.4", | ||
"@ckeditor/ckeditor5-enter": "^11.1.0", | ||
"@ckeditor/ckeditor5-heading": "^11.0.5", | ||
"@ckeditor/ckeditor5-highlight": "^11.0.5", | ||
"@ckeditor/ckeditor5-indent": "^10.1.0", | ||
"@ckeditor/ckeditor5-link": "^11.1.2", | ||
"@ckeditor/ckeditor5-table": "^14.0.0", | ||
"@ckeditor/ckeditor5-typing": "^12.2.0", | ||
"@ckeditor/ckeditor5-undo": "^11.0.5", | ||
"eslint": "^5.5.0", | ||
@@ -43,3 +45,3 @@ "eslint-config-ckeditor5": "^2.0.0", | ||
"homepage": "https://ckeditor.com/ckeditor-5", | ||
"bugs": "https://github.com/ckeditor/ckeditor5-list/issues", | ||
"bugs": "https://github.com/ckeditor/ckeditor5/issues", | ||
"repository": { | ||
@@ -46,0 +48,0 @@ "type": "git", |
@@ -12,3 +12,3 @@ CKEditor 5 list feature | ||
This package implements bulleted and numbered list feature for CKEditor 5. | ||
This package implements bulleted, numbered and to-do list feature for CKEditor 5. | ||
@@ -15,0 +15,0 @@ ## Documentation |
@@ -10,7 +10,13 @@ /** | ||
import { createViewListItemElement } from './utils'; | ||
import { | ||
generateLiInUl, | ||
injectViewList, | ||
mergeViewLists, | ||
getSiblingListItem, | ||
positionAfterUiElements | ||
} from './utils'; | ||
import TreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker'; | ||
/** | ||
* A model-to-view converter for `listItem` model element insertion. | ||
* A model-to-view converter for the `listItem` model element insertion. | ||
* | ||
@@ -47,3 +53,3 @@ * It creates a `<ul><li></li><ul>` (or `<ol>`) view structure out of a `listItem` model element, inserts it at the correct | ||
/** | ||
* A model-to-view converter for `listItem` model element removal. | ||
* A model-to-view converter for the `listItem` model element removal. | ||
* | ||
@@ -91,7 +97,11 @@ * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove | ||
/** | ||
* A model-to-view converter for `type` attribute change on `listItem` model element. | ||
* A model-to-view converter for the `type` attribute change on the `listItem` model element. | ||
* | ||
* This change means that `<li>` elements parent changes from `<ul>` to `<ol>` (or vice versa). This is accomplished | ||
* by breaking view elements, changing their name and merging them. | ||
* This change means that the `<li>` element parent changes from `<ul>` to `<ol>` (or vice versa). This is accomplished | ||
* by breaking view elements and changing their name. The next {@link module:list/converters~modelViewMergeAfterChangeType} | ||
* converter will attempt to merge split nodes. | ||
* | ||
* Splitting this conversion into 2 steps makes it possible to add an additional conversion in the middle. | ||
* Check {@link module:list/todolistconverters~modelViewChangeType} to see an example of it. | ||
* | ||
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute | ||
@@ -110,3 +120,3 @@ * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event. | ||
// 1. Break the container after and before the list item. | ||
// Break the container after and before the list item. | ||
// This will create a view list with one view list item -- the one that changed type. | ||
@@ -116,13 +126,28 @@ viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) ); | ||
// 2. Change name of the view list that holds the changed view item. | ||
// Change name of the view list that holds the changed view item. | ||
// We cannot just change name property, because that would not render properly. | ||
let viewList = viewItem.parent; | ||
const viewList = viewItem.parent; | ||
const listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul'; | ||
viewList = viewWriter.rename( listName, viewList ); | ||
// 3. Merge the changed view list with other lists, if possible. | ||
viewWriter.rename( listName, viewList ); | ||
} | ||
/** | ||
* A model-to-view converter that attempts to merge nodes split by {@link module:list/converters~modelViewChangeType}. | ||
* | ||
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute | ||
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event. | ||
* @param {Object} data Additional information about the change. | ||
* @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface. | ||
*/ | ||
export function modelViewMergeAfterChangeType( evt, data, conversionApi ) { | ||
const viewItem = conversionApi.mapper.toViewElement( data.item ); | ||
const viewList = viewItem.parent; | ||
const viewWriter = conversionApi.writer; | ||
// Merge the changed view list with other lists, if possible. | ||
mergeViewLists( viewWriter, viewList, viewList.nextSibling ); | ||
mergeViewLists( viewWriter, viewList.previousSibling, viewList ); | ||
// 4. Consumable insertion of children inside the item. They are already handled by re-building the item in view. | ||
// Consumable insertion of children inside the item. They are already handled by re-building the item in view. | ||
for ( const child of data.item.getChildren() ) { | ||
@@ -134,3 +159,3 @@ conversionApi.consumable.consume( child, 'insert' ); | ||
/** | ||
* A model-to-view converter for `listIndent` attribute change on `listItem` model element. | ||
* A model-to-view converter for the `listIndent` attribute change on the `listItem` model element. | ||
* | ||
@@ -335,3 +360,3 @@ * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute | ||
/** | ||
* A view-to-model converter that converts `<li>` view elements into `listItem` model elements. | ||
* A view-to-model converter that converts the `<li>` view elements into the `listItem` model elements. | ||
* | ||
@@ -396,4 +421,4 @@ * To set correct values of the `listType` and `listIndent` attributes the converter: | ||
/** | ||
* A view-to-model converter for `<ul>` and `<ol>` view elements that cleans the input view of garbage. | ||
* This is mostly to clean whitespaces from between `<li>` view elements inside the view list element, however, also | ||
* A view-to-model converter for the `<ul>` and `<ol>` view elements that cleans the input view of garbage. | ||
* This is mostly to clean whitespaces from between the `<li>` view elements inside the view list element, however, also | ||
* incorrect data can be cleared if the view was incorrect. | ||
@@ -420,3 +445,3 @@ * | ||
/** | ||
* A view-to-model converter for `<li>` elements that cleans whitespace formatting from the input view. | ||
* A view-to-model converter for the `<li>` elements that cleans whitespace formatting from the input view. | ||
* | ||
@@ -465,5 +490,5 @@ * @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element | ||
/** | ||
* Returns callback for model position to view position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes | ||
* positions between `listItem` elements that would be incorrectly mapped because of how list items are represented in model | ||
* and view. | ||
* Returns a callback for model position to view position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes | ||
* positions between the `listItem` elements that would be incorrectly mapped because of how list items are represented in the model | ||
* and in the view. | ||
* | ||
@@ -504,4 +529,4 @@ * @see module:engine/conversion/mapper~Mapper#event:modelToViewPosition | ||
* The callback for view position to model position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes | ||
* positions between `<li>` elements that would be incorrectly mapped because of how list items are represented in model | ||
* and view. | ||
* positions between the `<li>` elements that would be incorrectly mapped because of how list items are represented in the model | ||
* and in the view. | ||
* | ||
@@ -569,4 +594,4 @@ * @see module:engine/conversion/mapper~Mapper#event:viewToModelPosition | ||
* | ||
* In an example below, there is a correct list structure. | ||
* Then the middle element will be removed so the list structure will become incorrect: | ||
* In the example below, there is a correct list structure. | ||
* Then the middle element is removed so the list structure will become incorrect: | ||
* | ||
@@ -577,3 +602,3 @@ * <listItem listType="bulleted" listIndent=0>Item 1</listItem> | ||
* | ||
* List structure after the middle element removed: | ||
* The list structure after the middle element is removed: | ||
* | ||
@@ -800,19 +825,2 @@ * <listItem listType="bulleted" listIndent=0>Item 1</listItem> | ||
// Helper function that creates a `<ul><li></li></ul>` or (`<ol>`) structure out of given `modelItem` model `listItem` element. | ||
// Then, it binds created view list item (<li>) with model `listItem` element. | ||
// The function then returns created view list item (<li>). | ||
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 converts children of a given `<li>` view element into corresponding model elements. | ||
@@ -905,130 +913,2 @@ // The function maintains proper order of elements if model `listItem` is split during the conversion | ||
// Helper function that seeks for a previous list item sibling of given model item which meets given criteria. | ||
// `options` object may contain one or more of given values (by default they are `false`): | ||
// `options.sameIndent` - whether sought sibling should have same indent (default = no), | ||
// `options.smallerIndent` - whether sought sibling should have smaller indent (default = no). | ||
// `options.listIndent` - the reference indent. | ||
// Either `options.sameIndent` or `options.smallerIndent` should be set to `true`. | ||
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; | ||
} | ||
item = item.previousSibling; | ||
} | ||
return null; | ||
} | ||
// Helper function that takes two parameters, that are expected to be view list elements, and merges them. | ||
// The merge happen only if both parameters are UL or OL elements. | ||
function mergeViewLists( viewWriter, firstList, secondList ) { | ||
if ( firstList && secondList && ( firstList.name == 'ul' || firstList.name == 'ol' ) && firstList.name == secondList.name ) { | ||
return viewWriter.mergeContainers( viewWriter.createPositionAfter( firstList ) ); | ||
} | ||
return null; | ||
} | ||
// Helper function that takes model list item element `modelItem`, corresponding view list item element `injectedItem` | ||
// that is not added to the view and is inside a view list element (`ul` or `ol`) and is that's list only child. | ||
// The list is inserted at correct position (element breaking may be needed) and then merged with it's siblings. | ||
// See comments below to better understand the algorithm. | ||
function injectViewList( modelItem, injectedItem, conversionApi, model ) { | ||
const injectedList = injectedItem.parent; | ||
const mapper = conversionApi.mapper; | ||
const viewWriter = conversionApi.writer; | ||
// Position where view list will be inserted. | ||
let insertPosition = mapper.toViewPosition( model.createPositionBefore( modelItem ) ); | ||
// 1. Find previous list item that has same or smaller indent. Basically we are looking for a first model item | ||
// that is "parent" or "sibling" of injected model item. | ||
// If there is no such list item, it means that 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 same indent - we found same-level sibling. | ||
// Break the list after it. Inserted view item will be inserted in the broken space. | ||
const viewItem = mapper.toViewElement( refItem ); | ||
insertPosition = viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) ); | ||
} else { | ||
// There is no list item with same indent. Check previous model item. | ||
if ( prevItem && prevItem.name == 'listItem' ) { | ||
// If it is a list item, it has to have lower indent. | ||
// It means that inserted item should be added to it as its nested item. | ||
insertPosition = mapper.toViewPosition( model.createPositionAt( prevItem, 'end' ) ); | ||
} else { | ||
// Previous item is not a list item (or does not exist at all). | ||
// Just map the position and insert the view item at mapped position. | ||
insertPosition = mapper.toViewPosition( model.createPositionBefore( modelItem ) ); | ||
} | ||
} | ||
insertPosition = positionAfterUiElements( insertPosition ); | ||
// Insert the view item. | ||
viewWriter.insert( insertPosition, injectedList ); | ||
// 2. Handle possible children of 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( '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( 'ul' ) || nextViewList.is( '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 inserted view list with its possible neighbour lists. | ||
mergeViewLists( viewWriter, injectedList, injectedList.nextSibling ); | ||
mergeViewLists( viewWriter, injectedList.previousSibling, injectedList ); | ||
} | ||
// Helper function that takes all children of given `viewRemovedItem` and moves them in a correct place, according | ||
@@ -1130,10 +1010,1 @@ // to other given parameters. | ||
} | ||
// Helper function that for given `view.Position`, returns a `view.Position` that is after all `view.UIElement`s that | ||
// are after given position. | ||
// For example: | ||
// <container:p>foo^<ui:span></ui:span><ui:span></ui:span>bar</contain:p> | ||
// For position ^, a position before "bar" will be returned. | ||
function positionAfterUiElements( viewPosition ) { | ||
return viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) ); | ||
} |
@@ -47,3 +47,3 @@ /** | ||
/** | ||
* Indents or outdents (depends on the {@link #constructor}'s `indentDirection` parameter) selected list items. | ||
* Indents or outdents (depending on the {@link #constructor}'s `indentDirection` parameter) selected list items. | ||
* | ||
@@ -50,0 +50,0 @@ * @fires execute |
@@ -18,3 +18,3 @@ /** | ||
* | ||
* This is a "glue" plugin which loads the {@link module:list/listediting~ListEditing list editing feature} | ||
* This is a "glue" plugin that loads the {@link module:list/listediting~ListEditing list editing feature} | ||
* and {@link module:list/listui~ListUI list UI feature}. | ||
@@ -21,0 +21,0 @@ * |
@@ -32,5 +32,5 @@ /** | ||
* @readonly | ||
* @member {'numbered'|'bulleted'} | ||
* @member {'numbered'|'bulleted'|'todo'} | ||
*/ | ||
this.type = type == 'bulleted' ? 'bulleted' : 'numbered'; | ||
this.type = type; | ||
@@ -37,0 +37,0 @@ /** |
@@ -21,2 +21,3 @@ /** | ||
modelViewChangeType, | ||
modelViewMergeAfterChangeType, | ||
modelViewMergeAfter, | ||
@@ -81,11 +82,8 @@ modelViewRemove, | ||
editing.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType ); | ||
data.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType ); | ||
editing.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType, { priority: 'high' } ); | ||
editing.downcastDispatcher.on( 'attribute:listType:listItem', modelViewMergeAfterChangeType, { priority: 'low' } ); | ||
editing.downcastDispatcher.on( 'attribute:listIndent:listItem', modelViewChangeIndent( editor.model ) ); | ||
data.downcastDispatcher.on( 'attribute:listIndent:listItem', modelViewChangeIndent( editor.model ) ); | ||
editing.downcastDispatcher.on( 'remove:listItem', modelViewRemove( editor.model ) ); | ||
editing.downcastDispatcher.on( 'remove', modelViewMergeAfter, { priority: 'low' } ); | ||
data.downcastDispatcher.on( 'remove:listItem', modelViewRemove( editor.model ) ); | ||
data.downcastDispatcher.on( 'remove', modelViewMergeAfter, { priority: 'low' } ); | ||
@@ -108,3 +106,3 @@ data.upcastDispatcher.on( 'element:ul', cleanList, { priority: 'high' } ); | ||
const viewDocument = this.editor.editing.view.document; | ||
const viewDocument = editing.view.document; | ||
@@ -178,2 +176,5 @@ // Overwrite default Enter key behavior. | ||
/** | ||
* @inheritDoc | ||
*/ | ||
afterInit() { | ||
@@ -180,0 +181,0 @@ const commands = this.editor.commands; |
@@ -10,2 +10,4 @@ /** | ||
import { createUIComponent } from './utils'; | ||
import numberedListIcon from '../theme/icons/numberedlist.svg'; | ||
@@ -15,3 +17,2 @@ import bulletedListIcon from '../theme/icons/bulletedlist.svg'; | ||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; | ||
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; | ||
@@ -29,39 +30,8 @@ /** | ||
init() { | ||
// Create two buttons and link them with numberedList and bulletedList commands. | ||
const t = this.editor.t; | ||
this._addButton( 'numberedList', t( 'Numbered List' ), numberedListIcon ); | ||
this._addButton( 'bulletedList', t( 'Bulleted List' ), bulletedListIcon ); | ||
} | ||
/** | ||
* Helper method for initializing a button and linking it with an appropriate command. | ||
* | ||
* @private | ||
* @param {String} commandName The name of the command. | ||
* @param {Object} label The button label. | ||
* @param {String} icon The source of the icon. | ||
*/ | ||
_addButton( commandName, label, icon ) { | ||
const editor = this.editor; | ||
editor.ui.componentFactory.add( commandName, locale => { | ||
const command = editor.commands.get( commandName ); | ||
const buttonView = new ButtonView( locale ); | ||
buttonView.set( { | ||
label, | ||
icon, | ||
tooltip: true | ||
} ); | ||
// Bind button model to command. | ||
buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' ); | ||
// Execute command. | ||
this.listenTo( buttonView, 'execute', () => editor.execute( commandName ) ); | ||
return buttonView; | ||
} ); | ||
// Create two buttons and link them with numberedList and bulletedList commands. | ||
createUIComponent( this.editor, 'numberedList', t( 'Numbered List' ), numberedListIcon ); | ||
createUIComponent( this.editor, 'bulletedList', t( 'Bulleted List' ), bulletedListIcon ); | ||
} | ||
} |
237
src/utils.js
@@ -11,5 +11,6 @@ /** | ||
import { getFillerOffset } from '@ckeditor/ckeditor5-engine/src/view/containerelement'; | ||
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; | ||
/** | ||
* Creates list item {@link module:engine/view/containerelement~ContainerElement}. | ||
* Creates a list item {@link module:engine/view/containerelement~ContainerElement}. | ||
* | ||
@@ -21,2 +22,3 @@ * @param {module:engine/view/downcastwriter~DowncastWriter} writer The writer instance. | ||
const viewItem = writer.createContainerElement( 'li' ); | ||
viewItem.getFillerOffset = getListItemFillerOffset; | ||
@@ -27,2 +29,235 @@ | ||
/** | ||
* 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' ) ); | ||
} 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( '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( 'ul' ) || nextViewList.is( '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. | ||
* @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; | ||
} | ||
item = item.previousSibling; | ||
} | ||
return null; | ||
} | ||
export function findInRange( range, comparator ) { | ||
for ( const item of range.getItems() ) { | ||
const result = comparator( item ); | ||
if ( result ) { | ||
return result; | ||
} | ||
} | ||
} | ||
/** | ||
* 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 {Object} 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 ) ); | ||
return buttonView; | ||
} ); | ||
} | ||
// Implementation of getFillerOffset for view list item element. | ||
@@ -29,0 +264,0 @@ // |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
174158
70
2444
16
- Removed@ckeditor/ckeditor5-engine@13.2.1(transitive)
- Removed@ckeditor/ckeditor5-ui@13.0.2(transitive)
- Removed@ckeditor/ckeditor5-utils@13.0.1(transitive)