Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@ckeditor/ckeditor5-list

Package Overview
Dependencies
Maintainers
1
Versions
709
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ckeditor/ckeditor5-list - npm Package Compare versions

Comparing version 1.0.0-alpha.2 to 1.0.0-beta.1

lang/translations/es.po

13

CHANGELOG.md
Changelog
=========
## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-list/compare/v1.0.0-alpha.2...v1.0.0-beta.1) (2018-03-15)
### Features
* Updated icons for compatibility with the refreshed Lark theme. Minor adjustments in toolbar configurations (see [ckeditor/ckeditor5#645](https://github.com/ckeditor/ckeditor5/issues/645)). ([d1fae4b](https://github.com/ckeditor/ckeditor5-list/commit/d1fae4b))
### Other changes
* Aligned feature class naming to the new scheme. ([d677fb6](https://github.com/ckeditor/ckeditor5-list/commit/d677fb6))
* Removed `ViewListItemElement` class and introduced `createViewListItemElement()` utility method. Closes [#89](https://github.com/ckeditor/ckeditor5-list/issues/89). ([e4ac704](https://github.com/ckeditor/ckeditor5-list/commit/e4ac704))
* Updated translations. ([762a9ed](https://github.com/ckeditor/ckeditor5-list/commit/762a9ed))
## [1.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-list/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2017-11-14)

@@ -5,0 +18,0 @@

2

LICENSE.md

@@ -5,3 +5,3 @@ Software License Agreement

**CKEditor5 List Feature** – https://github.com/ckeditor/ckeditor5-list <br>
Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
Copyright (c) 2003-2018, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.

@@ -8,0 +8,0 @@ Licensed under the terms of any of the following licenses at your choice:

{
"name": "@ckeditor/ckeditor5-list",
"version": "1.0.0-alpha.2",
"version": "1.0.0-beta.1",
"description": "Ordered and unordered lists feature to CKEditor 5.",

@@ -10,22 +10,22 @@ "keywords": [

"dependencies": {
"@ckeditor/ckeditor5-engine": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-paragraph": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-core": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-ui": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-utils": "^1.0.0-alpha.2"
"@ckeditor/ckeditor5-engine": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-paragraph": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-core": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-ui": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-utils": "^1.0.0-beta.1"
},
"devDependencies": {
"@ckeditor/ckeditor5-basic-styles": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-block-quote": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-clipboard": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-editor-classic": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-enter": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-heading": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-link": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-typing": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-undo": "^1.0.0-alpha.2",
"eslint": "^4.8.0",
"eslint-config-ckeditor5": "^1.0.6",
"@ckeditor/ckeditor5-basic-styles": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-block-quote": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-clipboard": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-editor-classic": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-enter": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-heading": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-link": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-typing": "^1.0.0-beta.1",
"@ckeditor/ckeditor5-undo": "^1.0.0-beta.1",
"eslint": "^4.15.0",
"eslint-config-ckeditor5": "^1.0.7",
"husky": "^0.14.3",
"lint-staged": "^4.2.3"
"lint-staged": "^6.0.0"
},

@@ -32,0 +32,0 @@ "engines": {

@@ -7,3 +7,5 @@ CKEditor 5 list feature

[![Build Status](https://travis-ci.org/ckeditor/ckeditor5-list.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-list)
[![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-list/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-list/coverage)
[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://www.browserstack.com/automate/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-list/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-list?branch=master)
<br>
[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-list/status.svg)](https://david-dm.org/ckeditor/ckeditor5-list)

@@ -10,0 +12,0 @@ [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-list/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-list?type=dev)

/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.

@@ -10,14 +10,10 @@ */

import ViewListItemElement from './viewlistitemelement';
import ModelDocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
import modelWriter from '@ckeditor/ckeditor5-engine/src/model/writer';
import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
import ViewTreeWalker from '@ckeditor/ckeditor5-engine/src/view/treewalker';
import viewWriter from '@ckeditor/ckeditor5-engine/src/view/writer';
import { createViewListItemElement } from './utils';

@@ -30,12 +26,13 @@ /**

*
* @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert
* @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/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface.
*/
export function modelViewInsertion( evt, data, consumable, conversionApi ) {
export function modelViewInsertion( evt, data, conversionApi ) {
const consumable = conversionApi.consumable;
if ( !consumable.test( data.item, 'insert' ) ||
!consumable.test( data.item, 'addAttribute:type' ) ||
!consumable.test( data.item, 'addAttribute:indent' )
!consumable.test( data.item, 'attribute:type' ) ||
!consumable.test( data.item, 'attribute:indent' )
) {

@@ -46,68 +43,72 @@ return;

consumable.consume( data.item, 'insert' );
consumable.consume( data.item, 'addAttribute:type' );
consumable.consume( data.item, 'addAttribute:indent' );
consumable.consume( data.item, 'attribute:type' );
consumable.consume( data.item, 'attribute:indent' );
const modelItem = data.item;
const viewItem = generateLiInUl( modelItem, conversionApi.mapper );
const viewItem = generateLiInUl( modelItem, conversionApi );
// Providing kind of "default" insert position in case of converting incorrect model.
const insertPosition = conversionApi.mapper.toViewPosition( ModelPosition.createBefore( modelItem ) );
injectViewList( modelItem, viewItem, conversionApi.mapper, insertPosition );
injectViewList( modelItem, viewItem, conversionApi );
}
/**
* A model-to-view converter for `type` attribute change on `listItem` model element.
* A model-to-view converter for `listItem` model element removal.
*
* 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.
*
* @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove
* @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/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface.
*/
export function modelViewChangeType( evt, data, consumable, conversionApi ) {
if ( !consumable.consume( data.item, 'changeAttribute:type' ) ) {
return;
}
export function modelViewRemove( evt, data, conversionApi ) {
const viewStart = conversionApi.mapper.toViewPosition( data.position ).getLastMatchingPosition( value => !value.item.is( 'li' ) );
const viewItem = viewStart.nodeAfter;
const viewWriter = conversionApi.writer;
const viewItem = conversionApi.mapper.toViewElement( data.item );
// 1. 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.
// This will create a view list with one view list item - the one to remove.
viewWriter.breakContainer( ViewPosition.createBefore( viewItem ) );
viewWriter.breakContainer( ViewPosition.createAfter( viewItem ) );
// 2. 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 listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul';
viewList = viewWriter.rename( viewList, listName );
// 2. Remove the list with the item to remove.
const viewList = viewItem.parent;
const viewListPrev = viewList.previousSibling;
const removeRange = ViewRange.createOn( viewList );
const removed = viewWriter.remove( removeRange );
// 3. Merge the changed view list with other lists, if possible.
mergeViewLists( viewList, viewList.nextSibling );
mergeViewLists( viewList.previousSibling, viewList );
// 3. Merge the whole created by breaking and removing the list.
if ( viewListPrev && viewListPrev.nextSibling ) {
mergeViewLists( viewWriter, viewListPrev, viewListPrev.nextSibling );
}
// 4. Bring back nested list that was in the removed <li>.
const modelItem = conversionApi.mapper.toModelElement( viewItem );
hoistNestedLists( modelItem.getAttribute( 'indent' ) + 1, data.position, removeRange.start, viewItem, conversionApi );
// 5. Unbind removed view item and all children.
for ( const child of ViewRange.createIn( removed ).getItems() ) {
conversionApi.mapper.unbindViewElement( child );
}
evt.stop();
}
/**
* A model-to-view converter for `listItem` model element removal.
* A model-to-view converter for `type` attribute change on `listItem` model element.
*
* @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:remove
* 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.
*
* @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/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface.
*/
export function modelViewRemove( evt, data, consumable, conversionApi ) {
if ( !consumable.consume( data.item, 'remove' ) ) {
export function modelViewChangeType( evt, data, conversionApi ) {
if ( !conversionApi.consumable.consume( data.item, 'attribute:type' ) ) {
return;
}
let viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
viewPosition = viewPosition.getLastMatchingPosition( value => !value.item.is( 'li' ) );
const viewItem = conversionApi.mapper.toViewElement( data.item );
const viewWriter = conversionApi.writer;
const viewItem = viewPosition.nodeAfter;
// 1. Break the container after and before the list item.

@@ -118,19 +119,15 @@ // This will create a view list with one view list item -- the one that changed type.

// 2. Remove the UL that contains just the removed <li>.
const viewList = viewItem.parent;
const viewListPrev = viewList.previousSibling;
const removeRange = ViewRange.createOn( viewList );
viewWriter.remove( removeRange );
// 2. 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 listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul';
viewList = viewWriter.rename( viewList, listName );
if ( viewListPrev && viewListPrev.nextSibling ) {
mergeViewLists( viewListPrev, viewListPrev.nextSibling );
}
// 3. Merge the changed view list with other lists, if possible.
mergeViewLists( viewWriter, viewList, viewList.nextSibling );
mergeViewLists( viewWriter, viewList.previousSibling, viewList );
// 3. Bring back nested list that was in the removed <li>.
hoistNestedLists( data.item.getAttribute( 'indent' ) + 1, data.sourcePosition, removeRange.start, viewItem, conversionApi.mapper );
// Unbind this element only if it was moved to graveyard.
// See #847.
if ( data.item.root.rootName == '$graveyard' ) {
conversionApi.mapper.unbindModelElement( data.item );
// 4. 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() ) {
conversionApi.consumable.consume( child, 'insert' );
}

@@ -142,10 +139,9 @@ }

*
* @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute
* @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/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface.
*/
export function modelViewChangeIndent( evt, data, consumable, conversionApi ) {
if ( !consumable.consume( data.item, 'changeAttribute:indent' ) ) {
export function modelViewChangeIndent( evt, data, conversionApi ) {
if ( !conversionApi.consumable.consume( data.item, 'attribute:indent' ) ) {
return;

@@ -155,2 +151,3 @@ }

const viewItem = conversionApi.mapper.toViewElement( data.item );
const viewWriter = conversionApi.writer;

@@ -168,18 +165,16 @@ // 1. Break the container after and before the list item.

// TODO: get rid of `removePosition` when conversion is done on `changesDone`.
let removePosition;
if ( viewListPrev && viewListPrev.nextSibling ) {
removePosition = mergeViewLists( viewListPrev, viewListPrev.nextSibling );
mergeViewLists( viewWriter, viewListPrev, viewListPrev.nextSibling );
}
if ( !removePosition ) {
removePosition = removeRange.start;
}
// 3. Bring back nested list that was in the removed <li>.
hoistNestedLists( data.attributeOldValue + 1, data.range.start, removeRange.start, viewItem, conversionApi.mapper );
hoistNestedLists( data.attributeOldValue + 1, data.range.start, removeRange.start, viewItem, conversionApi );
// 4. Inject view list like it is newly inserted.
injectViewList( data.item, viewItem, conversionApi.mapper, removePosition );
injectViewList( data.item, viewItem, conversionApi );
// 5. Consume insertion of children inside the item. They are already handled by re-building the item in view.
for ( const child of data.item.getChildren() ) {
conversionApi.consumable.consume( child, 'insert' );
}
}

@@ -206,12 +201,12 @@

*
* @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert
* @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/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface.
*/
export function modelViewSplitOnInsert( evt, data, consumable, conversionApi ) {
export function modelViewSplitOnInsert( evt, data, conversionApi ) {
if ( data.item.name != 'listItem' ) {
let viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
const viewWriter = conversionApi.writer;
const lists = [];

@@ -295,3 +290,3 @@

if ( i > 0 ) {
const mergePos = mergeViewLists( previousList, previousList.nextSibling );
const mergePos = mergeViewLists( viewWriter, previousList, previousList.nextSibling );

@@ -307,3 +302,3 @@ // If `mergePos` is in `previousList` it means that the lists got merged.

// Merge last inserted list with element after it.
mergeViewLists( viewPosition.nodeBefore, viewPosition.nodeAfter );
mergeViewLists( viewWriter, viewPosition.nodeBefore, viewPosition.nodeAfter );
}

@@ -330,19 +325,16 @@ }

*
* @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:remove
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove
* @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/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface.
*/
export function modelViewMergeAfter( evt, data, consumable, conversionApi ) {
if ( !data.item.is( 'listItem' ) ) {
const viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
const viewItemPrev = viewPosition.nodeBefore;
const viewItemNext = viewPosition.nodeAfter;
export function modelViewMergeAfter( evt, data, conversionApi ) {
const viewPosition = conversionApi.mapper.toViewPosition( data.position );
const viewItemPrev = viewPosition.nodeBefore;
const viewItemNext = viewPosition.nodeAfter;
// Merge lists if something (remove, move) was done from inside of list.
// Merging will be done only if both items are view lists of the same type.
// The check is done inside the helper function.
mergeViewLists( viewItemPrev, viewItemNext );
}
// Merge lists if something (remove, move) was done from inside of list.
// Merging will be done only if both items are view lists of the same type.
// The check is done inside the helper function.
mergeViewLists( conversionApi.writer, viewItemPrev, viewItemNext );
}

@@ -355,55 +347,69 @@

* * checks `<li>`'s parent,
* * passes the `data.indent` value when `<li>`'s sub-items are converted.
* * stores and increases the `conversionApi.store.indent` value when `<li>`'s sub-items are converted.
*
* @see module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
* @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
* @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
* @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by the callback.
*/
export function viewModelConverter( evt, data, consumable, conversionApi ) {
if ( consumable.consume( data.input, { name: true } ) ) {
export function viewModelConverter( evt, data, conversionApi ) {
if ( conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
const writer = conversionApi.writer;
const conversionStore = this.conversionApi.store;
// 1. Create `listItem` model element.
const listItem = new ModelElement( 'listItem' );
const listItem = writer.createElement( 'listItem' );
// 2. Handle `listItem` model element attributes.
data.indent = data.indent ? data.indent : 0;
listItem.setAttribute( 'indent', data.indent );
conversionStore.indent = conversionStore.indent || 0;
writer.setAttribute( 'indent', conversionStore.indent, listItem );
// Set 'bulleted' as default. If this item is pasted into a context,
const type = data.input.parent && data.input.parent.name == 'ol' ? 'numbered' : 'bulleted';
listItem.setAttribute( 'type', type );
const type = data.viewItem.parent && data.viewItem.parent.name == 'ol' ? 'numbered' : 'bulleted';
writer.setAttribute( 'type', type, listItem );
// 3. Handle `<li>` children.
data.context.push( listItem );
// `listItem`s created recursively should have bigger indent.
data.indent++;
conversionStore.indent++;
// `listItem`s will be kept in flat structure.
const items = new ModelDocumentFragment();
items.appendChildren( listItem );
// Try to find allowed parent for list item.
const splitResult = conversionApi.splitToAllowedParent( listItem, data.modelCursor );
// When there is no allowed parent it means that list item cannot be converted at current model position
// and in any of position ancestors.
if ( !splitResult ) {
return;
}
writer.insert( listItem, splitResult.position );
// Remember position after list item, next list items will be inserted at this position.
let nextPosition = ModelPosition.createAfter( listItem );
// Check all children of the converted `<li>`.
// At this point we assume there are no "whitespace" view text nodes in view list, between view list items.
// This should be handled by `<ul>` and `<ol>` converters.
for ( const child of data.input.getChildren() ) {
// Let's convert the child.
const converted = conversionApi.convertItem( child, consumable, data );
// If this is a view list element, we will convert it and concat the result (`listItem` model elements)
// with already gathered results (in `items` array). `converted` should be a `ModelDocumentFragment`.
for ( const child of data.viewItem.getChildren() ) {
// If this is a view list element, we will convert it after last `listItem` model element.
if ( child.name == 'ul' || child.name == 'ol' ) {
items.appendChildren( Array.from( converted.getChildren() ) );
nextPosition = conversionApi.convertItem( child, nextPosition ).modelCursor;
}
// If it was not a list it was a "regular" list item content. Just append it to `listItem`.
// If it was not a list it was a "regular" list item content. Just convert it to `listItem`.
else {
modelWriter.insert( ModelPosition.createAt( listItem, 'end' ), converted );
conversionApi.convertItem( child, ModelPosition.createAt( listItem, 'end' ) );
}
}
data.indent--;
data.context.pop();
conversionStore.indent--;
data.output = items;
// Result range starts before the first item and ends after the last.
data.modelRange = new ModelRange( data.modelCursor, nextPosition );
// When modelCursor parent had to be split to insert list item.
if ( splitResult.cursorParent ) {
// Then continue conversion in split element.
data.modelCursor = ModelPosition.createAt( splitResult.cursorParent );
} else {
// Otherwise continue conversion after last list item.
data.modelCursor = data.modelRange.end;
}
}

@@ -417,15 +423,15 @@ }

*
* @see module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
* @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
* @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
* @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by the callback.
*/
export function cleanList( evt, data, consumable ) {
if ( consumable.test( data.input, { name: true } ) ) {
export function cleanList( evt, data, conversionApi ) {
if ( conversionApi.consumable.test( data.viewItem, { name: true } ) ) {
// Caching children because when we start removing them iterating fails.
const children = Array.from( data.input.getChildren() );
const children = Array.from( data.viewItem.getChildren() );
for ( const child of children ) {
if ( !child.is( 'li' ) ) {
child.remove();
child._remove();
}

@@ -439,14 +445,14 @@ }

*
* @see module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
* @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
* @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
* @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by the callback.
*/
export function cleanListItem( evt, data, consumable ) {
if ( consumable.test( data.input, { name: true } ) ) {
if ( data.input.childCount === 0 ) {
export function cleanListItem( evt, data, conversionApi ) {
if ( conversionApi.consumable.test( data.viewItem, { name: true } ) ) {
if ( data.viewItem.childCount === 0 ) {
return;
}
const children = [ ...data.input.getChildren() ];
const children = [ ...data.viewItem.getChildren() ];

@@ -458,3 +464,3 @@ let foundList = false;

if ( foundList && !child.is( 'ul' ) && !child.is( 'ol' ) ) {
child.remove();
child._remove();
}

@@ -465,3 +471,3 @@

if ( firstNode ) {
child.data = child.data.replace( /^\s+/, '' );
conversionApi.writer.setTextData( child.data.replace( /^\s+/, '' ), child );
}

@@ -471,3 +477,3 @@

if ( !child.nextSibling || ( child.nextSibling.is( 'ul' ) || child.nextSibling.is( 'ol' ) ) ) {
child.data = child.data.replace( /\s+$/, '' );
conversionApi.writer.setTextData( child.data.replace( /\s+$/, '' ), child );
}

@@ -494,2 +500,6 @@ } else if ( child.is( 'ul' ) || child.is( 'ol' ) ) {

export function modelToViewPosition( evt, data ) {
if ( data.isPhantom ) {
return;
}
const modelItem = data.modelPosition.nodeBefore;

@@ -578,3 +588,4 @@

*
* Example:
* In an example below, there is a correct list structure.
* Then the middle element will be removed so the list structure will become incorrect:
*

@@ -585,2 +596,7 @@ * <listItem type="bulleted" indent=0>Item 1</listItem>

*
* List structure after the middle element removed:
*
* <listItem type="bulleted" indent=0>Item 1</listItem>
* <listItem type="bulleted" indent=2>Item 3</listItem>
*
* Should become:

@@ -591,140 +607,142 @@ *

*
* @param {module:engine/model/document~Document} document The document to observe.
* @returns {Function} A callback to be attached to the {@link module:engine/model/document~Document#event:change document change event}.
* @param {module:engine/model/model~Model} model The data model.
* @param {module:engine/model/writer~Writer} writer The writer to do changes with.
* @returns {Boolean} `true` if any change has been applied, `false` otherwise.
*/
export function modelChangePostFixer( document ) {
return ( evt, type, changes, batch ) => {
if ( batch.type == 'transparent' ) {
return;
}
export function modelChangePostFixer( model, writer ) {
const changes = model.document.differ.getChanges();
const itemToListHead = new Map();
if ( type == 'remove' ) {
const howMany = changes.range.end.offset - changes.range.start.offset;
const sourcePos = changes.sourcePosition._getTransformedByInsertion( changes.range.start, howMany, true );
let applied = false;
// Fix list items after the cut-out range.
// This fix is needed if items in model after cut-out range have now wrong indents compared to their previous siblings.
_fixItemsIndent( sourcePos, document, batch );
// This fix is needed if two different nested lists got merged, change types of list items "below".
_fixItemsType( sourcePos, false, document, batch );
} else if ( type == 'move' ) {
const howMany = changes.range.end.offset - changes.range.start.offset;
const sourcePos = changes.sourcePosition._getTransformedByInsertion( changes.range.start, howMany, true );
for ( const entry of changes ) {
if ( entry.type == 'insert' && entry.name == 'listItem' ) {
_addListToFix( entry.position );
} else if ( entry.type == 'insert' && entry.name != 'listItem' ) {
if ( entry.name != '$text' ) {
// In case of renamed element.
const item = entry.position.nodeAfter;
// Fix list items after the cut-out range.
// This fix is needed if items in model after cut-out range have now wrong indents compared to their previous siblings.
_fixItemsIndent( sourcePos, document, batch );
// This fix is needed if two different nested lists got merged, change types of list items "below".
_fixItemsType( sourcePos, false, document, batch );
if ( item.hasAttribute( 'indent' ) ) {
writer.removeAttribute( 'indent', item );
// Fix items in moved range.
// This fix is needed if inserted items are too deeply intended.
_fixItemsIndent( changes.range.start, document, batch );
// This fix is needed if one or more first inserted items have different type.
_fixItemsType( changes.range.start, false, document, batch );
applied = true;
}
// Fix list items after inserted range.
// This fix is needed if items in model after inserted range have wrong indents.
_fixItemsIndent( changes.range.end, document, batch );
// This fix is needed if one or more last inserted items have different type.
_fixItemsType( changes.range.end, true, document, batch );
} else if ( type == 'rename' && changes.oldName == 'listItem' && changes.newName != 'listItem' ) {
const element = changes.element;
if ( item.hasAttribute( 'type' ) ) {
writer.removeAttribute( 'type', item );
// Element name is changed from list to something else. Remove useless attributes.
document.enqueueChanges( () => {
batch.removeAttribute( element, 'indent' ).removeAttribute( element, 'type' );
} );
applied = true;
}
}
const changePos = ModelPosition.createAfter( changes.element );
const posAfter = entry.position.getShiftedBy( entry.length );
// Fix list items after the renamed element.
// This fix is needed if there are items after renamed element, those items should start from indent = 0.
_fixItemsIndent( changePos, document, batch );
} else if ( type == 'insert' ) {
// Fix list items in inserted range.
// This fix is needed if inserted items are too deeply intended.
_fixItemsIndent( changes.range.start, document, batch );
// This fix is needed if one or more first inserted items have different type.
_fixItemsType( changes.range.start, false, document, batch );
// Fix list items after inserted range.
// This fix is needed if items in model after inserted range have wrong indents.
_fixItemsIndent( changes.range.end, document, batch );
// This fix is needed if one or more last inserted items have different type.
_fixItemsType( changes.range.end, true, document, batch );
_addListToFix( posAfter );
} else if ( entry.type == 'remove' && entry.name == 'listItem' ) {
_addListToFix( entry.position );
} else if ( entry.type == 'attribute' && entry.attributeKey == 'indent' ) {
_addListToFix( entry.range.start );
} else if ( entry.type == 'attribute' && entry.attributeKey == 'type' ) {
_addListToFix( entry.range.start );
}
};
}
}
// Helper function for post fixer callback. Performs fixing of model `listElement` items indent attribute. Checks the model at the
// `changePosition`. Looks at the node before position where change occurred and uses that node as a reference for following list items.
function _fixItemsIndent( changePosition, document, batch ) {
let nextItem = changePosition.nodeAfter;
for ( const listHead of itemToListHead.values() ) {
_fixListIndents( listHead );
_fixListTypes( listHead );
}
if ( nextItem && nextItem.name == 'listItem' ) {
document.enqueueChanges( () => {
const prevItem = nextItem.previousSibling;
// This is the maximum indent that following model list item may have.
const maxIndent = prevItem && prevItem.is( 'listItem' ) ? prevItem.getAttribute( 'indent' ) + 1 : 0;
return applied;
// Check how much the next item needs to be outdented.
let outdentBy = nextItem.getAttribute( 'indent' ) - maxIndent;
const items = [];
function _addListToFix( position ) {
const prev = position.nodeBefore;
while ( nextItem && nextItem.name == 'listItem' && nextItem.getAttribute( 'indent' ) > maxIndent ) {
if ( outdentBy > nextItem.getAttribute( 'indent' ) ) {
outdentBy = nextItem.getAttribute( 'indent' );
}
if ( !prev || !prev.is( 'listItem' ) ) {
const item = position.nodeAfter;
const newIndent = nextItem.getAttribute( 'indent' ) - outdentBy;
if ( item && item.is( 'listItem' ) ) {
itemToListHead.set( item, item );
}
} else {
let listHead = prev;
items.push( { item: nextItem, indent: newIndent } );
nextItem = nextItem.nextSibling;
if ( itemToListHead.has( listHead ) ) {
return;
}
if ( items.length > 0 ) {
// Since we are outdenting list items, it is safer to start from the last one (it will maintain correct model state).
for ( const item of items.reverse() ) {
batch.setAttribute( item.item, 'indent', item.indent );
while ( listHead.previousSibling && listHead.previousSibling.is( 'listItem' ) ) {
listHead = listHead.previousSibling;
if ( itemToListHead.has( listHead ) ) {
return;
}
}
} );
itemToListHead.set( position.nodeBefore, listHead );
}
}
}
// Helper function for post fixer callback. Performs fixing of model nested `listElement` items type attribute.
// Checks the model at the `changePosition`. Looks at nodes after/before that position and changes those items type
// to the same as node before/after `changePosition`.
function _fixItemsType( changePosition, fixPrevious, document, batch ) {
let item = changePosition[ fixPrevious ? 'nodeBefore' : 'nodeAfter' ];
function _fixListIndents( item ) {
let maxIndent = 0;
let fixBy = null;
if ( !item || !item.is( 'listItem' ) || item.getAttribute( 'indent' ) === 0 ) {
// !item - when last item got removed.
// !item.is( 'listItem' ) - when first element to fix is not a list item already.
// indent === 0 - do not fix if changes are done on top level lists.
return;
}
while ( item && item.is( 'listItem' ) ) {
const itemIndent = item.getAttribute( 'indent' );
document.enqueueChanges( () => {
const refItem = _getBoundaryItemOfSameList( item, !fixPrevious );
if ( itemIndent > maxIndent ) {
let newIndent;
if ( !refItem || refItem == item ) {
// !refItem - happens if first list item is inserted.
// refItem == item - happens if last item is inserted.
return;
if ( fixBy === null ) {
fixBy = itemIndent - maxIndent;
newIndent = maxIndent;
} else {
if ( fixBy > itemIndent ) {
fixBy = itemIndent;
}
newIndent = itemIndent - fixBy;
}
writer.setAttribute( 'indent', newIndent, item );
applied = true;
} else {
fixBy = null;
maxIndent = item.getAttribute( 'indent' ) + 1;
}
item = item.nextSibling;
}
}
const refIndent = refItem.getAttribute( 'indent' );
const refType = refItem.getAttribute( 'type' );
function _fixListTypes( item ) {
let typesStack = [];
let prev = null;
while ( item && item.is( 'listItem' ) && item.getAttribute( 'indent' ) >= refIndent ) {
if ( item.getAttribute( 'type' ) != refType && item.getAttribute( 'indent' ) == refIndent ) {
batch.setAttribute( item, 'type', refType );
while ( item && item.is( 'listItem' ) ) {
const itemIndent = item.getAttribute( 'indent' );
if ( prev && prev.getAttribute( 'indent' ) > itemIndent ) {
typesStack = typesStack.slice( 0, itemIndent + 1 );
}
item = item[ fixPrevious ? 'previousSibling' : 'nextSibling' ];
if ( itemIndent != 0 ) {
if ( typesStack[ itemIndent ] ) {
const type = typesStack[ itemIndent ];
if ( item.getAttribute( 'type' ) != type ) {
writer.setAttribute( 'type', type, item );
applied = true;
}
} else {
typesStack[ itemIndent ] = item.getAttribute( 'type' );
}
}
prev = item;
item = item.nextSibling;
}
} );
}
}

@@ -753,3 +771,3 @@

* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
* @param {Array} args Arguments of {@link module:engine/controller/datacontroller~DataController#insertContent}.
* @param {Array} args Arguments of {@link module:engine/model/model~Model#insertContent}.
*/

@@ -786,3 +804,3 @@ export function modelIndentPasteFixer( evt, [ content, selection ] ) {

while ( item && item.is( 'listItem' ) ) {
item.setAttribute( 'indent', item.getAttribute( 'indent' ) + indentChange );
item._setAttribute( 'indent', item.getAttribute( 'indent' ) + indentChange );

@@ -799,8 +817,10 @@ item = item.nextSibling;

// The function then returns created view list item (<li>).
function generateLiInUl( modelItem, mapper ) {
function generateLiInUl( modelItem, conversionApi ) {
const mapper = conversionApi.mapper;
const viewWriter = conversionApi.writer;
const listType = modelItem.getAttribute( 'type' ) == 'numbered' ? 'ol' : 'ul';
const viewItem = new ViewListItemElement();
const viewItem = createViewListItemElement( viewWriter );
const viewList = new ViewContainerElement( listType, null );
viewList.appendChildren( viewItem );
const viewList = viewWriter.createContainerElement( listType, null );
viewWriter.insert( ViewPosition.createAt( viewList ), viewItem );

@@ -814,22 +834,12 @@ mapper.bindElements( modelItem, viewItem );

// `options` object may contain one or more of given values (by default they are `false`):
// `options.getNext` - whether next or previous siblings should be checked (default = previous)
// `options.checkAllSiblings` - whether all siblings or just the first one should be checked (default = only one),
// `options.sameIndent` - whether sought sibling should have same indent (default = no),
// `options.biggerIndent` - whether sought sibling should have bigger indent (default = no).
// `options.smallerIndent` - whether sought sibling should have smaller indent (default = no).
// `options.isMapped` - whether sought sibling must be mapped to view (default = no).
// `options.mapper` - used to map model elements when `isMapped` option is set to true.
// `options.indent` - used as reference item when first parameter is a position
// Either `options.sameIndent` or `options.biggerIndent` should be set to `true`.
function getSiblingListItem( modelItemOrPosition, options ) {
const direction = options.getNext ? 'nextSibling' : 'previousSibling';
const posDirection = options.getNext ? 'nodeAfter' : 'nodeBefore';
const checkAllSiblings = !!options.checkAllSiblings;
const sameIndent = !!options.sameIndent;
const biggerIndent = !!options.biggerIndent;
const smallerIndent = !!options.smallerIndent;
const isMapped = !!options.isMapped;
const indent = modelItemOrPosition instanceof ModelElement ? modelItemOrPosition.getAttribute( 'indent' ) : options.indent;
let item = modelItemOrPosition instanceof ModelElement ? modelItemOrPosition[ direction ] : modelItemOrPosition[ posDirection ];
let item = modelItemOrPosition instanceof ModelElement ? modelItemOrPosition.previousSibling : modelItemOrPosition.nodeBefore;

@@ -839,21 +849,7 @@ while ( item && item.name == 'listItem' ) {

if (
( sameIndent && indent == itemIndent ) ||
( biggerIndent && indent < itemIndent ) ||
( smallerIndent && indent > itemIndent )
) {
if ( !isMapped || options.mapper.toViewElement( item ) ) {
return item;
} else {
item = item[ direction ];
continue;
}
if ( ( sameIndent && indent == itemIndent ) || ( smallerIndent && indent > itemIndent ) ) {
return item;
}
if ( !checkAllSiblings ) {
return null;
}
item = item[ direction ];
item = item.previousSibling;
}

@@ -866,3 +862,3 @@

// The merge happen only if both parameters are UL or OL elements.
function mergeViewLists( firstList, secondList ) {
function mergeViewLists( viewWriter, firstList, secondList ) {
if ( firstList && secondList && ( firstList.name == 'ul' || firstList.name == 'ol' ) && firstList.name == secondList.name ) {

@@ -879,40 +875,27 @@ return viewWriter.mergeContainers( ViewPosition.createAfter( firstList ) );

// See comments below to better understand the algorithm.
function injectViewList( modelItem, injectedItem, mapper, removePosition ) {
function injectViewList( modelItem, injectedItem, conversionApi ) {
const injectedList = injectedItem.parent;
const mapper = conversionApi.mapper;
const viewWriter = conversionApi.writer;
// Position where view list will be inserted.
let insertPosition;
let insertPosition = mapper.toViewPosition( ModelPosition.createBefore( 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" if injected 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".
let prevItem = getSiblingListItem( modelItem, { sameIndent: true, smallerIndent: true, checkAllSiblings: true } );
const refItem = getSiblingListItem( modelItem, { sameIndent: true, smallerIndent: true } );
const prevItem = modelItem.previousSibling;
if ( prevItem && prevItem.getAttribute( 'indent' ) == modelItem.getAttribute( 'indent' ) ) {
if ( refItem && refItem.getAttribute( 'indent' ) == modelItem.getAttribute( 'indent' ) ) {
// 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( prevItem );
const viewItem = mapper.toViewElement( refItem );
insertPosition = viewWriter.breakContainer( ViewPosition.createAfter( viewItem ) );
} else {
// There is no list item with same indent. Check previous model item.
prevItem = modelItem.previousSibling;
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( ModelPosition.createAt( prevItem, 'end' ) );
// ^ ACTUALLY NOT BECAUSE FIXING DOES NOT WORK PROPERLY.
// TODO: fix this part of code when conversion from model to view is done on `changesDone` event or post/prefixing is better.
if ( prevItem.getAttribute( 'indent' ) < modelItem.getAttribute( 'indent' ) ) {
// Lower indent, correct model, previous item is a parent and this model item is its nested item.
insertPosition = mapper.toViewPosition( ModelPosition.createAt( prevItem, 'end' ) );
} else {
// Higher indent, incorrect model that is probably being fixed. Inject the view list where it was.
// TODO: get rid of `removePosition` when conversion is done on `changesDone`.
if ( removePosition.parent.is( 'ul' ) || removePosition.parent.is( 'ol' ) ) {
insertPosition = viewWriter.breakContainer( removePosition );
} else {
insertPosition = removePosition;
}
}
// 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( ModelPosition.createAt( prevItem, 'end' ) );
} else {

@@ -931,32 +914,51 @@ // Previous item is not a list item (or does not exist at all).

// 2. Handle possible children of injected model item.
// We have to check if next list item in model has bigger indent. If it has, it means that it and possibly
// some following list items should be nested in the injected view item.
// Look only after model elements that are already mapped to view. Some following model items might not be mapped
// if multiple items in model were inserted/moved at once.
const nextItem = getSiblingListItem(
modelItem,
{ biggerIndent: true, getNext: true, isMapped: true, mapper }
);
if ( prevItem && prevItem.name == 'listItem' ) {
const prevView = mapper.toViewElement( prevItem );
if ( nextItem ) {
const viewItem = mapper.toViewElement( nextItem );
const walker = new ViewTreeWalker( {
boundaries: new ViewRange(
ViewPosition.createAt( prevView, 0 ),
insertPosition
),
ignoreElementEnd: true
} );
// Break the list between found view item and its preceding `<li>`s.
viewWriter.breakContainer( ViewPosition.createBefore( viewItem ) );
for ( const value of walker ) {
if ( value.item.is( 'li' ) ) {
const breakPosition = viewWriter.breakContainer( ViewPosition.createBefore( value.item ) );
const viewList = value.item.parent;
// The broken ("lower") part will be moved as nested children of the inserted view item.
const sourceStart = ViewPosition.createBefore( viewItem.parent );
const targetPosition = ViewPosition.createAt( injectedItem, 'end' );
mergeViewLists( viewWriter, targetPosition.nodeBefore, targetPosition.nodeAfter );
viewWriter.move( ViewRange.createOn( viewList ), targetPosition );
const lastModelItem = _getBoundaryItemOfSameList( nextItem, false );
const lastViewItem = mapper.toViewElement( lastModelItem );
const sourceEnd = viewWriter.breakContainer( ViewPosition.createAfter( lastViewItem ) );
const sourceRange = new ViewRange( sourceStart, sourceEnd );
walker.position = breakPosition;
}
}
} else {
const nextViewList = injectedList.nextSibling;
const targetPosition = ViewPosition.createAt( injectedItem, 'end' );
viewWriter.move( sourceRange, targetPosition );
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( 'indent' ) > modelItem.getAttribute( 'indent' ) ) {
lastSubChild = child;
} else {
break;
}
}
if ( lastSubChild ) {
viewWriter.breakContainer( ViewPosition.createAfter( lastSubChild ) );
viewWriter.move( ViewRange.createOn( lastSubChild.parent ), ViewPosition.createAt( injectedItem, 'end' ) );
}
}
}
// Merge inserted view list with its possible neighbour lists.
mergeViewLists( injectedList, injectedList.nextSibling );
mergeViewLists( injectedList.previousSibling, injectedList );
mergeViewLists( viewWriter, injectedList, injectedList.nextSibling );
mergeViewLists( viewWriter, injectedList.previousSibling, injectedList );
}

@@ -966,3 +968,3 @@

// to other given parameters.
function hoistNestedLists( nextIndent, modelRemoveStartPosition, viewRemoveStartPosition, viewRemovedItem, mapper ) {
function hoistNestedLists( nextIndent, modelRemoveStartPosition, viewRemoveStartPosition, viewRemovedItem, conversionApi ) {
// Find correct previous model list item element.

@@ -975,6 +977,8 @@ // The element has to have either same or smaller indent than given reference indent.

smallerIndent: true,
checkAllSiblings: true,
indent: nextIndent
} );
const mapper = conversionApi.mapper;
const viewWriter = conversionApi.writer;
// Indent of found element or `null` if the element has not been found.

@@ -1054,4 +1058,4 @@ const prevIndent = prevModelItem ? prevModelItem.getAttribute( 'indent' ) : null;

mergeViewLists( child, child.nextSibling );
mergeViewLists( child.previousSibling, child );
mergeViewLists( viewWriter, child, child.nextSibling );
mergeViewLists( viewWriter, child.previousSibling, child );
}

@@ -1061,20 +1065,2 @@ }

// Helper function to obtain the first or the last model list item which is in on the same indent level as given `item`.
function _getBoundaryItemOfSameList( item, getFirst ) {
const indent = item.getAttribute( 'indent' );
const direction = getFirst ? 'previousSibling' : 'nextSibling';
let result = item;
while ( item[ direction ] && item[ direction ].is( 'listItem' ) && item[ direction ].getAttribute( 'indent' ) >= indent ) {
item = item[ direction ];
if ( item.getAttribute( 'indent' ) == indent ) {
result = item;
}
}
return result;
}
// Helper function that for given `view.Position`, returns a `view.Position` that is after all `view.UIElement`s that

@@ -1081,0 +1067,0 @@ // are after given position.

/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.

@@ -52,7 +52,7 @@ */

execute() {
const doc = this.editor.document;
const batch = doc.batch();
const model = this.editor.model;
const doc = model.document;
let itemsToChange = Array.from( doc.selection.getSelectedBlocks() );
doc.enqueueChanges( () => {
model.change( writer => {
const lastItem = itemsToChange[ itemsToChange.length - 1 ];

@@ -87,19 +87,9 @@

// No need to remove attributes, will be removed by post fixer.
batch.rename( item, 'paragraph' );
writer.rename( item, 'paragraph' );
}
// If indent is >= 0, change the attribute value.
else {
batch.setAttribute( item, 'indent', indent );
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, batch );
}
} );

@@ -116,3 +106,3 @@ }

// Check whether any of position's ancestor is a list item.
const listItem = first( this.editor.document.selection.getSelectedBlocks() );
const listItem = first( this.editor.model.document.selection.getSelectedBlocks() );

@@ -152,42 +142,1 @@ // If selection is not in a list item, the command is disabled.

}
// 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, batch ) {
// 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 ) {
batch.setAttribute( item, 'type', prev.getAttribute( 'type' ) );
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 ) {
batch.setAttribute( item, 'type', next.getAttribute( 'type' ) );
}
}
// 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;
}
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.

@@ -10,15 +10,12 @@ */

import ListEngine from './listengine';
import ListEditing from './listediting';
import ListUI from './listui';
import numberedListIcon from '../theme/icons/numberedlist.svg';
import bulletedListIcon from '../theme/icons/bulletedlist.svg';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
/**
* The list feature. It introduces the `numberedList` and `bulletedList` buttons that
* allow to convert paragraphs to and from list items and indent or outdent them.
* The list feature.
*
* See also {@link module:list/listengine~ListEngine}.
* It loads the {@link module:list/listediting~ListEditing list editing feature}
* and {@link module:list/listui~ListUI list UI feature}.
*

@@ -32,3 +29,3 @@ * @extends module:core/plugin~Plugin

static get requires() {
return [ ListEngine ];
return [ ListEditing, ListUI ];
}

@@ -42,109 +39,2 @@

}
/**
* @inheritDoc
*/
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 );
// 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( this.editor.editing.view, 'enter', ( evt, data ) => {
const doc = this.editor.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( this.editor.editing.view, 'delete', ( evt, data ) => {
// Check conditions from those that require less computations like those immediately available.
if ( data.direction !== 'backward' ) {
return;
}
const selection = this.editor.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();
}
};
};
this.editor.keystrokes.set( 'Tab', getCommandExecuter( 'indentList' ) );
this.editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( 'outdentList' ) );
}
/**
* 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;
const command = editor.commands.get( commandName );
editor.ui.componentFactory.add( commandName, locale => {
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;
} );
}
}
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.

@@ -11,3 +11,2 @@ */

import Command from '@ckeditor/ckeditor5-core/src/command';
import Position from '@ckeditor/ckeditor5-engine/src/model/position';
import first from '@ckeditor/ckeditor5-utils/src/first';

@@ -59,10 +58,8 @@

* @protected
* @param {Object} [options] Options for the executed command.
* @param {module:engine/model/batch~Batch} [options.batch] A batch to collect all the change steps.
* A new batch will be created if this option is not set.
*/
execute( options = {} ) {
const document = this.editor.document;
execute() {
const model = this.editor.model;
const document = model.document;
const blocks = Array.from( document.selection.getSelectedBlocks() )
.filter( block => checkCanBecomeListItem( block, document.schema ) );
.filter( block => checkCanBecomeListItem( block, model.schema ) );

@@ -73,5 +70,3 @@ // Whether we are turning off some items.

document.enqueueChanges( () => {
const batch = options.batch || document.batch();
model.change( writer => {
// If part of a list got turned off, we need to handle (outdent) all of sub-items of the last turned-off item.

@@ -160,3 +155,3 @@ // To be sure that model is all the time in a good state, we first fix items below turned-off item.

for ( const item of changes ) {
batch.setAttribute( item.element, 'indent', item.indent );
writer.setAttribute( 'indent', item.indent, item.element );
}

@@ -211,11 +206,12 @@ }

// List item specific attributes are removed by post fixer.
batch.rename( element, 'paragraph' );
writer.rename( element, 'paragraph' );
} else if ( !turnOff && element.name != 'listItem' ) {
// We are turning on and the element is not a `listItem` - it should be converted to `listItem`.
// The order of operations is important to keep model in correct state.
batch.setAttribute( element, 'type', this.type ).setAttribute( element, 'indent', 0 ).rename( element, 'listItem' );
writer.setAttributes( { type: this.type, indent: 0 }, element );
writer.rename( element, 'listItem' );
} else if ( !turnOff && element.name == 'listItem' && element.getAttribute( 'type' ) != this.type ) {
// We are turning on and the element is a `listItem` but has different type - change it's type and
// type of it's all siblings that have same indent.
batch.setAttribute( element, 'type', this.type );
writer.setAttribute( 'type', this.type, element );
}

@@ -234,3 +230,3 @@ }

// Check whether closest `listItem` ancestor of the position has a correct type.
const listItem = first( this.editor.document.selection.getSelectedBlocks() );
const listItem = first( this.editor.model.document.selection.getSelectedBlocks() );

@@ -252,4 +248,4 @@ return !!listItem && listItem.is( 'listItem' ) && listItem.getAttribute( 'type' ) == this.type;

const selection = this.editor.document.selection;
const schema = this.editor.document.schema;
const selection = this.editor.model.document.selection;
const schema = this.editor.model.schema;

@@ -321,7 +317,3 @@ const firstBlock = first( selection.getSelectedBlocks() );

function checkCanBecomeListItem( block, schema ) {
return schema.check( {
name: 'listItem',
attributes: [ 'type', 'indent' ],
inside: Position.createBefore( block )
} );
return schema.checkChild( block.parent, 'listItem' ) && !schema.isObject( block );
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc