@ckeditor/ckeditor5-list 40.0.0 → 40.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +2 -2
- package/build/list.js +1 -1
- package/package.json +3 -3
- package/src/augmentation.d.ts +52 -52
- package/src/augmentation.js +5 -5
- package/src/documentlist/adjacentlistssupport.d.ts +15 -15
- package/src/documentlist/adjacentlistssupport.js +81 -81
- package/src/documentlist/converters.d.ts +65 -65
- package/src/documentlist/converters.js +441 -441
- package/src/documentlist/documentlistcommand.d.ts +80 -80
- package/src/documentlist/documentlistcommand.js +150 -150
- package/src/documentlist/documentlistediting.d.ts +212 -212
- package/src/documentlist/documentlistediting.js +646 -645
- package/src/documentlist/documentlistindentcommand.d.ts +62 -62
- package/src/documentlist/documentlistindentcommand.js +129 -129
- package/src/documentlist/documentlistmergecommand.d.ts +76 -76
- package/src/documentlist/documentlistmergecommand.js +174 -174
- package/src/documentlist/documentlistsplitcommand.d.ts +67 -67
- package/src/documentlist/documentlistsplitcommand.js +70 -70
- package/src/documentlist/documentlistutils.d.ts +46 -46
- package/src/documentlist/documentlistutils.js +50 -50
- package/src/documentlist/utils/listwalker.d.ts +145 -145
- package/src/documentlist/utils/listwalker.js +182 -182
- package/src/documentlist/utils/model.d.ts +202 -202
- package/src/documentlist/utils/model.js +455 -455
- package/src/documentlist/utils/postfixers.d.ts +37 -37
- package/src/documentlist/utils/postfixers.js +126 -126
- package/src/documentlist/utils/view.d.ts +81 -81
- package/src/documentlist/utils/view.js +117 -117
- package/src/documentlist.d.ts +26 -26
- package/src/documentlist.js +30 -30
- package/src/documentlistproperties/converters.d.ts +19 -19
- package/src/documentlistproperties/converters.js +43 -43
- package/src/documentlistproperties/documentlistpropertiesediting.d.ts +88 -88
- package/src/documentlistproperties/documentlistpropertiesediting.js +266 -266
- package/src/documentlistproperties/documentlistpropertiesutils.d.ts +33 -33
- package/src/documentlistproperties/documentlistpropertiesutils.js +44 -44
- package/src/documentlistproperties/documentlistreversedcommand.d.ts +36 -36
- package/src/documentlistproperties/documentlistreversedcommand.js +55 -55
- package/src/documentlistproperties/documentliststartcommand.d.ts +38 -38
- package/src/documentlistproperties/documentliststartcommand.js +57 -57
- package/src/documentlistproperties/documentliststylecommand.d.ts +72 -72
- package/src/documentlistproperties/documentliststylecommand.js +113 -113
- package/src/documentlistproperties/utils/style.d.ts +20 -20
- package/src/documentlistproperties/utils/style.js +54 -54
- package/src/documentlistproperties.d.ts +27 -27
- package/src/documentlistproperties.js +31 -31
- package/src/index.d.ts +43 -43
- package/src/index.js +29 -29
- package/src/list/converters.d.ts +196 -196
- package/src/list/converters.js +905 -905
- package/src/list/indentcommand.d.ts +37 -37
- package/src/list/indentcommand.js +107 -107
- package/src/list/listcommand.d.ts +55 -55
- package/src/list/listcommand.js +274 -274
- package/src/list/listediting.d.ts +32 -32
- package/src/list/listediting.js +161 -161
- package/src/list/listui.d.ts +19 -19
- package/src/list/listui.js +32 -32
- package/src/list/listutils.d.ts +41 -41
- package/src/list/listutils.js +46 -46
- package/src/list/utils.d.ts +112 -112
- package/src/list/utils.js +374 -374
- package/src/list.d.ts +26 -26
- package/src/list.js +30 -30
- package/src/listconfig.d.ts +132 -132
- package/src/listconfig.js +5 -5
- package/src/listproperties/listpropertiesediting.d.ts +72 -72
- package/src/listproperties/listpropertiesediting.js +696 -696
- package/src/listproperties/listpropertiesui.d.ts +23 -23
- package/src/listproperties/listpropertiesui.js +277 -277
- package/src/listproperties/listreversedcommand.d.ts +38 -38
- package/src/listproperties/listreversedcommand.js +52 -52
- package/src/listproperties/liststartcommand.d.ts +37 -37
- package/src/listproperties/liststartcommand.js +51 -51
- package/src/listproperties/liststylecommand.d.ts +67 -67
- package/src/listproperties/liststylecommand.js +99 -99
- package/src/listproperties/ui/collapsibleview.d.ts +63 -63
- package/src/listproperties/ui/collapsibleview.js +89 -89
- package/src/listproperties/ui/listpropertiesview.d.ts +157 -157
- package/src/listproperties/ui/listpropertiesview.js +299 -299
- package/src/listproperties.d.ts +26 -26
- package/src/listproperties.js +30 -30
- package/src/liststyle.d.ts +28 -28
- package/src/liststyle.js +36 -36
- package/src/tododocumentlist/checktododocumentlistcommand.d.ts +49 -49
- package/src/tododocumentlist/checktododocumentlistcommand.js +82 -82
- package/src/tododocumentlist/todocheckboxchangeobserver.d.ts +41 -41
- package/src/tododocumentlist/todocheckboxchangeobserver.js +37 -37
- package/src/tododocumentlist/tododocumentlistediting.d.ts +38 -38
- package/src/tododocumentlist/tododocumentlistediting.js +399 -399
- package/src/tododocumentlist.d.ts +27 -27
- package/src/tododocumentlist.js +31 -31
- package/src/todolist/checktodolistcommand.d.ts +52 -52
- package/src/todolist/checktodolistcommand.js +76 -76
- package/src/todolist/todolistconverters.d.ts +82 -82
- package/src/todolist/todolistconverters.js +260 -260
- package/src/todolist/todolistediting.d.ts +39 -39
- package/src/todolist/todolistediting.js +161 -161
- package/src/todolist/todolistui.d.ts +19 -19
- package/src/todolist/todolistui.js +29 -29
- package/src/todolist.d.ts +27 -27
- package/src/todolist.js +31 -31
- package/build/list.js.map +0 -1
|
@@ -1,399 +1,399 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @module list/tododocumentlist/tododocumentlistediting
|
|
7
|
-
*/
|
|
8
|
-
import { Matcher } from 'ckeditor5/src/engine';
|
|
9
|
-
import { getCode, parseKeystroke, getLocalizedArrowKeyCodeDirection } from 'ckeditor5/src/utils';
|
|
10
|
-
import { Plugin } from 'ckeditor5/src/core';
|
|
11
|
-
import { isFirstBlockOfListItem, isListItemBlock } from '../documentlist/utils/model';
|
|
12
|
-
import DocumentListEditing from '../documentlist/documentlistediting';
|
|
13
|
-
import DocumentListCommand from '../documentlist/documentlistcommand';
|
|
14
|
-
import CheckTodoDocumentListCommand from './checktododocumentlistcommand';
|
|
15
|
-
import TodoCheckboxChangeObserver from './todocheckboxchangeobserver';
|
|
16
|
-
const ITEM_TOGGLE_KEYSTROKE = parseKeystroke('Ctrl+Enter');
|
|
17
|
-
/**
|
|
18
|
-
* The engine of the to-do list feature. It handles creating, editing and removing to-do lists and their items.
|
|
19
|
-
*
|
|
20
|
-
* It registers the entire functionality of the {@link module:list/documentlist/documentlistediting~DocumentListEditing list editing plugin}
|
|
21
|
-
* and extends it with the commands:
|
|
22
|
-
*
|
|
23
|
-
* - `'todoList'`,
|
|
24
|
-
* - `'checkTodoList'`,
|
|
25
|
-
*/
|
|
26
|
-
export default class TodoDocumentListEditing extends Plugin {
|
|
27
|
-
/**
|
|
28
|
-
* @inheritDoc
|
|
29
|
-
*/
|
|
30
|
-
static get pluginName() {
|
|
31
|
-
return 'TodoDocumentListEditing';
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* @inheritDoc
|
|
35
|
-
*/
|
|
36
|
-
static get requires() {
|
|
37
|
-
return [DocumentListEditing];
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* @inheritDoc
|
|
41
|
-
*/
|
|
42
|
-
init() {
|
|
43
|
-
const editor = this.editor;
|
|
44
|
-
const model = editor.model;
|
|
45
|
-
const editing = editor.editing;
|
|
46
|
-
const documentListEditing = editor.plugins.get(DocumentListEditing);
|
|
47
|
-
const multiBlock = editor.config.get('list.multiBlock');
|
|
48
|
-
const elementName = multiBlock ? 'paragraph' : 'listItem';
|
|
49
|
-
editor.commands.add('todoList', new DocumentListCommand(editor, 'todo'));
|
|
50
|
-
editor.commands.add('checkTodoList', new CheckTodoDocumentListCommand(editor));
|
|
51
|
-
editing.view.addObserver(TodoCheckboxChangeObserver);
|
|
52
|
-
model.schema.extend('$listItem', { allowAttributes: 'todoListChecked' });
|
|
53
|
-
model.schema.addAttributeCheck((context, attributeName) => {
|
|
54
|
-
const item = context.last;
|
|
55
|
-
if (attributeName != 'todoListChecked') {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (!item.getAttribute('listItemId') || item.getAttribute('listType') != 'todo') {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
editor.conversion.for('upcast').add(dispatcher => {
|
|
63
|
-
// Upcast of to-do list item is based on a checkbox at the beginning of a <li> to keep compatibility with markdown input.
|
|
64
|
-
dispatcher.on('element:input', todoItemInputConverter());
|
|
65
|
-
// Consume other elements that are normally generated in data downcast, so they won't get captured by GHS.
|
|
66
|
-
dispatcher.on('element:label', elementUpcastConsumingConverter({ name: 'label', classes: 'todo-list__label' }));
|
|
67
|
-
dispatcher.on('element:label', elementUpcastConsumingConverter({ name: 'label', classes: ['todo-list__label', 'todo-list__label_without-description'] }));
|
|
68
|
-
dispatcher.on('element:span', elementUpcastConsumingConverter({ name: 'span', classes: 'todo-list__label__description' }));
|
|
69
|
-
dispatcher.on('element:ul', attributeUpcastConsumingConverter({ name: 'ul', classes: 'todo-list' }));
|
|
70
|
-
});
|
|
71
|
-
editor.conversion.for('downcast').elementToElement({
|
|
72
|
-
model: elementName,
|
|
73
|
-
view: (element, { writer }) => {
|
|
74
|
-
if (isDescriptionBlock(element, documentListEditing.getListAttributeNames())) {
|
|
75
|
-
return writer.createContainerElement('span', { class: 'todo-list__label__description' });
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
converterPriority: 'highest'
|
|
79
|
-
});
|
|
80
|
-
documentListEditing.registerDowncastStrategy({
|
|
81
|
-
scope: 'list',
|
|
82
|
-
attributeName: 'listType',
|
|
83
|
-
setAttributeOnDowncast(writer, value, element) {
|
|
84
|
-
if (value == 'todo') {
|
|
85
|
-
writer.addClass('todo-list', element);
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
writer.removeClass('todo-list', element);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
documentListEditing.registerDowncastStrategy({
|
|
93
|
-
scope: 'itemMarker',
|
|
94
|
-
attributeName: 'todoListChecked',
|
|
95
|
-
createElement(writer, modelElement, { dataPipeline }) {
|
|
96
|
-
if (modelElement.getAttribute('listType') != 'todo') {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
const viewElement = writer.createEmptyElement('input', {
|
|
100
|
-
type: 'checkbox',
|
|
101
|
-
...(modelElement.getAttribute('todoListChecked') ?
|
|
102
|
-
{ checked: 'checked' } :
|
|
103
|
-
null),
|
|
104
|
-
...(dataPipeline ?
|
|
105
|
-
{ disabled: 'disabled' } :
|
|
106
|
-
{ tabindex: '-1' })
|
|
107
|
-
});
|
|
108
|
-
if (dataPipeline) {
|
|
109
|
-
return viewElement;
|
|
110
|
-
}
|
|
111
|
-
return writer.createContainerElement('span', { contenteditable: 'false' }, viewElement);
|
|
112
|
-
},
|
|
113
|
-
canWrapElement(modelElement) {
|
|
114
|
-
return isDescriptionBlock(modelElement, documentListEditing.getListAttributeNames());
|
|
115
|
-
},
|
|
116
|
-
createWrapperElement(writer, modelElement, { dataPipeline }) {
|
|
117
|
-
const classes = ['todo-list__label'];
|
|
118
|
-
if (!isDescriptionBlock(modelElement, documentListEditing.getListAttributeNames())) {
|
|
119
|
-
classes.push('todo-list__label_without-description');
|
|
120
|
-
}
|
|
121
|
-
return writer.createAttributeElement(dataPipeline ? 'label' : 'span', {
|
|
122
|
-
class: classes.join(' ')
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
// We need to register the model length callback for the view checkbox input because it has no mapped model element.
|
|
127
|
-
// The to-do list item checkbox does not use the UIElement because it would be trimmed by ViewRange#getTrimmed()
|
|
128
|
-
// and removing the default remove converter would not include checkbox in the range to remove.
|
|
129
|
-
editing.mapper.registerViewToModelLength('input', viewElement => {
|
|
130
|
-
if (viewElement.getAttribute('type') == 'checkbox' &&
|
|
131
|
-
viewElement.findAncestor({ classes: 'todo-list__label' })) {
|
|
132
|
-
return 0;
|
|
133
|
-
}
|
|
134
|
-
return editing.mapper.toModelElement(viewElement) ? 1 : 0;
|
|
135
|
-
});
|
|
136
|
-
// Verifies if a to-do list block requires reconversion of a first item downcasted as an item description.
|
|
137
|
-
documentListEditing.on('checkElement', (evt, { modelElement, viewElement }) => {
|
|
138
|
-
const isFirstTodoModelParagraphBlock = isDescriptionBlock(modelElement, documentListEditing.getListAttributeNames());
|
|
139
|
-
const hasViewClass = viewElement.hasClass('todo-list__label__description');
|
|
140
|
-
if (hasViewClass != isFirstTodoModelParagraphBlock) {
|
|
141
|
-
evt.return = true;
|
|
142
|
-
evt.stop();
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
// Verifies if a to-do list block requires reconversion of a checkbox element
|
|
146
|
-
// (for example there is a new paragraph inserted as a first block of a list item).
|
|
147
|
-
documentListEditing.on('checkElement', (evt, { modelElement, viewElement }) => {
|
|
148
|
-
const isFirstTodoModelItemBlock = modelElement.getAttribute('listType') == 'todo' && isFirstBlockOfListItem(modelElement);
|
|
149
|
-
let hasViewItemMarker = false;
|
|
150
|
-
const viewWalker = editor.editing.view.createPositionBefore(viewElement).getWalker({ direction: 'backward' });
|
|
151
|
-
for (const { item } of viewWalker) {
|
|
152
|
-
if (item.is('element') && editor.editing.mapper.toModelElement(item)) {
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
if (item.is('element', 'input') && item.getAttribute('type') == 'checkbox') {
|
|
156
|
-
hasViewItemMarker = true;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (hasViewItemMarker != isFirstTodoModelItemBlock) {
|
|
160
|
-
evt.return = true;
|
|
161
|
-
evt.stop();
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
// Make sure that all blocks of the same list item have the same todoListChecked attribute.
|
|
165
|
-
documentListEditing.on('postFixer', (evt, { listNodes, writer }) => {
|
|
166
|
-
for (const { node, previousNodeInList } of listNodes) {
|
|
167
|
-
// This is a first item of a nested list.
|
|
168
|
-
if (!previousNodeInList) {
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
if (previousNodeInList.getAttribute('listItemId') != node.getAttribute('listItemId')) {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
const previousHasAttribute = previousNodeInList.hasAttribute('todoListChecked');
|
|
175
|
-
const nodeHasAttribute = node.hasAttribute('todoListChecked');
|
|
176
|
-
if (nodeHasAttribute && !previousHasAttribute) {
|
|
177
|
-
writer.removeAttribute('todoListChecked', node);
|
|
178
|
-
evt.return = true;
|
|
179
|
-
}
|
|
180
|
-
else if (!nodeHasAttribute && previousHasAttribute) {
|
|
181
|
-
writer.setAttribute('todoListChecked', true, node);
|
|
182
|
-
evt.return = true;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
// Make sure that todoListChecked attribute is only present for to-do list items.
|
|
187
|
-
model.document.registerPostFixer(writer => {
|
|
188
|
-
const changes = model.document.differ.getChanges();
|
|
189
|
-
let wasFixed = false;
|
|
190
|
-
for (const change of changes) {
|
|
191
|
-
if (change.type == 'attribute' && change.attributeKey == 'listType') {
|
|
192
|
-
const element = change.range.start.nodeAfter;
|
|
193
|
-
if (change.attributeOldValue == 'todo' && element.hasAttribute('todoListChecked')) {
|
|
194
|
-
writer.removeAttribute('todoListChecked', element);
|
|
195
|
-
wasFixed = true;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
else if (change.type == 'insert' && change.name != '$text') {
|
|
199
|
-
for (const { item } of writer.createRangeOn(change.position.nodeAfter)) {
|
|
200
|
-
if (item.is('element') && item.getAttribute('listType') != 'todo' && item.hasAttribute('todoListChecked')) {
|
|
201
|
-
writer.removeAttribute('todoListChecked', item);
|
|
202
|
-
wasFixed = true;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return wasFixed;
|
|
208
|
-
});
|
|
209
|
-
// Toggle check state of selected to-do list items on keystroke.
|
|
210
|
-
this.listenTo(editing.view.document, 'keydown', (evt, data) => {
|
|
211
|
-
if (getCode(data) === ITEM_TOGGLE_KEYSTROKE) {
|
|
212
|
-
editor.execute('checkTodoList');
|
|
213
|
-
evt.stop();
|
|
214
|
-
}
|
|
215
|
-
}, { priority: 'high' });
|
|
216
|
-
// Toggle check state of a to-do list item clicked on the checkbox.
|
|
217
|
-
this.listenTo(editing.view.document, 'todoCheckboxChange', (evt, data) => {
|
|
218
|
-
const viewTarget = data.target;
|
|
219
|
-
if (!viewTarget || !viewTarget.is('element', 'input')) {
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const viewPositionAfter = editing.view.createPositionAfter(viewTarget);
|
|
223
|
-
const modelPositionAfter = editing.mapper.toModelPosition(viewPositionAfter);
|
|
224
|
-
const modelElement = modelPositionAfter.parent;
|
|
225
|
-
if (modelElement && isListItemBlock(modelElement) && modelElement.getAttribute('listType') == 'todo') {
|
|
226
|
-
this._handleCheckmarkChange(modelElement);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
// Jump at the start/end of the next node on right arrow key press, when selection is before the checkbox.
|
|
230
|
-
//
|
|
231
|
-
// <blockquote><p>Foo{}</p></blockquote>
|
|
232
|
-
// <ul><li><checkbox/>Bar</li></ul>
|
|
233
|
-
//
|
|
234
|
-
// press: `->`
|
|
235
|
-
//
|
|
236
|
-
// <blockquote><p>Foo</p></blockquote>
|
|
237
|
-
// <ul><li><checkbox/>{}Bar</li></ul>
|
|
238
|
-
//
|
|
239
|
-
this.listenTo(editing.view.document, 'arrowKey', jumpOverCheckmarkOnSideArrowKeyPress(model, editor.locale), { context: '$text' });
|
|
240
|
-
// Map view positions inside the checkbox and wrappers to the position in the first block of the list item.
|
|
241
|
-
this.listenTo(editing.mapper, 'viewToModelPosition', (evt, data) => {
|
|
242
|
-
const viewParent = data.viewPosition.parent;
|
|
243
|
-
const isStartOfListItem = viewParent.is('attributeElement', 'li') && data.viewPosition.offset == 0;
|
|
244
|
-
const isStartOfListLabel = isLabelElement(viewParent) && data.viewPosition.offset <= 1;
|
|
245
|
-
const isInInputWrapper = viewParent.is('element', 'span') &&
|
|
246
|
-
viewParent.getAttribute('contenteditable') == 'false' &&
|
|
247
|
-
isLabelElement(viewParent.parent);
|
|
248
|
-
if (!isStartOfListItem && !isStartOfListLabel && !isInInputWrapper) {
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
const nodeAfter = data.modelPosition.nodeAfter;
|
|
252
|
-
if (nodeAfter && nodeAfter.getAttribute('listType') == 'todo') {
|
|
253
|
-
data.modelPosition = model.createPositionAt(nodeAfter, 0);
|
|
254
|
-
}
|
|
255
|
-
}, { priority: 'low' });
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Handles the checkbox element change, moves the selection to the corresponding model item to make it possible
|
|
259
|
-
* to toggle the `todoListChecked` attribute using the command, and restores the selection position.
|
|
260
|
-
*
|
|
261
|
-
* Some say it's a hack :) Moving the selection only for executing the command on a certain node and restoring it after,
|
|
262
|
-
* is not a clear solution. We need to design an API for using commands beyond the selection range.
|
|
263
|
-
* See https://github.com/ckeditor/ckeditor5/issues/1954.
|
|
264
|
-
*/
|
|
265
|
-
_handleCheckmarkChange(listItem) {
|
|
266
|
-
const editor = this.editor;
|
|
267
|
-
const model = editor.model;
|
|
268
|
-
const previousSelectionRanges = Array.from(model.document.selection.getRanges());
|
|
269
|
-
model.change(writer => {
|
|
270
|
-
writer.setSelection(listItem, 'end');
|
|
271
|
-
editor.execute('checkTodoList');
|
|
272
|
-
writer.setSelection(previousSelectionRanges);
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Returns an upcast converter that detects a to-do list checkbox and marks the list item as a to-do list.
|
|
278
|
-
*/
|
|
279
|
-
function todoItemInputConverter() {
|
|
280
|
-
return (evt, data, conversionApi) => {
|
|
281
|
-
const modelCursor = data.modelCursor;
|
|
282
|
-
const modelItem = modelCursor.parent;
|
|
283
|
-
const viewItem = data.viewItem;
|
|
284
|
-
if (!conversionApi.consumable.test(viewItem, { name: true })) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
if (viewItem.getAttribute('type') != 'checkbox' || !modelCursor.isAtStart || !modelItem.hasAttribute('listType')) {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
conversionApi.consumable.consume(viewItem, { name: true });
|
|
291
|
-
const writer = conversionApi.writer;
|
|
292
|
-
writer.setAttribute('listType', 'todo', modelItem);
|
|
293
|
-
if (data.viewItem.hasAttribute('checked')) {
|
|
294
|
-
writer.setAttribute('todoListChecked', true, modelItem);
|
|
295
|
-
}
|
|
296
|
-
data.modelRange = writer.createRange(modelCursor);
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Returns an upcast converter that consumes element matching the given matcher pattern.
|
|
301
|
-
*/
|
|
302
|
-
function elementUpcastConsumingConverter(matcherPattern) {
|
|
303
|
-
const matcher = new Matcher(matcherPattern);
|
|
304
|
-
return (evt, data, conversionApi) => {
|
|
305
|
-
const matcherResult = matcher.match(data.viewItem);
|
|
306
|
-
if (!matcherResult) {
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (!conversionApi.consumable.consume(data.viewItem, matcherResult.match)) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
Object.assign(data, conversionApi.convertChildren(data.viewItem, data.modelCursor));
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Returns an upcast converter that consumes attributes matching the given matcher pattern.
|
|
317
|
-
*/
|
|
318
|
-
function attributeUpcastConsumingConverter(matcherPattern) {
|
|
319
|
-
const matcher = new Matcher(matcherPattern);
|
|
320
|
-
return (evt, data, conversionApi) => {
|
|
321
|
-
const matcherResult = matcher.match(data.viewItem);
|
|
322
|
-
if (!matcherResult) {
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
const match = matcherResult.match;
|
|
326
|
-
match.name = false;
|
|
327
|
-
conversionApi.consumable.consume(data.viewItem, match);
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Returns true if the given list item block should be converted as a description block of a to-do list item.
|
|
332
|
-
*/
|
|
333
|
-
function isDescriptionBlock(modelElement, listAttributeNames) {
|
|
334
|
-
return (modelElement.is('element', 'paragraph') || modelElement.is('element', 'listItem')) &&
|
|
335
|
-
modelElement.getAttribute('listType') == 'todo' &&
|
|
336
|
-
isFirstBlockOfListItem(modelElement) &&
|
|
337
|
-
hasOnlyListAttributes(modelElement, listAttributeNames);
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Returns true if only attributes from the given list are present on the model element.
|
|
341
|
-
*/
|
|
342
|
-
function hasOnlyListAttributes(modelElement, attributeNames) {
|
|
343
|
-
for (const attributeKey of modelElement.getAttributeKeys()) {
|
|
344
|
-
// Ignore selection attributes stored on block elements.
|
|
345
|
-
if (attributeKey.startsWith('selection:')) {
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
if (!attributeNames.includes(attributeKey)) {
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return true;
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Jump at the start and end of a to-do list item.
|
|
356
|
-
*/
|
|
357
|
-
function jumpOverCheckmarkOnSideArrowKeyPress(model, locale) {
|
|
358
|
-
return (eventInfo, domEventData) => {
|
|
359
|
-
const direction = getLocalizedArrowKeyCodeDirection(domEventData.keyCode, locale.contentLanguageDirection);
|
|
360
|
-
const schema = model.schema;
|
|
361
|
-
const selection = model.document.selection;
|
|
362
|
-
if (!selection.isCollapsed) {
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
const position = selection.getFirstPosition();
|
|
366
|
-
const parent = position.parent;
|
|
367
|
-
// Right arrow before a to-do list item.
|
|
368
|
-
if (direction == 'right' && position.isAtEnd) {
|
|
369
|
-
const newRange = schema.getNearestSelectionRange(model.createPositionAfter(parent), 'forward');
|
|
370
|
-
if (!newRange) {
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
const newRangeParent = newRange.start.parent;
|
|
374
|
-
if (newRangeParent && isListItemBlock(newRangeParent) && newRangeParent.getAttribute('listType') == 'todo') {
|
|
375
|
-
model.change(writer => writer.setSelection(newRange));
|
|
376
|
-
domEventData.preventDefault();
|
|
377
|
-
domEventData.stopPropagation();
|
|
378
|
-
eventInfo.stop();
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// Left arrow at the beginning of a to-do list item.
|
|
382
|
-
else if (direction == 'left' && position.isAtStart && isListItemBlock(parent) && parent.getAttribute('listType') == 'todo') {
|
|
383
|
-
const newRange = schema.getNearestSelectionRange(model.createPositionBefore(parent), 'backward');
|
|
384
|
-
if (!newRange) {
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
model.change(writer => writer.setSelection(newRange));
|
|
388
|
-
domEventData.preventDefault();
|
|
389
|
-
domEventData.stopPropagation();
|
|
390
|
-
eventInfo.stop();
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Returns true if the given element is a label element of a to-do list item.
|
|
396
|
-
*/
|
|
397
|
-
function isLabelElement(viewElement) {
|
|
398
|
-
return !!viewElement && viewElement.is('attributeElement') && viewElement.hasClass('todo-list__label');
|
|
399
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @module list/tododocumentlist/tododocumentlistediting
|
|
7
|
+
*/
|
|
8
|
+
import { Matcher } from 'ckeditor5/src/engine';
|
|
9
|
+
import { getCode, parseKeystroke, getLocalizedArrowKeyCodeDirection } from 'ckeditor5/src/utils';
|
|
10
|
+
import { Plugin } from 'ckeditor5/src/core';
|
|
11
|
+
import { isFirstBlockOfListItem, isListItemBlock } from '../documentlist/utils/model';
|
|
12
|
+
import DocumentListEditing from '../documentlist/documentlistediting';
|
|
13
|
+
import DocumentListCommand from '../documentlist/documentlistcommand';
|
|
14
|
+
import CheckTodoDocumentListCommand from './checktododocumentlistcommand';
|
|
15
|
+
import TodoCheckboxChangeObserver from './todocheckboxchangeobserver';
|
|
16
|
+
const ITEM_TOGGLE_KEYSTROKE = parseKeystroke('Ctrl+Enter');
|
|
17
|
+
/**
|
|
18
|
+
* The engine of the to-do list feature. It handles creating, editing and removing to-do lists and their items.
|
|
19
|
+
*
|
|
20
|
+
* It registers the entire functionality of the {@link module:list/documentlist/documentlistediting~DocumentListEditing list editing plugin}
|
|
21
|
+
* and extends it with the commands:
|
|
22
|
+
*
|
|
23
|
+
* - `'todoList'`,
|
|
24
|
+
* - `'checkTodoList'`,
|
|
25
|
+
*/
|
|
26
|
+
export default class TodoDocumentListEditing extends Plugin {
|
|
27
|
+
/**
|
|
28
|
+
* @inheritDoc
|
|
29
|
+
*/
|
|
30
|
+
static get pluginName() {
|
|
31
|
+
return 'TodoDocumentListEditing';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* @inheritDoc
|
|
35
|
+
*/
|
|
36
|
+
static get requires() {
|
|
37
|
+
return [DocumentListEditing];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @inheritDoc
|
|
41
|
+
*/
|
|
42
|
+
init() {
|
|
43
|
+
const editor = this.editor;
|
|
44
|
+
const model = editor.model;
|
|
45
|
+
const editing = editor.editing;
|
|
46
|
+
const documentListEditing = editor.plugins.get(DocumentListEditing);
|
|
47
|
+
const multiBlock = editor.config.get('list.multiBlock');
|
|
48
|
+
const elementName = multiBlock ? 'paragraph' : 'listItem';
|
|
49
|
+
editor.commands.add('todoList', new DocumentListCommand(editor, 'todo'));
|
|
50
|
+
editor.commands.add('checkTodoList', new CheckTodoDocumentListCommand(editor));
|
|
51
|
+
editing.view.addObserver(TodoCheckboxChangeObserver);
|
|
52
|
+
model.schema.extend('$listItem', { allowAttributes: 'todoListChecked' });
|
|
53
|
+
model.schema.addAttributeCheck((context, attributeName) => {
|
|
54
|
+
const item = context.last;
|
|
55
|
+
if (attributeName != 'todoListChecked') {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!item.getAttribute('listItemId') || item.getAttribute('listType') != 'todo') {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
editor.conversion.for('upcast').add(dispatcher => {
|
|
63
|
+
// Upcast of to-do list item is based on a checkbox at the beginning of a <li> to keep compatibility with markdown input.
|
|
64
|
+
dispatcher.on('element:input', todoItemInputConverter());
|
|
65
|
+
// Consume other elements that are normally generated in data downcast, so they won't get captured by GHS.
|
|
66
|
+
dispatcher.on('element:label', elementUpcastConsumingConverter({ name: 'label', classes: 'todo-list__label' }));
|
|
67
|
+
dispatcher.on('element:label', elementUpcastConsumingConverter({ name: 'label', classes: ['todo-list__label', 'todo-list__label_without-description'] }));
|
|
68
|
+
dispatcher.on('element:span', elementUpcastConsumingConverter({ name: 'span', classes: 'todo-list__label__description' }));
|
|
69
|
+
dispatcher.on('element:ul', attributeUpcastConsumingConverter({ name: 'ul', classes: 'todo-list' }));
|
|
70
|
+
});
|
|
71
|
+
editor.conversion.for('downcast').elementToElement({
|
|
72
|
+
model: elementName,
|
|
73
|
+
view: (element, { writer }) => {
|
|
74
|
+
if (isDescriptionBlock(element, documentListEditing.getListAttributeNames())) {
|
|
75
|
+
return writer.createContainerElement('span', { class: 'todo-list__label__description' });
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
converterPriority: 'highest'
|
|
79
|
+
});
|
|
80
|
+
documentListEditing.registerDowncastStrategy({
|
|
81
|
+
scope: 'list',
|
|
82
|
+
attributeName: 'listType',
|
|
83
|
+
setAttributeOnDowncast(writer, value, element) {
|
|
84
|
+
if (value == 'todo') {
|
|
85
|
+
writer.addClass('todo-list', element);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
writer.removeClass('todo-list', element);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
documentListEditing.registerDowncastStrategy({
|
|
93
|
+
scope: 'itemMarker',
|
|
94
|
+
attributeName: 'todoListChecked',
|
|
95
|
+
createElement(writer, modelElement, { dataPipeline }) {
|
|
96
|
+
if (modelElement.getAttribute('listType') != 'todo') {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const viewElement = writer.createEmptyElement('input', {
|
|
100
|
+
type: 'checkbox',
|
|
101
|
+
...(modelElement.getAttribute('todoListChecked') ?
|
|
102
|
+
{ checked: 'checked' } :
|
|
103
|
+
null),
|
|
104
|
+
...(dataPipeline ?
|
|
105
|
+
{ disabled: 'disabled' } :
|
|
106
|
+
{ tabindex: '-1' })
|
|
107
|
+
});
|
|
108
|
+
if (dataPipeline) {
|
|
109
|
+
return viewElement;
|
|
110
|
+
}
|
|
111
|
+
return writer.createContainerElement('span', { contenteditable: 'false' }, viewElement);
|
|
112
|
+
},
|
|
113
|
+
canWrapElement(modelElement) {
|
|
114
|
+
return isDescriptionBlock(modelElement, documentListEditing.getListAttributeNames());
|
|
115
|
+
},
|
|
116
|
+
createWrapperElement(writer, modelElement, { dataPipeline }) {
|
|
117
|
+
const classes = ['todo-list__label'];
|
|
118
|
+
if (!isDescriptionBlock(modelElement, documentListEditing.getListAttributeNames())) {
|
|
119
|
+
classes.push('todo-list__label_without-description');
|
|
120
|
+
}
|
|
121
|
+
return writer.createAttributeElement(dataPipeline ? 'label' : 'span', {
|
|
122
|
+
class: classes.join(' ')
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// We need to register the model length callback for the view checkbox input because it has no mapped model element.
|
|
127
|
+
// The to-do list item checkbox does not use the UIElement because it would be trimmed by ViewRange#getTrimmed()
|
|
128
|
+
// and removing the default remove converter would not include checkbox in the range to remove.
|
|
129
|
+
editing.mapper.registerViewToModelLength('input', viewElement => {
|
|
130
|
+
if (viewElement.getAttribute('type') == 'checkbox' &&
|
|
131
|
+
viewElement.findAncestor({ classes: 'todo-list__label' })) {
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
return editing.mapper.toModelElement(viewElement) ? 1 : 0;
|
|
135
|
+
});
|
|
136
|
+
// Verifies if a to-do list block requires reconversion of a first item downcasted as an item description.
|
|
137
|
+
documentListEditing.on('checkElement', (evt, { modelElement, viewElement }) => {
|
|
138
|
+
const isFirstTodoModelParagraphBlock = isDescriptionBlock(modelElement, documentListEditing.getListAttributeNames());
|
|
139
|
+
const hasViewClass = viewElement.hasClass('todo-list__label__description');
|
|
140
|
+
if (hasViewClass != isFirstTodoModelParagraphBlock) {
|
|
141
|
+
evt.return = true;
|
|
142
|
+
evt.stop();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// Verifies if a to-do list block requires reconversion of a checkbox element
|
|
146
|
+
// (for example there is a new paragraph inserted as a first block of a list item).
|
|
147
|
+
documentListEditing.on('checkElement', (evt, { modelElement, viewElement }) => {
|
|
148
|
+
const isFirstTodoModelItemBlock = modelElement.getAttribute('listType') == 'todo' && isFirstBlockOfListItem(modelElement);
|
|
149
|
+
let hasViewItemMarker = false;
|
|
150
|
+
const viewWalker = editor.editing.view.createPositionBefore(viewElement).getWalker({ direction: 'backward' });
|
|
151
|
+
for (const { item } of viewWalker) {
|
|
152
|
+
if (item.is('element') && editor.editing.mapper.toModelElement(item)) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (item.is('element', 'input') && item.getAttribute('type') == 'checkbox') {
|
|
156
|
+
hasViewItemMarker = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (hasViewItemMarker != isFirstTodoModelItemBlock) {
|
|
160
|
+
evt.return = true;
|
|
161
|
+
evt.stop();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// Make sure that all blocks of the same list item have the same todoListChecked attribute.
|
|
165
|
+
documentListEditing.on('postFixer', (evt, { listNodes, writer }) => {
|
|
166
|
+
for (const { node, previousNodeInList } of listNodes) {
|
|
167
|
+
// This is a first item of a nested list.
|
|
168
|
+
if (!previousNodeInList) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (previousNodeInList.getAttribute('listItemId') != node.getAttribute('listItemId')) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const previousHasAttribute = previousNodeInList.hasAttribute('todoListChecked');
|
|
175
|
+
const nodeHasAttribute = node.hasAttribute('todoListChecked');
|
|
176
|
+
if (nodeHasAttribute && !previousHasAttribute) {
|
|
177
|
+
writer.removeAttribute('todoListChecked', node);
|
|
178
|
+
evt.return = true;
|
|
179
|
+
}
|
|
180
|
+
else if (!nodeHasAttribute && previousHasAttribute) {
|
|
181
|
+
writer.setAttribute('todoListChecked', true, node);
|
|
182
|
+
evt.return = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
// Make sure that todoListChecked attribute is only present for to-do list items.
|
|
187
|
+
model.document.registerPostFixer(writer => {
|
|
188
|
+
const changes = model.document.differ.getChanges();
|
|
189
|
+
let wasFixed = false;
|
|
190
|
+
for (const change of changes) {
|
|
191
|
+
if (change.type == 'attribute' && change.attributeKey == 'listType') {
|
|
192
|
+
const element = change.range.start.nodeAfter;
|
|
193
|
+
if (change.attributeOldValue == 'todo' && element.hasAttribute('todoListChecked')) {
|
|
194
|
+
writer.removeAttribute('todoListChecked', element);
|
|
195
|
+
wasFixed = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (change.type == 'insert' && change.name != '$text') {
|
|
199
|
+
for (const { item } of writer.createRangeOn(change.position.nodeAfter)) {
|
|
200
|
+
if (item.is('element') && item.getAttribute('listType') != 'todo' && item.hasAttribute('todoListChecked')) {
|
|
201
|
+
writer.removeAttribute('todoListChecked', item);
|
|
202
|
+
wasFixed = true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return wasFixed;
|
|
208
|
+
});
|
|
209
|
+
// Toggle check state of selected to-do list items on keystroke.
|
|
210
|
+
this.listenTo(editing.view.document, 'keydown', (evt, data) => {
|
|
211
|
+
if (getCode(data) === ITEM_TOGGLE_KEYSTROKE) {
|
|
212
|
+
editor.execute('checkTodoList');
|
|
213
|
+
evt.stop();
|
|
214
|
+
}
|
|
215
|
+
}, { priority: 'high' });
|
|
216
|
+
// Toggle check state of a to-do list item clicked on the checkbox.
|
|
217
|
+
this.listenTo(editing.view.document, 'todoCheckboxChange', (evt, data) => {
|
|
218
|
+
const viewTarget = data.target;
|
|
219
|
+
if (!viewTarget || !viewTarget.is('element', 'input')) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const viewPositionAfter = editing.view.createPositionAfter(viewTarget);
|
|
223
|
+
const modelPositionAfter = editing.mapper.toModelPosition(viewPositionAfter);
|
|
224
|
+
const modelElement = modelPositionAfter.parent;
|
|
225
|
+
if (modelElement && isListItemBlock(modelElement) && modelElement.getAttribute('listType') == 'todo') {
|
|
226
|
+
this._handleCheckmarkChange(modelElement);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// Jump at the start/end of the next node on right arrow key press, when selection is before the checkbox.
|
|
230
|
+
//
|
|
231
|
+
// <blockquote><p>Foo{}</p></blockquote>
|
|
232
|
+
// <ul><li><checkbox/>Bar</li></ul>
|
|
233
|
+
//
|
|
234
|
+
// press: `->`
|
|
235
|
+
//
|
|
236
|
+
// <blockquote><p>Foo</p></blockquote>
|
|
237
|
+
// <ul><li><checkbox/>{}Bar</li></ul>
|
|
238
|
+
//
|
|
239
|
+
this.listenTo(editing.view.document, 'arrowKey', jumpOverCheckmarkOnSideArrowKeyPress(model, editor.locale), { context: '$text' });
|
|
240
|
+
// Map view positions inside the checkbox and wrappers to the position in the first block of the list item.
|
|
241
|
+
this.listenTo(editing.mapper, 'viewToModelPosition', (evt, data) => {
|
|
242
|
+
const viewParent = data.viewPosition.parent;
|
|
243
|
+
const isStartOfListItem = viewParent.is('attributeElement', 'li') && data.viewPosition.offset == 0;
|
|
244
|
+
const isStartOfListLabel = isLabelElement(viewParent) && data.viewPosition.offset <= 1;
|
|
245
|
+
const isInInputWrapper = viewParent.is('element', 'span') &&
|
|
246
|
+
viewParent.getAttribute('contenteditable') == 'false' &&
|
|
247
|
+
isLabelElement(viewParent.parent);
|
|
248
|
+
if (!isStartOfListItem && !isStartOfListLabel && !isInInputWrapper) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const nodeAfter = data.modelPosition.nodeAfter;
|
|
252
|
+
if (nodeAfter && nodeAfter.getAttribute('listType') == 'todo') {
|
|
253
|
+
data.modelPosition = model.createPositionAt(nodeAfter, 0);
|
|
254
|
+
}
|
|
255
|
+
}, { priority: 'low' });
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Handles the checkbox element change, moves the selection to the corresponding model item to make it possible
|
|
259
|
+
* to toggle the `todoListChecked` attribute using the command, and restores the selection position.
|
|
260
|
+
*
|
|
261
|
+
* Some say it's a hack :) Moving the selection only for executing the command on a certain node and restoring it after,
|
|
262
|
+
* is not a clear solution. We need to design an API for using commands beyond the selection range.
|
|
263
|
+
* See https://github.com/ckeditor/ckeditor5/issues/1954.
|
|
264
|
+
*/
|
|
265
|
+
_handleCheckmarkChange(listItem) {
|
|
266
|
+
const editor = this.editor;
|
|
267
|
+
const model = editor.model;
|
|
268
|
+
const previousSelectionRanges = Array.from(model.document.selection.getRanges());
|
|
269
|
+
model.change(writer => {
|
|
270
|
+
writer.setSelection(listItem, 'end');
|
|
271
|
+
editor.execute('checkTodoList');
|
|
272
|
+
writer.setSelection(previousSelectionRanges);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Returns an upcast converter that detects a to-do list checkbox and marks the list item as a to-do list.
|
|
278
|
+
*/
|
|
279
|
+
function todoItemInputConverter() {
|
|
280
|
+
return (evt, data, conversionApi) => {
|
|
281
|
+
const modelCursor = data.modelCursor;
|
|
282
|
+
const modelItem = modelCursor.parent;
|
|
283
|
+
const viewItem = data.viewItem;
|
|
284
|
+
if (!conversionApi.consumable.test(viewItem, { name: true })) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (viewItem.getAttribute('type') != 'checkbox' || !modelCursor.isAtStart || !modelItem.hasAttribute('listType')) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
conversionApi.consumable.consume(viewItem, { name: true });
|
|
291
|
+
const writer = conversionApi.writer;
|
|
292
|
+
writer.setAttribute('listType', 'todo', modelItem);
|
|
293
|
+
if (data.viewItem.hasAttribute('checked')) {
|
|
294
|
+
writer.setAttribute('todoListChecked', true, modelItem);
|
|
295
|
+
}
|
|
296
|
+
data.modelRange = writer.createRange(modelCursor);
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Returns an upcast converter that consumes element matching the given matcher pattern.
|
|
301
|
+
*/
|
|
302
|
+
function elementUpcastConsumingConverter(matcherPattern) {
|
|
303
|
+
const matcher = new Matcher(matcherPattern);
|
|
304
|
+
return (evt, data, conversionApi) => {
|
|
305
|
+
const matcherResult = matcher.match(data.viewItem);
|
|
306
|
+
if (!matcherResult) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!conversionApi.consumable.consume(data.viewItem, matcherResult.match)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
Object.assign(data, conversionApi.convertChildren(data.viewItem, data.modelCursor));
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Returns an upcast converter that consumes attributes matching the given matcher pattern.
|
|
317
|
+
*/
|
|
318
|
+
function attributeUpcastConsumingConverter(matcherPattern) {
|
|
319
|
+
const matcher = new Matcher(matcherPattern);
|
|
320
|
+
return (evt, data, conversionApi) => {
|
|
321
|
+
const matcherResult = matcher.match(data.viewItem);
|
|
322
|
+
if (!matcherResult) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const match = matcherResult.match;
|
|
326
|
+
match.name = false;
|
|
327
|
+
conversionApi.consumable.consume(data.viewItem, match);
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Returns true if the given list item block should be converted as a description block of a to-do list item.
|
|
332
|
+
*/
|
|
333
|
+
function isDescriptionBlock(modelElement, listAttributeNames) {
|
|
334
|
+
return (modelElement.is('element', 'paragraph') || modelElement.is('element', 'listItem')) &&
|
|
335
|
+
modelElement.getAttribute('listType') == 'todo' &&
|
|
336
|
+
isFirstBlockOfListItem(modelElement) &&
|
|
337
|
+
hasOnlyListAttributes(modelElement, listAttributeNames);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Returns true if only attributes from the given list are present on the model element.
|
|
341
|
+
*/
|
|
342
|
+
function hasOnlyListAttributes(modelElement, attributeNames) {
|
|
343
|
+
for (const attributeKey of modelElement.getAttributeKeys()) {
|
|
344
|
+
// Ignore selection attributes stored on block elements.
|
|
345
|
+
if (attributeKey.startsWith('selection:')) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (!attributeNames.includes(attributeKey)) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Jump at the start and end of a to-do list item.
|
|
356
|
+
*/
|
|
357
|
+
function jumpOverCheckmarkOnSideArrowKeyPress(model, locale) {
|
|
358
|
+
return (eventInfo, domEventData) => {
|
|
359
|
+
const direction = getLocalizedArrowKeyCodeDirection(domEventData.keyCode, locale.contentLanguageDirection);
|
|
360
|
+
const schema = model.schema;
|
|
361
|
+
const selection = model.document.selection;
|
|
362
|
+
if (!selection.isCollapsed) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const position = selection.getFirstPosition();
|
|
366
|
+
const parent = position.parent;
|
|
367
|
+
// Right arrow before a to-do list item.
|
|
368
|
+
if (direction == 'right' && position.isAtEnd) {
|
|
369
|
+
const newRange = schema.getNearestSelectionRange(model.createPositionAfter(parent), 'forward');
|
|
370
|
+
if (!newRange) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const newRangeParent = newRange.start.parent;
|
|
374
|
+
if (newRangeParent && isListItemBlock(newRangeParent) && newRangeParent.getAttribute('listType') == 'todo') {
|
|
375
|
+
model.change(writer => writer.setSelection(newRange));
|
|
376
|
+
domEventData.preventDefault();
|
|
377
|
+
domEventData.stopPropagation();
|
|
378
|
+
eventInfo.stop();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Left arrow at the beginning of a to-do list item.
|
|
382
|
+
else if (direction == 'left' && position.isAtStart && isListItemBlock(parent) && parent.getAttribute('listType') == 'todo') {
|
|
383
|
+
const newRange = schema.getNearestSelectionRange(model.createPositionBefore(parent), 'backward');
|
|
384
|
+
if (!newRange) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
model.change(writer => writer.setSelection(newRange));
|
|
388
|
+
domEventData.preventDefault();
|
|
389
|
+
domEventData.stopPropagation();
|
|
390
|
+
eventInfo.stop();
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Returns true if the given element is a label element of a to-do list item.
|
|
396
|
+
*/
|
|
397
|
+
function isLabelElement(viewElement) {
|
|
398
|
+
return !!viewElement && viewElement.is('attributeElement') && viewElement.hasClass('todo-list__label');
|
|
399
|
+
}
|