UNPKG

12.7 kBJavaScriptView Raw
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/documentlistproperties/documentlistpropertiesediting
7 */
8import { Plugin } from 'ckeditor5/src/core';
9import DocumentListEditing from '../documentlist/documentlistediting';
10import DocumentListStartCommand from './documentliststartcommand';
11import DocumentListStyleCommand from './documentliststylecommand';
12import DocumentListReversedCommand from './documentlistreversedcommand';
13import { listPropertiesUpcastConverter } from './converters';
14import { getAllSupportedStyleTypes, getListTypeFromListStyleType, getListStyleTypeFromTypeAttribute, getTypeAttributeFromListStyleType } from './utils/style';
15import DocumentListPropertiesUtils from './documentlistpropertiesutils';
16const DEFAULT_LIST_TYPE = 'default';
17/**
18 * The document list properties engine feature.
19 *
20 * It registers the `'listStyle'`, `'listReversed'` and `'listStart'` commands if they are enabled in the configuration.
21 * Read more in {@link module:list/listconfig~ListPropertiesConfig}.
22 */
23export default class DocumentListPropertiesEditing extends Plugin {
24 /**
25 * @inheritDoc
26 */
27 static get requires() {
28 return [DocumentListEditing, DocumentListPropertiesUtils];
29 }
30 /**
31 * @inheritDoc
32 */
33 static get pluginName() {
34 return 'DocumentListPropertiesEditing';
35 }
36 /**
37 * @inheritDoc
38 */
39 constructor(editor) {
40 super(editor);
41 editor.config.define('list', {
42 properties: {
43 styles: true,
44 startIndex: false,
45 reversed: false
46 }
47 });
48 }
49 /**
50 * @inheritDoc
51 */
52 init() {
53 const editor = this.editor;
54 const model = editor.model;
55 const documentListEditing = editor.plugins.get(DocumentListEditing);
56 const enabledProperties = editor.config.get('list.properties');
57 const strategies = createAttributeStrategies(enabledProperties);
58 for (const strategy of strategies) {
59 strategy.addCommand(editor);
60 model.schema.extend('$container', { allowAttributes: strategy.attributeName });
61 model.schema.extend('$block', { allowAttributes: strategy.attributeName });
62 model.schema.extend('$blockObject', { allowAttributes: strategy.attributeName });
63 // Register downcast strategy.
64 documentListEditing.registerDowncastStrategy({
65 scope: 'list',
66 attributeName: strategy.attributeName,
67 setAttributeOnDowncast(writer, attributeValue, viewElement) {
68 strategy.setAttributeOnDowncast(writer, attributeValue, viewElement);
69 }
70 });
71 }
72 // Set up conversion.
73 editor.conversion.for('upcast').add(dispatcher => {
74 for (const strategy of strategies) {
75 dispatcher.on('element:ol', listPropertiesUpcastConverter(strategy));
76 dispatcher.on('element:ul', listPropertiesUpcastConverter(strategy));
77 }
78 });
79 // Verify if the list view element (ul or ol) requires refreshing.
80 documentListEditing.on('checkAttributes:list', (evt, { viewElement, modelAttributes }) => {
81 for (const strategy of strategies) {
82 if (strategy.getAttributeOnUpcast(viewElement) != modelAttributes[strategy.attributeName]) {
83 evt.return = true;
84 evt.stop();
85 }
86 }
87 });
88 // Reset list properties after indenting list items.
89 this.listenTo(editor.commands.get('indentList'), 'afterExecute', (evt, changedBlocks) => {
90 model.change(writer => {
91 for (const node of changedBlocks) {
92 for (const strategy of strategies) {
93 if (strategy.appliesToListItem(node)) {
94 // Just reset the attribute.
95 // If there is a previous indented list that this node should be merged into,
96 // the postfixer will unify all the attributes of both sub-lists.
97 writer.setAttribute(strategy.attributeName, strategy.defaultValue, node);
98 }
99 }
100 }
101 });
102 });
103 // Add or remove list properties attributes depending on the list type.
104 documentListEditing.on('postFixer', (evt, { listNodes, writer }) => {
105 for (const { node } of listNodes) {
106 for (const strategy of strategies) {
107 // Check if attribute is valid.
108 if (strategy.hasValidAttribute(node)) {
109 continue;
110 }
111 // Add missing default property attributes...
112 if (strategy.appliesToListItem(node)) {
113 writer.setAttribute(strategy.attributeName, strategy.defaultValue, node);
114 }
115 // ...or remove invalid property attributes.
116 else {
117 writer.removeAttribute(strategy.attributeName, node);
118 }
119 evt.return = true;
120 }
121 }
122 });
123 // Make sure that all items in a single list (items at the same level & listType) have the same properties.
124 documentListEditing.on('postFixer', (evt, { listNodes, writer }) => {
125 const previousNodesByIndent = []; // Last seen nodes of lower indented lists.
126 for (const { node, previous } of listNodes) {
127 // For the first list block there is nothing to compare with.
128 if (!previous) {
129 continue;
130 }
131 const nodeIndent = node.getAttribute('listIndent');
132 const previousNodeIndent = previous.getAttribute('listIndent');
133 let previousNodeInList = null; // It's like `previous` but has the same indent as current node.
134 // Let's find previous node for the same indent.
135 // We're going to need that when we get back to previous indent.
136 if (nodeIndent > previousNodeIndent) {
137 previousNodesByIndent[previousNodeIndent] = previous;
138 }
139 // Restore the one for given indent.
140 else if (nodeIndent < previousNodeIndent) {
141 previousNodeInList = previousNodesByIndent[nodeIndent];
142 previousNodesByIndent.length = nodeIndent;
143 }
144 // Same indent.
145 else {
146 previousNodeInList = previous;
147 }
148 // This is a first item of a nested list.
149 if (!previousNodeInList) {
150 continue;
151 }
152 // This is a first block of a list of a different type.
153 if (previousNodeInList.getAttribute('listType') != node.getAttribute('listType')) {
154 continue;
155 }
156 // Copy properties from the previous one.
157 for (const strategy of strategies) {
158 const { attributeName } = strategy;
159 if (!strategy.appliesToListItem(node)) {
160 continue;
161 }
162 const value = previousNodeInList.getAttribute(attributeName);
163 if (node.getAttribute(attributeName) != value) {
164 writer.setAttribute(attributeName, value, node);
165 evt.return = true;
166 }
167 }
168 }
169 });
170 }
171}
172/**
173 * Creates an array of strategies for dealing with enabled listItem attributes.
174 */
175function createAttributeStrategies(enabledProperties) {
176 const strategies = [];
177 if (enabledProperties.styles) {
178 const useAttribute = typeof enabledProperties.styles == 'object' && enabledProperties.styles.useAttribute;
179 strategies.push({
180 attributeName: 'listStyle',
181 defaultValue: DEFAULT_LIST_TYPE,
182 viewConsumables: { styles: 'list-style-type' },
183 addCommand(editor) {
184 let supportedTypes = getAllSupportedStyleTypes();
185 if (useAttribute) {
186 supportedTypes = supportedTypes.filter(styleType => !!getTypeAttributeFromListStyleType(styleType));
187 }
188 editor.commands.add('listStyle', new DocumentListStyleCommand(editor, DEFAULT_LIST_TYPE, supportedTypes));
189 },
190 appliesToListItem() {
191 return true;
192 },
193 hasValidAttribute(item) {
194 if (!item.hasAttribute('listStyle')) {
195 return false;
196 }
197 const value = item.getAttribute('listStyle');
198 if (value == DEFAULT_LIST_TYPE) {
199 return true;
200 }
201 return getListTypeFromListStyleType(value) == item.getAttribute('listType');
202 },
203 setAttributeOnDowncast(writer, listStyle, element) {
204 if (listStyle && listStyle !== DEFAULT_LIST_TYPE) {
205 if (useAttribute) {
206 const value = getTypeAttributeFromListStyleType(listStyle);
207 if (value) {
208 writer.setAttribute('type', value, element);
209 return;
210 }
211 }
212 else {
213 writer.setStyle('list-style-type', listStyle, element);
214 return;
215 }
216 }
217 writer.removeStyle('list-style-type', element);
218 writer.removeAttribute('type', element);
219 },
220 getAttributeOnUpcast(listParent) {
221 const style = listParent.getStyle('list-style-type');
222 if (style) {
223 return style;
224 }
225 const attribute = listParent.getAttribute('type');
226 if (attribute) {
227 return getListStyleTypeFromTypeAttribute(attribute);
228 }
229 return DEFAULT_LIST_TYPE;
230 }
231 });
232 }
233 if (enabledProperties.reversed) {
234 strategies.push({
235 attributeName: 'listReversed',
236 defaultValue: false,
237 viewConsumables: { attributes: 'reversed' },
238 addCommand(editor) {
239 editor.commands.add('listReversed', new DocumentListReversedCommand(editor));
240 },
241 appliesToListItem(item) {
242 return item.getAttribute('listType') == 'numbered';
243 },
244 hasValidAttribute(item) {
245 return this.appliesToListItem(item) == item.hasAttribute('listReversed');
246 },
247 setAttributeOnDowncast(writer, listReversed, element) {
248 if (listReversed) {
249 writer.setAttribute('reversed', 'reversed', element);
250 }
251 else {
252 writer.removeAttribute('reversed', element);
253 }
254 },
255 getAttributeOnUpcast(listParent) {
256 return listParent.hasAttribute('reversed');
257 }
258 });
259 }
260 if (enabledProperties.startIndex) {
261 strategies.push({
262 attributeName: 'listStart',
263 defaultValue: 1,
264 viewConsumables: { attributes: 'start' },
265 addCommand(editor) {
266 editor.commands.add('listStart', new DocumentListStartCommand(editor));
267 },
268 appliesToListItem(item) {
269 return item.getAttribute('listType') == 'numbered';
270 },
271 hasValidAttribute(item) {
272 return this.appliesToListItem(item) == item.hasAttribute('listStart');
273 },
274 setAttributeOnDowncast(writer, listStart, element) {
275 if (listStart == 0 || listStart > 1) {
276 writer.setAttribute('start', listStart, element);
277 }
278 else {
279 writer.removeAttribute('start', element);
280 }
281 },
282 getAttributeOnUpcast(listParent) {
283 const startAttributeValue = listParent.getAttribute('start');
284 return startAttributeValue >= 0 ? startAttributeValue : 1;
285 }
286 });
287 }
288 return strategies;
289}