@ckeditor/ckeditor5-html-support 38.0.1 → 38.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/src/datafilter.js CHANGED
@@ -11,6 +11,7 @@ import { CKEditorError, priorities, isValidAttributeName } from 'ckeditor5/src/u
11
11
  import { Widget } from 'ckeditor5/src/widget';
12
12
  import { viewToModelObjectConverter, toObjectWidgetConverter, createObjectView, viewToAttributeInlineConverter, attributeToViewInlineConverter, viewToModelBlockAttributeConverter, modelToViewBlockAttributeConverter } from './converters';
13
13
  import { default as DataSchema } from './dataschema';
14
+ import { getHtmlAttributeName } from './utils';
14
15
  import { isPlainObject, pull as removeItemFromArray } from 'lodash-es';
15
16
  import '../theme/datafilter.css';
16
17
  /**
@@ -57,7 +58,8 @@ export default class DataFilter extends Plugin {
57
58
  this._coupledAttributes = null;
58
59
  this._registerElementsAfterInit();
59
60
  this._registerElementHandlers();
60
- this._registerModelPostFixer();
61
+ this._registerCoupledAttributesPostFixer();
62
+ this._registerAssociatedHtmlAttributesPostFixer();
61
63
  }
62
64
  /**
63
65
  * @inheritDoc
@@ -215,7 +217,7 @@ export default class DataFilter extends Plugin {
215
217
  }, {
216
218
  // With the highest priority listener we are able to register elements right before
217
219
  // running data conversion.
218
- priority: priorities.get('highest') + 1
220
+ priority: priorities.highest + 1
219
221
  });
220
222
  }
221
223
  }
@@ -237,7 +239,7 @@ export default class DataFilter extends Plugin {
237
239
  // * Make sure no other features hook into this event before GHS because otherwise the
238
240
  // downcast conversion (for these features) could run before GHS registered its converters
239
241
  // (https://github.com/ckeditor/ckeditor5/issues/11356).
240
- priority: priorities.get('highest') + 1
242
+ priority: priorities.highest + 1
241
243
  });
242
244
  }
243
245
  /**
@@ -296,7 +298,7 @@ export default class DataFilter extends Plugin {
296
298
  * The `htmlA` attribute would stay in the model and would cause GHS to generate an `<a>` element.
297
299
  * This is incorrect from UX point of view, as the user wanted to remove the whole link (not only `href`).
298
300
  */
299
- _registerModelPostFixer() {
301
+ _registerCoupledAttributesPostFixer() {
300
302
  const model = this.editor.model;
301
303
  model.document.registerPostFixer(writer => {
302
304
  const changes = model.document.differ.getChanges();
@@ -325,6 +327,57 @@ export default class DataFilter extends Plugin {
325
327
  return changed;
326
328
  });
327
329
  }
330
+ /**
331
+ * Removes `html*Attributes` attributes from incompatible elements.
332
+ *
333
+ * For example, consider the following HTML:
334
+ *
335
+ * ```html
336
+ * <heading2 htmlH2Attributes="...">foobar[]</heading2>
337
+ * ```
338
+ *
339
+ * Pressing `enter` creates a new `paragraph` element that inherits
340
+ * the `htmlH2Attributes` attribute from `heading2`.
341
+ *
342
+ * ```html
343
+ * <heading2 htmlH2Attributes="...">foobar</heading2>
344
+ * <paragraph htmlH2Attributes="...">[]</paragraph>
345
+ * ```
346
+ *
347
+ * This postfixer ensures that this doesn't happen, and that elements can
348
+ * only have `html*Attributes` associated with them,
349
+ * e.g.: `htmlPAttributes` for `<p>`, `htmlDivAttributes` for `<div>`, etc.
350
+ *
351
+ * With it enabled, pressing `enter` at the end of `<heading2>` will create
352
+ * a new paragraph without the `htmlH2Attributes` attribute.
353
+ *
354
+ * ```html
355
+ * <heading2 htmlH2Attributes="...">foobar</heading2>
356
+ * <paragraph>[]</paragraph>
357
+ * ```
358
+ */
359
+ _registerAssociatedHtmlAttributesPostFixer() {
360
+ const model = this.editor.model;
361
+ model.document.registerPostFixer(writer => {
362
+ const changes = model.document.differ.getChanges();
363
+ let changed = false;
364
+ for (const change of changes) {
365
+ if (change.type !== 'insert' || change.name === '$text') {
366
+ continue;
367
+ }
368
+ for (const attr of change.attributes.keys()) {
369
+ if (!attr.startsWith('html') || !attr.endsWith('Attributes')) {
370
+ continue;
371
+ }
372
+ if (!model.schema.checkAttribute(change.name, attr)) {
373
+ writer.removeAttribute(attr, change.position.nodeAfter);
374
+ changed = true;
375
+ }
376
+ }
377
+ }
378
+ return changed;
379
+ });
380
+ }
328
381
  /**
329
382
  * Collects the map of coupled attributes. The returned map is keyed by the feature attribute name
330
383
  * and coupled GHS attribute names are stored in the value array.
@@ -370,7 +423,7 @@ export default class DataFilter extends Plugin {
370
423
  return;
371
424
  }
372
425
  schema.extend(definition.model, {
373
- allowAttributes: ['htmlAttributes', 'htmlContent']
426
+ allowAttributes: [getHtmlAttributeName(viewName), 'htmlContent']
374
427
  });
375
428
  // Store element content in special `$rawContent` custom property to
376
429
  // avoid editor's data filtering mechanism.
@@ -382,15 +435,14 @@ export default class DataFilter extends Plugin {
382
435
  model: viewToModelObjectConverter(definition),
383
436
  // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure
384
437
  // this listener is called before it. If not, some elements will be transformed into a paragraph.
385
- converterPriority: priorities.get('low') + 1
438
+ // `+ 2` is used to take priority over `_addDefaultH1Conversion` in the Heading plugin.
439
+ converterPriority: priorities.low + 2
386
440
  });
387
441
  conversion.for('upcast').add(viewToModelBlockAttributeConverter(definition, this));
388
442
  conversion.for('editingDowncast').elementToStructure({
389
443
  model: {
390
444
  name: modelName,
391
- attributes: [
392
- 'htmlAttributes'
393
- ]
445
+ attributes: [getHtmlAttributeName(viewName)]
394
446
  },
395
447
  view: toObjectWidgetConverter(editor, definition)
396
448
  });
@@ -420,7 +472,8 @@ export default class DataFilter extends Plugin {
420
472
  view: viewName,
421
473
  // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure
422
474
  // this listener is called before it. If not, some elements will be transformed into a paragraph.
423
- converterPriority: priorities.get('low') + 1
475
+ // `+ 2` is used to take priority over `_addDefaultH1Conversion` in the Heading plugin.
476
+ converterPriority: priorities.low + 2
424
477
  });
425
478
  conversion.for('downcast').elementToElement({
426
479
  model: modelName,
@@ -431,7 +484,7 @@ export default class DataFilter extends Plugin {
431
484
  return;
432
485
  }
433
486
  schema.extend(definition.model, {
434
- allowAttributes: 'htmlAttributes'
487
+ allowAttributes: getHtmlAttributeName(viewName)
435
488
  });
436
489
  conversion.for('upcast').add(viewToModelBlockAttributeConverter(definition, this));
437
490
  conversion.for('downcast').add(modelToViewBlockAttributeConverter(definition));
@@ -47,7 +47,7 @@ export default class DataSchema extends Plugin {
47
47
  /**
48
48
  * @inheritDoc
49
49
  */
50
- static get pluginName(): 'DataSchema';
50
+ static get pluginName(): "DataSchema";
51
51
  /**
52
52
  * @inheritDoc
53
53
  */
@@ -166,7 +166,7 @@ export interface DataSchemaInlineElementDefinition extends DataSchemaDefinition
166
166
  /**
167
167
  * The name of the model attribute that generates the same view element. GHS inline attribute
168
168
  * will be removed from the model tree as soon as the coupled attribute is removed. See
169
- * {@link module:html-support/datafilter~DataFilter#_registerModelPostFixer GHS post-fixer} for more details.
169
+ * {@link module:html-support/datafilter~DataFilter#_registerCoupledAttributesPostFixer GHS post-fixer} for more details.
170
170
  */
171
171
  coupledAttribute?: string;
172
172
  /**
package/src/fullpage.d.ts CHANGED
@@ -13,7 +13,7 @@ export default class FullPage extends Plugin {
13
13
  /**
14
14
  * @inheritDoc
15
15
  */
16
- static get pluginName(): 'FullPage';
16
+ static get pluginName(): "FullPage";
17
17
  /**
18
18
  * @inheritDoc
19
19
  */
@@ -29,7 +29,7 @@ export default class GeneralHtmlSupport extends Plugin {
29
29
  /**
30
30
  * @inheritDoc
31
31
  */
32
- static get pluginName(): 'GeneralHtmlSupport';
32
+ static get pluginName(): "GeneralHtmlSupport";
33
33
  /**
34
34
  * @inheritDoc
35
35
  */
@@ -18,7 +18,7 @@ import TableElementSupport from './integrations/table';
18
18
  import StyleElementSupport from './integrations/style';
19
19
  import DocumentListElementSupport from './integrations/documentlist';
20
20
  import CustomElementSupport from './integrations/customelement';
21
- import { modifyGhsAttribute } from './utils';
21
+ import { getHtmlAttributeName, modifyGhsAttribute } from './utils';
22
22
  /**
23
23
  * The General HTML Support feature.
24
24
  *
@@ -73,7 +73,7 @@ export default class GeneralHtmlSupport extends Plugin {
73
73
  if (inlineDefinition) {
74
74
  return inlineDefinition.model;
75
75
  }
76
- return 'htmlAttributes';
76
+ return getHtmlAttributeName(viewElementName);
77
77
  }
78
78
  /**
79
79
  * Updates GHS model attribute for a specified view element name, so it includes the given class name.
@@ -16,7 +16,7 @@ export default class HtmlComment extends Plugin {
16
16
  /**
17
17
  * @inheritDoc
18
18
  */
19
- static get pluginName(): 'HtmlComment';
19
+ static get pluginName(): "HtmlComment";
20
20
  /**
21
21
  * @inheritDoc
22
22
  */
@@ -21,6 +21,7 @@ export default class HtmlComment extends Plugin {
21
21
  */
22
22
  init() {
23
23
  const editor = this.editor;
24
+ const loadedCommentsContent = new Map();
24
25
  editor.data.processor.skipComments = false;
25
26
  // Allow storing comment's content as the $root attribute with the name `$comment:<unique id>`.
26
27
  editor.model.schema.addAttributeCheck((context, attributeName) => {
@@ -32,11 +33,11 @@ export default class HtmlComment extends Plugin {
32
33
  // attribute. The comment content is needed in the `dataDowncast` pipeline to re-create the comment node.
33
34
  editor.conversion.for('upcast').elementToMarker({
34
35
  view: '$comment',
35
- model: (viewElement, { writer }) => {
36
- const root = this.editor.model.document.getRoot();
36
+ model: viewElement => {
37
+ const markerUid = uid();
38
+ const markerName = `$comment:${markerUid}`;
37
39
  const commentContent = viewElement.getCustomProperty('$rawContent');
38
- const markerName = `$comment:${uid()}`;
39
- writer.setAttribute(markerName, commentContent, root);
40
+ loadedCommentsContent.set(markerName, commentContent);
40
41
  return markerName;
41
42
  }
42
43
  });
@@ -44,7 +45,13 @@ export default class HtmlComment extends Plugin {
44
45
  editor.conversion.for('dataDowncast').markerToElement({
45
46
  model: '$comment',
46
47
  view: (modelElement, { writer }) => {
47
- const root = this.editor.model.document.getRoot();
48
+ let root = undefined;
49
+ for (const rootName of this.editor.model.document.getRootNames()) {
50
+ root = this.editor.model.document.getRoot(rootName);
51
+ if (root.hasAttribute(modelElement.markerName)) {
52
+ break;
53
+ }
54
+ }
48
55
  const markerName = modelElement.markerName;
49
56
  const commentContent = root.getAttribute(markerName);
50
57
  const comment = writer.createUIElement('$comment');
@@ -52,25 +59,45 @@ export default class HtmlComment extends Plugin {
52
59
  return comment;
53
60
  }
54
61
  });
55
- // Remove comments' markers and their corresponding $root attributes, which are no longer present.
62
+ // Remove comments' markers and their corresponding $root attributes, which are moved to the graveyard.
56
63
  editor.model.document.registerPostFixer(writer => {
57
- const root = editor.model.document.getRoot();
58
- const changedMarkers = editor.model.document.differ.getChangedMarkers();
59
- const changedCommentMarkers = changedMarkers.filter(marker => {
60
- return marker.name.startsWith('$comment');
61
- });
62
- const removedCommentMarkers = changedCommentMarkers.filter(marker => {
63
- const newRange = marker.data.newRange;
64
- return newRange && newRange.root.rootName === '$graveyard';
65
- });
66
- if (removedCommentMarkers.length === 0) {
67
- return false;
68
- }
69
- for (const marker of removedCommentMarkers) {
70
- writer.removeMarker(marker.name);
71
- writer.removeAttribute(marker.name, root);
64
+ let changed = false;
65
+ const markers = editor.model.document.differ.getChangedMarkers().filter(marker => marker.name.startsWith('$comment:'));
66
+ for (const marker of markers) {
67
+ const { oldRange, newRange } = marker.data;
68
+ if (oldRange && newRange && oldRange.root == newRange.root) {
69
+ // The marker was moved in the same root. Don't do anything.
70
+ continue;
71
+ }
72
+ if (oldRange) {
73
+ // The comment marker was moved from one root to another (most probably to the graveyard).
74
+ // Remove the related attribute from the previous root.
75
+ const oldRoot = oldRange.root;
76
+ if (oldRoot.hasAttribute(marker.name)) {
77
+ writer.removeAttribute(marker.name, oldRoot);
78
+ changed = true;
79
+ }
80
+ }
81
+ if (newRange) {
82
+ const newRoot = newRange.root;
83
+ if (newRoot.rootName == '$graveyard') {
84
+ // Comment marker was moved to the graveyard -- remove it entirely.
85
+ writer.removeMarker(marker.name);
86
+ changed = true;
87
+ }
88
+ else if (!newRoot.hasAttribute(marker.name)) {
89
+ // Comment marker was just added or was moved to another root - updated roots attributes.
90
+ //
91
+ // Added fallback to `''` for the comment content in case if someone incorrectly added just the marker "by hand"
92
+ // and forgot to add the root attribute or add them in different change blocks.
93
+ //
94
+ // It caused an infinite loop in one of the unit tests.
95
+ writer.setAttribute(marker.name, loadedCommentsContent.get(marker.name) || '', newRoot);
96
+ changed = true;
97
+ }
98
+ }
72
99
  }
73
- return true;
100
+ return changed;
74
101
  });
75
102
  // Delete all comment markers from the document before setting new data.
76
103
  editor.data.on('set', () => {
@@ -109,7 +136,7 @@ export default class HtmlComment extends Plugin {
109
136
  const id = uid();
110
137
  const editor = this.editor;
111
138
  const model = editor.model;
112
- const root = model.document.getRoot();
139
+ const root = model.document.getRoot(position.root.rootName);
113
140
  const markerName = `$comment:${id}`;
114
141
  return model.change(writer => {
115
142
  const range = writer.createRange(position);
@@ -135,14 +162,12 @@ export default class HtmlComment extends Plugin {
135
162
  */
136
163
  removeHtmlComment(commentID) {
137
164
  const editor = this.editor;
138
- const root = editor.model.document.getRoot();
139
165
  const marker = editor.model.markers.get(commentID);
140
166
  if (!marker) {
141
167
  return false;
142
168
  }
143
169
  editor.model.change(writer => {
144
170
  writer.removeMarker(marker);
145
- writer.removeAttribute(commentID, root);
146
171
  });
147
172
  return true;
148
173
  }
@@ -155,12 +180,19 @@ export default class HtmlComment extends Plugin {
155
180
  getHtmlCommentData(commentID) {
156
181
  const editor = this.editor;
157
182
  const marker = editor.model.markers.get(commentID);
158
- const root = editor.model.document.getRoot();
159
183
  if (!marker) {
160
184
  return null;
161
185
  }
186
+ let content = '';
187
+ for (const rootName of this.editor.model.document.getRootNames()) {
188
+ const root = editor.model.document.getRoot(rootName);
189
+ if (root.hasAttribute(commentID)) {
190
+ content = root.getAttribute(commentID);
191
+ break;
192
+ }
193
+ }
162
194
  return {
163
- content: root.getAttribute(commentID),
195
+ content,
164
196
  position: marker.getStart()
165
197
  };
166
198
  }
package/src/index.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  export { default as GeneralHtmlSupport } from './generalhtmlsupport';
9
9
  export { default as DataFilter } from './datafilter';
10
- export { default as DataSchema } from './dataschema';
10
+ export { default as DataSchema, type DataSchemaBlockElementDefinition } from './dataschema';
11
11
  export { default as HtmlComment } from './htmlcomment';
12
12
  export { default as FullPage } from './fullpage';
13
13
  export { default as HtmlPageDataProcessor } from './htmlpagedataprocessor';
@@ -15,7 +15,7 @@ export default class CodeBlockElementSupport extends Plugin {
15
15
  /**
16
16
  * @inheritDoc
17
17
  */
18
- static get pluginName(): 'CodeBlockElementSupport';
18
+ static get pluginName(): "CodeBlockElementSupport";
19
19
  /**
20
20
  * @inheritDoc
21
21
  */
@@ -38,7 +38,7 @@ export default class CodeBlockElementSupport extends Plugin {
38
38
  const conversion = editor.conversion;
39
39
  // Extend codeBlock to allow attributes required by attribute filtration.
40
40
  schema.extend('codeBlock', {
41
- allowAttributes: ['htmlAttributes', 'htmlContentAttributes']
41
+ allowAttributes: ['htmlPreAttributes', 'htmlContentAttributes']
42
42
  });
43
43
  conversion.for('upcast').add(viewToModelCodeBlockAttributeConverter(dataFilter));
44
44
  conversion.for('downcast').add(modelToViewCodeBlockAttributeConverter());
@@ -50,7 +50,7 @@ export default class CodeBlockElementSupport extends Plugin {
50
50
  * View-to-model conversion helper preserving allowed attributes on {@link module:code-block/codeblock~CodeBlock Code Block}
51
51
  * feature model element.
52
52
  *
53
- * Attributes are preserved as a value of `htmlAttributes` model attribute.
53
+ * Attributes are preserved as a value of `html*Attributes` model attribute.
54
54
  * @param dataFilter
55
55
  * @returns Returns a conversion callback.
56
56
  */
@@ -62,7 +62,7 @@ function viewToModelCodeBlockAttributeConverter(dataFilter) {
62
62
  if (!viewPreElement || !viewPreElement.is('element', 'pre')) {
63
63
  return;
64
64
  }
65
- preserveElementAttributes(viewPreElement, 'htmlAttributes');
65
+ preserveElementAttributes(viewPreElement, 'htmlPreAttributes');
66
66
  preserveElementAttributes(viewCodeElement, 'htmlContentAttributes');
67
67
  function preserveElementAttributes(viewElement, attributeName) {
68
68
  const viewAttributes = dataFilter.processViewAttributes(viewElement, conversionApi);
@@ -80,7 +80,7 @@ function viewToModelCodeBlockAttributeConverter(dataFilter) {
80
80
  */
81
81
  function modelToViewCodeBlockAttributeConverter() {
82
82
  return (dispatcher) => {
83
- dispatcher.on('attribute:htmlAttributes:codeBlock', (evt, data, conversionApi) => {
83
+ dispatcher.on('attribute:htmlPreAttributes:codeBlock', (evt, data, conversionApi) => {
84
84
  if (!conversionApi.consumable.consume(data.item, evt.name)) {
85
85
  return;
86
86
  }
@@ -19,7 +19,7 @@ export default class CustomElementSupport extends Plugin {
19
19
  /**
20
20
  * @inheritDoc
21
21
  */
22
- static get pluginName(): 'CustomElementSupport';
22
+ static get pluginName(): "CustomElementSupport";
23
23
  /**
24
24
  * @inheritDoc
25
25
  */
@@ -42,7 +42,7 @@ export default class CustomElementSupport extends Plugin {
42
42
  const preLikeElements = editor.data.htmlProcessor.domConverter.preElements;
43
43
  schema.register(definition.model, definition.modelSchema);
44
44
  schema.extend(definition.model, {
45
- allowAttributes: ['htmlElementName', 'htmlAttributes', 'htmlContent'],
45
+ allowAttributes: ['htmlElementName', 'htmlCustomElementAttributes', 'htmlContent'],
46
46
  isContent: true
47
47
  });
48
48
  // Being executed on the low priority, it will catch all elements that were not caught by other converters.
@@ -74,7 +74,7 @@ export default class CustomElementSupport extends Plugin {
74
74
  });
75
75
  const htmlAttributes = dataFilter.processViewAttributes(viewElement, conversionApi);
76
76
  if (htmlAttributes) {
77
- conversionApi.writer.setAttribute('htmlAttributes', htmlAttributes, modelElement);
77
+ conversionApi.writer.setAttribute('htmlCustomElementAttributes', htmlAttributes, modelElement);
78
78
  }
79
79
  // Store the whole element in the attribute so that DomConverter will be able to use the pre like element context.
80
80
  const viewWriter = new UpcastWriter(viewElement.document);
@@ -94,13 +94,13 @@ export default class CustomElementSupport extends Plugin {
94
94
  conversion.for('editingDowncast').elementToElement({
95
95
  model: {
96
96
  name: definition.model,
97
- attributes: ['htmlElementName', 'htmlAttributes', 'htmlContent']
97
+ attributes: ['htmlElementName', 'htmlCustomElementAttributes', 'htmlContent']
98
98
  },
99
99
  view: (modelElement, { writer }) => {
100
100
  const viewName = modelElement.getAttribute('htmlElementName');
101
101
  const viewElement = writer.createRawElement(viewName);
102
- if (modelElement.hasAttribute('htmlAttributes')) {
103
- setViewAttributes(writer, modelElement.getAttribute('htmlAttributes'), viewElement);
102
+ if (modelElement.hasAttribute('htmlCustomElementAttributes')) {
103
+ setViewAttributes(writer, modelElement.getAttribute('htmlCustomElementAttributes'), viewElement);
104
104
  }
105
105
  return viewElement;
106
106
  }
@@ -108,7 +108,7 @@ export default class CustomElementSupport extends Plugin {
108
108
  conversion.for('dataDowncast').elementToElement({
109
109
  model: {
110
110
  name: definition.model,
111
- attributes: ['htmlElementName', 'htmlAttributes', 'htmlContent']
111
+ attributes: ['htmlElementName', 'htmlCustomElementAttributes', 'htmlContent']
112
112
  },
113
113
  view: (modelElement, { writer }) => {
114
114
  const viewName = modelElement.getAttribute('htmlElementName');
@@ -123,8 +123,8 @@ export default class CustomElementSupport extends Plugin {
123
123
  domElement.appendChild(customElement.firstChild);
124
124
  }
125
125
  });
126
- if (modelElement.hasAttribute('htmlAttributes')) {
127
- setViewAttributes(writer, modelElement.getAttribute('htmlAttributes'), viewElement);
126
+ if (modelElement.hasAttribute('htmlCustomElementAttributes')) {
127
+ setViewAttributes(writer, modelElement.getAttribute('htmlCustomElementAttributes'), viewElement);
128
128
  }
129
129
  return viewElement;
130
130
  }
@@ -15,7 +15,7 @@ export default class DocumentListElementSupport extends Plugin {
15
15
  /**
16
16
  * @inheritDoc
17
17
  */
18
- static get pluginName(): 'DocumentListElementSupport';
18
+ static get pluginName(): "DocumentListElementSupport";
19
19
  /**
20
20
  * @inheritDoc
21
21
  */
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { isEqual } from 'lodash-es';
9
9
  import { Plugin } from 'ckeditor5/src/core';
10
- import { setViewAttributes } from '../utils';
10
+ import { getHtmlAttributeName, setViewAttributes } from '../utils';
11
11
  import DataFilter from '../datafilter';
12
12
  /**
13
13
  * Provides the General HTML Support integration with the {@link module:list/documentlist~DocumentList Document List} feature.
@@ -37,37 +37,40 @@ export default class DocumentListElementSupport extends Plugin {
37
37
  const conversion = editor.conversion;
38
38
  const dataFilter = editor.plugins.get(DataFilter);
39
39
  const documentListEditing = editor.plugins.get('DocumentListEditing');
40
+ const viewElements = ['ul', 'ol', 'li'];
40
41
  // Register downcast strategy.
41
42
  // Note that this must be done before document list editing registers conversion in afterInit.
42
43
  documentListEditing.registerDowncastStrategy({
43
44
  scope: 'item',
44
45
  attributeName: 'htmlLiAttributes',
45
- setAttributeOnDowncast(writer, attributeValue, viewElement) {
46
- setViewAttributes(writer, attributeValue, viewElement);
47
- }
46
+ setAttributeOnDowncast: setViewAttributes
48
47
  });
49
48
  documentListEditing.registerDowncastStrategy({
50
49
  scope: 'list',
51
- attributeName: 'htmlListAttributes',
52
- setAttributeOnDowncast(writer, viewAttributes, viewElement) {
53
- setViewAttributes(writer, viewAttributes, viewElement);
54
- }
50
+ attributeName: 'htmlUlAttributes',
51
+ setAttributeOnDowncast: setViewAttributes
52
+ });
53
+ documentListEditing.registerDowncastStrategy({
54
+ scope: 'list',
55
+ attributeName: 'htmlOlAttributes',
56
+ setAttributeOnDowncast: setViewAttributes
55
57
  });
56
58
  dataFilter.on('register', (evt, definition) => {
57
- if (!['ul', 'ol', 'li'].includes(definition.view)) {
59
+ if (!viewElements.includes(definition.view)) {
58
60
  return;
59
61
  }
60
62
  evt.stop();
61
63
  // Do not register same converters twice.
62
- if (schema.checkAttribute('$block', 'htmlListAttributes')) {
64
+ if (schema.checkAttribute('$block', 'htmlLiAttributes')) {
63
65
  return;
64
66
  }
65
- schema.extend('$block', { allowAttributes: ['htmlListAttributes', 'htmlLiAttributes'] });
66
- schema.extend('$blockObject', { allowAttributes: ['htmlListAttributes', 'htmlLiAttributes'] });
67
- schema.extend('$container', { allowAttributes: ['htmlListAttributes', 'htmlLiAttributes'] });
67
+ const allowAttributes = viewElements.map(element => getHtmlAttributeName(element));
68
+ schema.extend('$block', { allowAttributes });
69
+ schema.extend('$blockObject', { allowAttributes });
70
+ schema.extend('$container', { allowAttributes });
68
71
  conversion.for('upcast').add(dispatcher => {
69
- dispatcher.on('element:ul', viewToModelListAttributeConverter('htmlListAttributes', dataFilter), { priority: 'low' });
70
- dispatcher.on('element:ol', viewToModelListAttributeConverter('htmlListAttributes', dataFilter), { priority: 'low' });
72
+ dispatcher.on('element:ul', viewToModelListAttributeConverter('htmlUlAttributes', dataFilter), { priority: 'low' });
73
+ dispatcher.on('element:ol', viewToModelListAttributeConverter('htmlOlAttributes', dataFilter), { priority: 'low' });
71
74
  dispatcher.on('element:li', viewToModelListAttributeConverter('htmlLiAttributes', dataFilter), { priority: 'low' });
72
75
  });
73
76
  });
@@ -102,21 +105,38 @@ export default class DocumentListElementSupport extends Plugin {
102
105
  continue;
103
106
  }
104
107
  if (previousNodeInList.getAttribute('listType') == node.getAttribute('listType')) {
105
- const value = previousNodeInList.getAttribute('htmlListAttributes');
106
- if (!isEqual(node.getAttribute('htmlListAttributes'), value)) {
107
- writer.setAttribute('htmlListAttributes', value, node);
108
+ const attribute = getAttributeFromListType(previousNodeInList.getAttribute('listType'));
109
+ const value = previousNodeInList.getAttribute(attribute);
110
+ if (!isEqual(node.getAttribute(attribute), value) &&
111
+ writer.model.schema.checkAttribute(node, attribute)) {
112
+ writer.setAttribute(attribute, value, node);
108
113
  evt.return = true;
109
114
  }
110
115
  }
111
116
  if (previousNodeInList.getAttribute('listItemId') == node.getAttribute('listItemId')) {
112
117
  const value = previousNodeInList.getAttribute('htmlLiAttributes');
113
- if (!isEqual(node.getAttribute('htmlLiAttributes'), value)) {
118
+ if (!isEqual(node.getAttribute('htmlLiAttributes'), value) &&
119
+ writer.model.schema.checkAttribute(node, 'htmlLiAttributes')) {
114
120
  writer.setAttribute('htmlLiAttributes', value, node);
115
121
  evt.return = true;
116
122
  }
117
123
  }
118
124
  }
119
125
  });
126
+ // Remove `ol` attributes from `ul` elements and vice versa.
127
+ documentListEditing.on('postFixer', (evt, { listNodes, writer }) => {
128
+ for (const { node } of listNodes) {
129
+ const listType = node.getAttribute('listType');
130
+ if (listType === 'bulleted' && node.getAttribute('htmlOlAttributes')) {
131
+ writer.removeAttribute('htmlOlAttributes', node);
132
+ evt.return = true;
133
+ }
134
+ if (listType === 'numbered' && node.getAttribute('htmlUlAttributes')) {
135
+ writer.removeAttribute('htmlUlAttributes', node);
136
+ evt.return = true;
137
+ }
138
+ }
139
+ });
120
140
  }
121
141
  /**
122
142
  * @inheritDoc
@@ -131,10 +151,14 @@ export default class DocumentListElementSupport extends Plugin {
131
151
  this.listenTo(indentList, 'afterExecute', (evt, changedBlocks) => {
132
152
  editor.model.change(writer => {
133
153
  for (const node of changedBlocks) {
154
+ const attribute = getAttributeFromListType(node.getAttribute('listType'));
155
+ if (!editor.model.schema.checkAttribute(node, attribute)) {
156
+ continue;
157
+ }
134
158
  // Just reset the attribute.
135
159
  // If there is a previous indented list that this node should be merged into,
136
160
  // the postfixer will unify all the attributes of both sub-lists.
137
- writer.setAttribute('htmlListAttributes', {}, node);
161
+ writer.setAttribute(attribute, {}, node);
138
162
  }
139
163
  });
140
164
  });
@@ -147,7 +171,7 @@ export default class DocumentListElementSupport extends Plugin {
147
171
  * @returns Returns a conversion callback.
148
172
  */
149
173
  function viewToModelListAttributeConverter(attributeName, dataFilter) {
150
- const callback = (evt, data, conversionApi) => {
174
+ return (evt, data, conversionApi) => {
151
175
  const viewElement = data.viewItem;
152
176
  if (!data.modelRange) {
153
177
  Object.assign(data, conversionApi.convertChildren(data.viewItem, data.modelCursor));
@@ -163,8 +187,17 @@ function viewToModelListAttributeConverter(attributeName, dataFilter) {
163
187
  if (item.hasAttribute(attributeName)) {
164
188
  continue;
165
189
  }
166
- conversionApi.writer.setAttribute(attributeName, viewAttributes || {}, item);
190
+ if (conversionApi.writer.model.schema.checkAttribute(item, attributeName)) {
191
+ conversionApi.writer.setAttribute(attributeName, viewAttributes || {}, item);
192
+ }
167
193
  }
168
194
  };
169
- return callback;
195
+ }
196
+ /**
197
+ * Returns HTML attribute name based on provided list type.
198
+ */
199
+ function getAttributeFromListType(listType) {
200
+ return listType === 'bulleted' ?
201
+ 'htmlUlAttributes' :
202
+ 'htmlOlAttributes';
170
203
  }