@ckeditor/ckeditor5-html-support 34.0.0 → 35.0.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/CHANGELOG.md +4 -0
- package/LICENSE.md +6 -2
- package/build/html-support.js +1 -1
- package/build/translations/ar.js +1 -0
- package/build/translations/bg.js +1 -0
- package/build/translations/bn.js +1 -0
- package/build/translations/ca.js +1 -0
- package/build/translations/da.js +1 -0
- package/build/translations/et.js +1 -0
- package/build/translations/fi.js +1 -0
- package/build/translations/fr.js +1 -0
- package/build/translations/he.js +1 -0
- package/build/translations/hi.js +1 -0
- package/build/translations/ja.js +1 -0
- package/build/translations/ko.js +1 -0
- package/build/translations/lt.js +1 -0
- package/build/translations/lv.js +1 -0
- package/build/translations/ms.js +1 -0
- package/build/translations/no.js +1 -0
- package/build/translations/pt.js +1 -0
- package/build/translations/ro.js +1 -0
- package/build/translations/sv.js +1 -0
- package/build/translations/th.js +1 -0
- package/build/translations/tr.js +1 -0
- package/build/translations/uk.js +1 -0
- package/build/translations/ur.js +1 -0
- package/build/translations/vi.js +1 -0
- package/build/translations/zh-cn.js +1 -0
- package/lang/translations/ar.po +21 -0
- package/lang/translations/bg.po +21 -0
- package/lang/translations/bn.po +21 -0
- package/lang/translations/ca.po +21 -0
- package/lang/translations/da.po +21 -0
- package/lang/translations/es.po +1 -1
- package/lang/translations/et.po +21 -0
- package/lang/translations/fi.po +21 -0
- package/lang/translations/fr.po +21 -0
- package/lang/translations/he.po +21 -0
- package/lang/translations/hi.po +21 -0
- package/lang/translations/it.po +1 -1
- package/lang/translations/ja.po +21 -0
- package/lang/translations/ko.po +21 -0
- package/lang/translations/lt.po +21 -0
- package/lang/translations/lv.po +21 -0
- package/lang/translations/ms.po +21 -0
- package/lang/translations/no.po +21 -0
- package/lang/translations/pt-br.po +1 -1
- package/lang/translations/pt.po +21 -0
- package/lang/translations/ro.po +21 -0
- package/lang/translations/sv.po +21 -0
- package/lang/translations/th.po +21 -0
- package/lang/translations/tr.po +21 -0
- package/lang/translations/uk.po +21 -0
- package/lang/translations/ur.po +21 -0
- package/lang/translations/vi.po +21 -0
- package/lang/translations/zh-cn.po +21 -0
- package/package.json +32 -31
- package/src/converters.js +16 -5
- package/src/datafilter.js +149 -13
- package/src/dataschema.js +3 -0
- package/src/generalhtmlsupport.js +7 -5
- package/src/integrations/codeblock.js +8 -1
- package/src/integrations/customelement.js +179 -0
- package/src/integrations/documentlist.js +10 -3
- package/src/integrations/dualcontent.js +19 -3
- package/src/integrations/heading.js +7 -0
- package/src/integrations/image.js +36 -10
- package/src/integrations/mediaembed.js +42 -6
- package/src/integrations/script.js +7 -0
- package/src/integrations/style.js +7 -0
- package/src/integrations/table.js +36 -7
- package/src/schemadefinitions.js +19 -0
- package/build/html-support.js.map +0 -1
package/src/datafilter.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* @module html-support/datafilter
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/* globals document */
|
|
11
|
+
|
|
10
12
|
import DataSchema from './dataschema';
|
|
11
13
|
|
|
12
14
|
import { Plugin } from 'ckeditor5/src/core';
|
|
@@ -53,6 +55,9 @@ import '../theme/datafilter.css';
|
|
|
53
55
|
* }
|
|
54
56
|
* } );
|
|
55
57
|
*
|
|
58
|
+
* To apply the information about allowed and disallowed attributes in custom integration plugin,
|
|
59
|
+
* use the {@link module:html-support/datafilter~DataFilter#processViewAttributes `processViewAttributes()`} method.
|
|
60
|
+
*
|
|
56
61
|
* @extends module:core/plugin~Plugin
|
|
57
62
|
*/
|
|
58
63
|
export default class DataFilter extends Plugin {
|
|
@@ -106,8 +111,18 @@ export default class DataFilter extends Plugin {
|
|
|
106
111
|
*/
|
|
107
112
|
this._dataInitialized = false;
|
|
108
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Cached map of coupled attributes. Keys are the feature attributes names
|
|
116
|
+
* and values are arrays with coupled GHS attributes names.
|
|
117
|
+
*
|
|
118
|
+
* @private
|
|
119
|
+
* @member {Map.<String,Array>}
|
|
120
|
+
*/
|
|
121
|
+
this._coupledAttributes = null;
|
|
122
|
+
|
|
109
123
|
this._registerElementsAfterInit();
|
|
110
124
|
this._registerElementHandlers();
|
|
125
|
+
this._registerModelPostFixer();
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
/**
|
|
@@ -167,6 +182,9 @@ export default class DataFilter extends Plugin {
|
|
|
167
182
|
if ( this._dataInitialized ) {
|
|
168
183
|
this._fireRegisterEvent( definition );
|
|
169
184
|
}
|
|
185
|
+
|
|
186
|
+
// Reset cached map to recalculate it on the next usage.
|
|
187
|
+
this._coupledAttributes = null;
|
|
170
188
|
}
|
|
171
189
|
}
|
|
172
190
|
|
|
@@ -208,17 +226,30 @@ export default class DataFilter extends Plugin {
|
|
|
208
226
|
}
|
|
209
227
|
|
|
210
228
|
/**
|
|
211
|
-
*
|
|
229
|
+
* Processes all allowed and disallowed attributes on the view element by consuming them and returning the allowed ones.
|
|
212
230
|
*
|
|
213
|
-
* @
|
|
231
|
+
* This method applies the configuration set up by {@link #allowAttributes `allowAttributes()`}
|
|
232
|
+
* and {@link #disallowAttributes `disallowAttributes()`} over the given view element by consuming relevant attributes.
|
|
233
|
+
* It returns the allowed attributes that were found on the given view element for further processing by integration code.
|
|
234
|
+
*
|
|
235
|
+
* dispatcher.on( 'element:myElement', ( evt, data, conversionApi ) => {
|
|
236
|
+
* // Get rid of disallowed and extract all allowed attributes from a viewElement.
|
|
237
|
+
* const viewAttributes = dataFilter.processViewAttributes( data.viewItem, conversionApi );
|
|
238
|
+
* // Do something with them, i.e. store inside a model as a dictionary.
|
|
239
|
+
* if ( viewAttributes ) {
|
|
240
|
+
* conversionApi.writer.setAttribute( 'htmlAttributesOfMyElement', viewAttributes, data.modelRange );
|
|
241
|
+
* }
|
|
242
|
+
* } );
|
|
243
|
+
*
|
|
244
|
+
* @see module:engine/conversion/viewconsumable~ViewConsumable#consume
|
|
214
245
|
* @param {module:engine/view/element~Element} viewElement
|
|
215
|
-
* @param {module:engine/conversion/
|
|
246
|
+
* @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
|
|
216
247
|
* @returns {Object} [result]
|
|
217
248
|
* @returns {Object} result.attributes Set with matched attribute names.
|
|
218
249
|
* @returns {Object} result.styles Set with matched style names.
|
|
219
250
|
* @returns {Array.<String>} result.classes Set with matched class names.
|
|
220
251
|
*/
|
|
221
|
-
|
|
252
|
+
processViewAttributes( viewElement, conversionApi ) {
|
|
222
253
|
// Make sure that the disabled attributes are handled before the allowed attributes are called.
|
|
223
254
|
// For example, for block images the <figure> converter triggers conversion for <img> first and then for other elements, i.e. <a>.
|
|
224
255
|
consumeAttributes( viewElement, conversionApi, this._disallowedAttributes );
|
|
@@ -288,6 +319,91 @@ export default class DataFilter extends Plugin {
|
|
|
288
319
|
}, { priority: 'lowest' } );
|
|
289
320
|
}
|
|
290
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Registers a model post-fixer that is removing coupled GHS attributes of inline elements. Those attributes
|
|
324
|
+
* are removed if a coupled feature attribute is removed.
|
|
325
|
+
*
|
|
326
|
+
* For example, consider following HTML:
|
|
327
|
+
*
|
|
328
|
+
* <a href="foo.html" id="myId">bar</a>
|
|
329
|
+
*
|
|
330
|
+
* Which would be upcasted to following text node in the model:
|
|
331
|
+
*
|
|
332
|
+
* <$text linkHref="foo.html" htmlA="{ attributes: { id: 'myId' } }">bar</$text>
|
|
333
|
+
*
|
|
334
|
+
* When the user removes the link from that text (using UI), only `linkHref` attribute would be removed:
|
|
335
|
+
*
|
|
336
|
+
* <$text htmlA="{ attributes: { id: 'myId' } }">bar</$text>
|
|
337
|
+
*
|
|
338
|
+
* The `htmlA` attribute would stay in the model and would cause GHS to generate an `<a>` element.
|
|
339
|
+
* This is incorrect from UX point of view, as the user wanted to remove the whole link (not only `href`).
|
|
340
|
+
*
|
|
341
|
+
* @private
|
|
342
|
+
*/
|
|
343
|
+
_registerModelPostFixer() {
|
|
344
|
+
const model = this.editor.model;
|
|
345
|
+
|
|
346
|
+
model.document.registerPostFixer( writer => {
|
|
347
|
+
const changes = model.document.differ.getChanges();
|
|
348
|
+
let changed = false;
|
|
349
|
+
|
|
350
|
+
const coupledAttributes = this._getCoupledAttributesMap();
|
|
351
|
+
|
|
352
|
+
for ( const change of changes ) {
|
|
353
|
+
// Handle only attribute removals.
|
|
354
|
+
if ( change.type != 'attribute' || change.attributeNewValue !== null ) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Find a list of coupled GHS attributes.
|
|
359
|
+
const attributeKeys = coupledAttributes.get( change.attributeKey );
|
|
360
|
+
|
|
361
|
+
if ( !attributeKeys ) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Remove the coupled GHS attributes on the same range as the feature attribute was removed.
|
|
366
|
+
for ( const { item } of change.range.getWalker( { shallow: true } ) ) {
|
|
367
|
+
for ( const attributeKey of attributeKeys ) {
|
|
368
|
+
if ( item.hasAttribute( attributeKey ) ) {
|
|
369
|
+
writer.removeAttribute( attributeKey, item );
|
|
370
|
+
changed = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return changed;
|
|
377
|
+
} );
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Collects the map of coupled attributes. The returned map is keyed by the feature attribute name
|
|
382
|
+
* and coupled GHS attribute names are stored in the value array .
|
|
383
|
+
*
|
|
384
|
+
* @private
|
|
385
|
+
* @returns {Map.<String,Array>}
|
|
386
|
+
*/
|
|
387
|
+
_getCoupledAttributesMap() {
|
|
388
|
+
if ( this._coupledAttributes ) {
|
|
389
|
+
return this._coupledAttributes;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
this._coupledAttributes = new Map();
|
|
393
|
+
|
|
394
|
+
for ( const definition of this._allowedElements ) {
|
|
395
|
+
if ( definition.coupledAttribute && definition.model ) {
|
|
396
|
+
const attributeNames = this._coupledAttributes.get( definition.coupledAttribute );
|
|
397
|
+
|
|
398
|
+
if ( attributeNames ) {
|
|
399
|
+
attributeNames.push( definition.model );
|
|
400
|
+
} else {
|
|
401
|
+
this._coupledAttributes.set( definition.coupledAttribute, [ definition.model ] );
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
291
407
|
/**
|
|
292
408
|
* Fires `register` event for the given element definition.
|
|
293
409
|
*
|
|
@@ -438,14 +554,14 @@ export default class DataFilter extends Plugin {
|
|
|
438
554
|
* as an event namespace, e.g. `register:span`.
|
|
439
555
|
*
|
|
440
556
|
* dataFilter.on( 'register', ( evt, definition ) => {
|
|
441
|
-
* editor.schema.register( definition.model, definition.modelSchema );
|
|
557
|
+
* editor.model.schema.register( definition.model, definition.modelSchema );
|
|
442
558
|
* editor.conversion.elementToElement( { model: definition.model, view: definition.view } );
|
|
443
559
|
*
|
|
444
560
|
* evt.stop();
|
|
445
561
|
* } );
|
|
446
562
|
*
|
|
447
563
|
* dataFilter.on( 'register:span', ( evt, definition ) => {
|
|
448
|
-
* editor.schema.extend( '$text', { allowAttributes: 'htmlSpan' } );
|
|
564
|
+
* editor.model.schema.extend( '$text', { allowAttributes: 'htmlSpan' } );
|
|
449
565
|
*
|
|
450
566
|
* editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'span', model: 'htmlSpan' } );
|
|
451
567
|
* editor.conversion.for( 'downcast' ).attributeToElement( { view: 'span', model: 'htmlSpan' } );
|
|
@@ -462,7 +578,7 @@ export default class DataFilter extends Plugin {
|
|
|
462
578
|
//
|
|
463
579
|
// @private
|
|
464
580
|
// @param {module:engine/view/element~Element} viewElement
|
|
465
|
-
// @param {module:engine/conversion/
|
|
581
|
+
// @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
|
|
466
582
|
// @param {module:engine/view/matcher~Matcher Matcher} matcher
|
|
467
583
|
// @returns {Object} [result]
|
|
468
584
|
// @returns {Object} result.attributes
|
|
@@ -473,6 +589,15 @@ function consumeAttributes( viewElement, conversionApi, matcher ) {
|
|
|
473
589
|
const { attributes, styles, classes } = mergeMatchResults( matches );
|
|
474
590
|
const viewAttributes = {};
|
|
475
591
|
|
|
592
|
+
// Remove invalid DOM element attributes.
|
|
593
|
+
if ( attributes.size ) {
|
|
594
|
+
for ( const key of attributes ) {
|
|
595
|
+
if ( !isValidAttributeName( key ) ) {
|
|
596
|
+
attributes.delete( key );
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
476
601
|
if ( attributes.size ) {
|
|
477
602
|
viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) );
|
|
478
603
|
}
|
|
@@ -496,7 +621,7 @@ function consumeAttributes( viewElement, conversionApi, matcher ) {
|
|
|
496
621
|
//
|
|
497
622
|
// @private
|
|
498
623
|
// @param {module:engine/view/element~Element} viewElement
|
|
499
|
-
// @param {module:engine/conversion/
|
|
624
|
+
// @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
|
|
500
625
|
// @param {module:engine/view/matcher~Matcher Matcher} matcher
|
|
501
626
|
// @returns {Array.<Object>} Array with match information about found attributes.
|
|
502
627
|
function consumeAttributeMatches( viewElement, { consumable }, matcher ) {
|
|
@@ -509,9 +634,8 @@ function consumeAttributeMatches( viewElement, { consumable }, matcher ) {
|
|
|
509
634
|
// We only want to consume attributes, so element can be still processed by other converters.
|
|
510
635
|
delete match.match.name;
|
|
511
636
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
637
|
+
consumable.consume( viewElement, match.match );
|
|
638
|
+
consumedMatches.push( match );
|
|
515
639
|
}
|
|
516
640
|
|
|
517
641
|
return consumedMatches;
|
|
@@ -521,7 +645,7 @@ function consumeAttributeMatches( viewElement, { consumable }, matcher ) {
|
|
|
521
645
|
//
|
|
522
646
|
// @private
|
|
523
647
|
// @param {module:engine/view/element~Element} viewElement
|
|
524
|
-
// @param {module:engine/conversion/
|
|
648
|
+
// @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable
|
|
525
649
|
// @param {Object} match
|
|
526
650
|
function removeConsumedAttributes( consumable, viewElement, match ) {
|
|
527
651
|
for ( const key of [ 'attributes', 'classes', 'styles' ] ) {
|
|
@@ -531,7 +655,8 @@ function removeConsumedAttributes( consumable, viewElement, match ) {
|
|
|
531
655
|
continue;
|
|
532
656
|
}
|
|
533
657
|
|
|
534
|
-
|
|
658
|
+
// Iterating over a copy of an array so removing items doesn't influence iteration.
|
|
659
|
+
for ( const value of Array.from( attributes ) ) {
|
|
535
660
|
if ( !consumable.test( viewElement, ( { [ key ]: [ value ] } ) ) ) {
|
|
536
661
|
removeItemFromArray( attributes, value );
|
|
537
662
|
}
|
|
@@ -638,3 +763,14 @@ function splitRules( rules ) {
|
|
|
638
763
|
|
|
639
764
|
return splittedRules;
|
|
640
765
|
}
|
|
766
|
+
|
|
767
|
+
// Returns true if name is valid for a DOM attribute name.
|
|
768
|
+
function isValidAttributeName( name ) {
|
|
769
|
+
try {
|
|
770
|
+
document.createAttribute( name );
|
|
771
|
+
} catch ( error ) {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return true;
|
|
776
|
+
}
|
package/src/dataschema.js
CHANGED
|
@@ -251,5 +251,8 @@ function testViewName( pattern, viewName ) {
|
|
|
251
251
|
* @property {Number} [priority] Element priority. Decides in what order elements are wrapped by
|
|
252
252
|
* {@link module:engine/view/downcastwriter~DowncastWriter}.
|
|
253
253
|
* Set by {@link module:html-support/dataschema~DataSchema#registerInlineElement} method.
|
|
254
|
+
* @property {String} [coupledAttribute] The name of the model attribute that generates the same view element. GHS inline attribute
|
|
255
|
+
* will be removed from the model tree as soon as the coupled attribute is removed. See
|
|
256
|
+
* {@link module:html-support/datafilter~DataFilter#_registerModelPostFixer GHS post-fixer} for more details.
|
|
254
257
|
* @extends module:html-support/dataschema~DataSchemaDefinition
|
|
255
258
|
*/
|
|
@@ -20,6 +20,7 @@ import ScriptElementSupport from './integrations/script';
|
|
|
20
20
|
import TableElementSupport from './integrations/table';
|
|
21
21
|
import StyleElementSupport from './integrations/style';
|
|
22
22
|
import DocumentListElementSupport from './integrations/documentlist';
|
|
23
|
+
import CustomElementSupport from './integrations/customelement';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* The General HTML Support feature.
|
|
@@ -51,7 +52,8 @@ export default class GeneralHtmlSupport extends Plugin {
|
|
|
51
52
|
ScriptElementSupport,
|
|
52
53
|
TableElementSupport,
|
|
53
54
|
StyleElementSupport,
|
|
54
|
-
DocumentListElementSupport
|
|
55
|
+
DocumentListElementSupport,
|
|
56
|
+
CustomElementSupport
|
|
55
57
|
];
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -86,7 +88,7 @@ export default class GeneralHtmlSupport extends Plugin {
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
/**
|
|
89
|
-
* Updates GHS model attribute for a specified view element name, so it includes
|
|
91
|
+
* Updates GHS model attribute for a specified view element name, so it includes the given class name.
|
|
90
92
|
*
|
|
91
93
|
* @protected
|
|
92
94
|
* @param {String} viewElementName A view element name.
|
|
@@ -109,7 +111,7 @@ export default class GeneralHtmlSupport extends Plugin {
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
/**
|
|
112
|
-
* Updates GHS model attribute for a specified view element name, so it does not include
|
|
114
|
+
* Updates GHS model attribute for a specified view element name, so it does not include the given class name.
|
|
113
115
|
*
|
|
114
116
|
* @protected
|
|
115
117
|
* @param {String} viewElementName A view element name.
|
|
@@ -132,7 +134,7 @@ export default class GeneralHtmlSupport extends Plugin {
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
/**
|
|
135
|
-
* Updates GHS model attribute for a specified view element name, so it includes
|
|
137
|
+
* Updates GHS model attribute for a specified view element name, so it includes the given attribute.
|
|
136
138
|
*
|
|
137
139
|
* @protected
|
|
138
140
|
* @param {String} viewElementName A view element name.
|
|
@@ -155,7 +157,7 @@ export default class GeneralHtmlSupport extends Plugin {
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
/**
|
|
158
|
-
* Updates GHS model attribute for a specified view element name, so it does not include
|
|
160
|
+
* Updates GHS model attribute for a specified view element name, so it does not include the given attribute.
|
|
159
161
|
*
|
|
160
162
|
* @protected
|
|
161
163
|
* @param {String} viewElementName A view element name.
|
|
@@ -25,6 +25,13 @@ export default class CodeBlockElementSupport extends Plugin {
|
|
|
25
25
|
return [ DataFilter ];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* @inheritDoc
|
|
30
|
+
*/
|
|
31
|
+
static get pluginName() {
|
|
32
|
+
return 'CodeBlockElementSupport';
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
/**
|
|
29
36
|
* @inheritDoc
|
|
30
37
|
*/
|
|
@@ -79,7 +86,7 @@ function viewToModelCodeBlockAttributeConverter( dataFilter ) {
|
|
|
79
86
|
preserveElementAttributes( viewCodeElement, 'htmlContentAttributes' );
|
|
80
87
|
|
|
81
88
|
function preserveElementAttributes( viewElement, attributeName ) {
|
|
82
|
-
const viewAttributes = dataFilter.
|
|
89
|
+
const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
|
|
83
90
|
|
|
84
91
|
if ( viewAttributes ) {
|
|
85
92
|
conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2022, 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
|
+
/**
|
|
7
|
+
* @module html-support/integrations/customelement
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* globals document */
|
|
11
|
+
|
|
12
|
+
import { Plugin } from 'ckeditor5/src/core';
|
|
13
|
+
import { UpcastWriter } from 'ckeditor5/src/engine';
|
|
14
|
+
|
|
15
|
+
import DataSchema from '../dataschema';
|
|
16
|
+
import DataFilter from '../datafilter';
|
|
17
|
+
import { setViewAttributes } from '../conversionutils';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Provides the General HTML Support for custom elements (not registered in the {@link module:html-support/dataschema~DataSchema}).
|
|
21
|
+
*
|
|
22
|
+
* @extends module:core/plugin~Plugin
|
|
23
|
+
*/
|
|
24
|
+
export default class CustomElementSupport extends Plugin {
|
|
25
|
+
/**
|
|
26
|
+
* @inheritDoc
|
|
27
|
+
*/
|
|
28
|
+
static get requires() {
|
|
29
|
+
return [ DataFilter, DataSchema ];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @inheritDoc
|
|
34
|
+
*/
|
|
35
|
+
static get pluginName() {
|
|
36
|
+
return 'CustomElementSupport';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @inheritDoc
|
|
41
|
+
*/
|
|
42
|
+
init() {
|
|
43
|
+
const dataFilter = this.editor.plugins.get( DataFilter );
|
|
44
|
+
const dataSchema = this.editor.plugins.get( DataSchema );
|
|
45
|
+
|
|
46
|
+
dataFilter.on( 'register:$customElement', ( evt, definition ) => {
|
|
47
|
+
evt.stop();
|
|
48
|
+
|
|
49
|
+
const editor = this.editor;
|
|
50
|
+
const schema = editor.model.schema;
|
|
51
|
+
const conversion = editor.conversion;
|
|
52
|
+
const unsafeElements = editor.editing.view.domConverter.unsafeElements;
|
|
53
|
+
const preLikeElements = editor.data.htmlProcessor.domConverter.preElements;
|
|
54
|
+
|
|
55
|
+
schema.register( definition.model, definition.modelSchema );
|
|
56
|
+
schema.extend( definition.model, {
|
|
57
|
+
allowAttributes: [ 'htmlElementName', 'htmlAttributes', 'htmlContent' ],
|
|
58
|
+
isContent: true
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
// Being executed on the low priority, it will catch all elements that were not caught by other converters.
|
|
62
|
+
conversion.for( 'upcast' ).elementToElement( {
|
|
63
|
+
view: /.*/,
|
|
64
|
+
model: ( viewElement, conversionApi ) => {
|
|
65
|
+
// Do not try to convert $comment fake element.
|
|
66
|
+
if ( viewElement.name == '$comment' ) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ( !isValidElementName( viewElement.name ) ) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Allow for fallback only if this element is not defined in data schema to make sure
|
|
75
|
+
// that this will handle only custom elements not registered in the data schema.
|
|
76
|
+
if ( dataSchema.getDefinitionsForView( viewElement.name ).size ) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Make sure that this element will not render in the editing view.
|
|
81
|
+
if ( !unsafeElements.includes( viewElement.name ) ) {
|
|
82
|
+
unsafeElements.push( viewElement.name );
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Make sure that whitespaces will not be trimmed or replaced by nbsps while stringify content.
|
|
86
|
+
if ( !preLikeElements.includes( viewElement.name ) ) {
|
|
87
|
+
preLikeElements.push( viewElement.name );
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const modelElement = conversionApi.writer.createElement( definition.model, {
|
|
91
|
+
htmlElementName: viewElement.name
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
const htmlAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
|
|
95
|
+
|
|
96
|
+
if ( htmlAttributes ) {
|
|
97
|
+
conversionApi.writer.setAttribute( 'htmlAttributes', htmlAttributes, modelElement );
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Store the whole element in the attribute so that DomConverter will be able to use the pre like element context.
|
|
101
|
+
const viewWriter = new UpcastWriter( viewElement.document );
|
|
102
|
+
const documentFragment = viewWriter.createDocumentFragment( viewElement );
|
|
103
|
+
const htmlContent = editor.data.processor.toData( documentFragment );
|
|
104
|
+
|
|
105
|
+
conversionApi.writer.setAttribute( 'htmlContent', htmlContent, modelElement );
|
|
106
|
+
|
|
107
|
+
// Consume the content of the element.
|
|
108
|
+
for ( const { item } of editor.editing.view.createRangeIn( viewElement ) ) {
|
|
109
|
+
conversionApi.consumable.consume( item, { name: true } );
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return modelElement;
|
|
113
|
+
},
|
|
114
|
+
converterPriority: 'low'
|
|
115
|
+
} );
|
|
116
|
+
|
|
117
|
+
// Because this element is unsafe (DomConverter#unsafeElements), it will render as a transparent <span> but it must
|
|
118
|
+
// be rendered anyway for the mapping between the model and the view to exist.
|
|
119
|
+
conversion.for( 'editingDowncast' ).elementToElement( {
|
|
120
|
+
model: {
|
|
121
|
+
name: definition.model,
|
|
122
|
+
attributes: [ 'htmlElementName', 'htmlAttributes', 'htmlContent' ]
|
|
123
|
+
},
|
|
124
|
+
view: ( modelElement, { writer } ) => {
|
|
125
|
+
const viewName = modelElement.getAttribute( 'htmlElementName' );
|
|
126
|
+
const viewElement = writer.createRawElement( viewName );
|
|
127
|
+
|
|
128
|
+
if ( modelElement.hasAttribute( 'htmlAttributes' ) ) {
|
|
129
|
+
setViewAttributes( writer, modelElement.getAttribute( 'htmlAttributes' ), viewElement );
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return viewElement;
|
|
133
|
+
}
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
conversion.for( 'dataDowncast' ).elementToElement( {
|
|
137
|
+
model: {
|
|
138
|
+
name: definition.model,
|
|
139
|
+
attributes: [ 'htmlElementName', 'htmlAttributes', 'htmlContent' ]
|
|
140
|
+
},
|
|
141
|
+
view: ( modelElement, { writer } ) => {
|
|
142
|
+
const viewName = modelElement.getAttribute( 'htmlElementName' );
|
|
143
|
+
const htmlContent = modelElement.getAttribute( 'htmlContent' );
|
|
144
|
+
|
|
145
|
+
const viewElement = writer.createRawElement( viewName, null, ( domElement, domConverter ) => {
|
|
146
|
+
domConverter.setContentOf( domElement, htmlContent );
|
|
147
|
+
|
|
148
|
+
// Unwrap the custom element content (it was stored in the attribute as the whole custom element).
|
|
149
|
+
// See the upcast conversion for the "htmlContent" attribute to learn more.
|
|
150
|
+
const customElement = domElement.firstChild;
|
|
151
|
+
|
|
152
|
+
customElement.remove();
|
|
153
|
+
|
|
154
|
+
while ( customElement.firstChild ) {
|
|
155
|
+
domElement.appendChild( customElement.firstChild );
|
|
156
|
+
}
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
if ( modelElement.hasAttribute( 'htmlAttributes' ) ) {
|
|
160
|
+
setViewAttributes( writer, modelElement.getAttribute( 'htmlAttributes' ), viewElement );
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return viewElement;
|
|
164
|
+
}
|
|
165
|
+
} );
|
|
166
|
+
} );
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Returns true if name is valid for a DOM element name.
|
|
171
|
+
function isValidElementName( name ) {
|
|
172
|
+
try {
|
|
173
|
+
document.createElement( name );
|
|
174
|
+
} catch ( error ) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
@@ -14,7 +14,7 @@ import { setViewAttributes } from '../conversionutils.js';
|
|
|
14
14
|
import DataFilter from '../datafilter';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Provides the General HTML Support integration with {@link module:list/documentlist~DocumentList Document List} feature.
|
|
17
|
+
* Provides the General HTML Support integration with the {@link module:list/documentlist~DocumentList Document List} feature.
|
|
18
18
|
*
|
|
19
19
|
* @extends module:core/plugin~Plugin
|
|
20
20
|
*/
|
|
@@ -26,6 +26,13 @@ export default class DocumentListElementSupport extends Plugin {
|
|
|
26
26
|
return [ DataFilter ];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @inheritDoc
|
|
31
|
+
*/
|
|
32
|
+
static get pluginName() {
|
|
33
|
+
return 'DocumentListElementSupport';
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
/**
|
|
30
37
|
* @inheritDoc
|
|
31
38
|
*/
|
|
@@ -85,7 +92,7 @@ export default class DocumentListElementSupport extends Plugin {
|
|
|
85
92
|
} );
|
|
86
93
|
|
|
87
94
|
// Make sure that all items in a single list (items at the same level & listType) have the same properties.
|
|
88
|
-
// Note: This is almost exact copy from DocumentListPropertiesEditing.
|
|
95
|
+
// Note: This is almost an exact copy from DocumentListPropertiesEditing.
|
|
89
96
|
documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => {
|
|
90
97
|
const previousNodesByIndent = []; // Last seen nodes of lower indented lists.
|
|
91
98
|
|
|
@@ -180,7 +187,7 @@ function viewToModelListAttributeConverter( attributeName, dataFilter ) {
|
|
|
180
187
|
Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
|
|
181
188
|
}
|
|
182
189
|
|
|
183
|
-
const viewAttributes = dataFilter.
|
|
190
|
+
const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
|
|
184
191
|
|
|
185
192
|
for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
|
|
186
193
|
// Apply only to list item blocks.
|
|
@@ -43,6 +43,13 @@ export default class DualContentModelElementSupport extends Plugin {
|
|
|
43
43
|
return [ DataFilter ];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* @inheritDoc
|
|
48
|
+
*/
|
|
49
|
+
static get pluginName() {
|
|
50
|
+
return 'DualContentModelElementSupport';
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
/**
|
|
47
54
|
* @inheritDoc
|
|
48
55
|
*/
|
|
@@ -111,10 +118,19 @@ export default class DualContentModelElementSupport extends Plugin {
|
|
|
111
118
|
* @returns {Boolean}
|
|
112
119
|
*/
|
|
113
120
|
_hasBlockContent( viewElement ) {
|
|
114
|
-
const
|
|
121
|
+
const view = this.editor.editing.view;
|
|
122
|
+
const blockElements = view.domConverter.blockElements;
|
|
123
|
+
|
|
124
|
+
// Traversing the viewElement subtree looking for block elements.
|
|
125
|
+
// Especially for the cases like <div><a href="#"><p>foo</p></a></div>.
|
|
126
|
+
// https://github.com/ckeditor/ckeditor5/issues/11513
|
|
127
|
+
for ( const viewItem of view.createRangeIn( viewElement ).getItems() ) {
|
|
128
|
+
if ( viewItem.is( 'element' ) && blockElements.includes( viewItem.name ) ) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
115
132
|
|
|
116
|
-
return
|
|
117
|
-
.some( node => blockElements.includes( node.name ) );
|
|
133
|
+
return false;
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
/**
|