@ckeditor/ckeditor5-html-support 38.0.0 → 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/build/html-support.js +1 -1
- package/package.json +2 -45
- package/src/converters.d.ts +3 -3
- package/src/converters.js +9 -8
- package/src/datafilter.d.ts +33 -3
- package/src/datafilter.js +64 -11
- package/src/dataschema.d.ts +2 -2
- package/src/fullpage.d.ts +1 -1
- package/src/generalhtmlsupport.d.ts +1 -1
- package/src/generalhtmlsupport.js +2 -2
- package/src/htmlcomment.d.ts +1 -1
- package/src/htmlcomment.js +59 -27
- package/src/index.d.ts +1 -1
- package/src/integrations/codeblock.d.ts +1 -1
- package/src/integrations/codeblock.js +4 -4
- package/src/integrations/customelement.d.ts +1 -1
- package/src/integrations/customelement.js +8 -8
- package/src/integrations/documentlist.d.ts +1 -1
- package/src/integrations/documentlist.js +56 -23
- package/src/integrations/dualcontent.d.ts +1 -1
- package/src/integrations/dualcontent.js +3 -2
- package/src/integrations/heading.d.ts +1 -5
- package/src/integrations/heading.js +0 -15
- package/src/integrations/image.d.ts +1 -1
- package/src/integrations/image.js +5 -5
- package/src/integrations/mediaembed.d.ts +1 -1
- package/src/integrations/mediaembed.js +4 -4
- package/src/integrations/script.d.ts +1 -1
- package/src/integrations/script.js +1 -1
- package/src/integrations/style.d.ts +1 -1
- package/src/integrations/style.js +1 -1
- package/src/integrations/table.d.ts +1 -1
- package/src/integrations/table.js +3 -3
- package/src/schemadefinitions.js +9 -5
- package/src/utils.d.ts +11 -0
- package/src/utils.js +16 -1
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
487
|
+
allowAttributes: getHtmlAttributeName(viewName)
|
|
435
488
|
});
|
|
436
489
|
conversion.for('upcast').add(viewToModelBlockAttributeConverter(definition, this));
|
|
437
490
|
conversion.for('downcast').add(modelToViewBlockAttributeConverter(definition));
|
package/src/dataschema.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export default class DataSchema extends Plugin {
|
|
|
47
47
|
/**
|
|
48
48
|
* @inheritDoc
|
|
49
49
|
*/
|
|
50
|
-
static get pluginName():
|
|
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#
|
|
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
|
@@ -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
|
|
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.
|
package/src/htmlcomment.d.ts
CHANGED
package/src/htmlcomment.js
CHANGED
|
@@ -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:
|
|
36
|
-
const
|
|
36
|
+
model: viewElement => {
|
|
37
|
+
const markerUid = uid();
|
|
38
|
+
const markerName = `$comment:${markerUid}`;
|
|
37
39
|
const commentContent = viewElement.getCustomProperty('$rawContent');
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
62
|
+
// Remove comments' markers and their corresponding $root attributes, which are moved to the graveyard.
|
|
56
63
|
editor.model.document.registerPostFixer(writer => {
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
|
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';
|
|
@@ -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: ['
|
|
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 `
|
|
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, '
|
|
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:
|
|
83
|
+
dispatcher.on('attribute:htmlPreAttributes:codeBlock', (evt, data, conversionApi) => {
|
|
84
84
|
if (!conversionApi.consumable.consume(data.item, evt.name)) {
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
@@ -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', '
|
|
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('
|
|
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', '
|
|
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('
|
|
103
|
-
setViewAttributes(writer, modelElement.getAttribute('
|
|
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', '
|
|
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('
|
|
127
|
-
setViewAttributes(writer, modelElement.getAttribute('
|
|
126
|
+
if (modelElement.hasAttribute('htmlCustomElementAttributes')) {
|
|
127
|
+
setViewAttributes(writer, modelElement.getAttribute('htmlCustomElementAttributes'), viewElement);
|
|
128
128
|
}
|
|
129
129
|
return viewElement;
|
|
130
130
|
}
|
|
@@ -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
|
|
46
|
-
setViewAttributes(writer, attributeValue, viewElement);
|
|
47
|
-
}
|
|
46
|
+
setAttributeOnDowncast: setViewAttributes
|
|
48
47
|
});
|
|
49
48
|
documentListEditing.registerDowncastStrategy({
|
|
50
49
|
scope: 'list',
|
|
51
|
-
attributeName: '
|
|
52
|
-
setAttributeOnDowncast
|
|
53
|
-
|
|
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 (!
|
|
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', '
|
|
64
|
+
if (schema.checkAttribute('$block', 'htmlLiAttributes')) {
|
|
63
65
|
return;
|
|
64
66
|
}
|
|
65
|
-
|
|
66
|
-
schema.extend('$
|
|
67
|
-
schema.extend('$
|
|
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('
|
|
70
|
-
dispatcher.on('element:ol', viewToModelListAttributeConverter('
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
190
|
+
if (conversionApi.writer.model.schema.checkAttribute(item, attributeName)) {
|
|
191
|
+
conversionApi.writer.setAttribute(attributeName, viewAttributes || {}, item);
|
|
192
|
+
}
|
|
167
193
|
}
|
|
168
194
|
};
|
|
169
|
-
|
|
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
|
}
|