@ckeditor/ckeditor5-html-support 32.0.0 → 34.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +2 -2
- package/README.md +2 -1
- package/build/html-support.js +2 -2
- package/build/translations/el.js +1 -0
- package/build/translations/en-au.js +1 -0
- package/build/translations/hr.js +1 -0
- package/build/translations/jv.js +1 -0
- package/build/translations/lv.js +1 -0
- package/build/translations/ur.js +1 -0
- package/lang/translations/el.po +21 -0
- package/lang/translations/en-au.po +21 -0
- package/lang/translations/hr.po +21 -0
- package/lang/translations/jv.po +21 -0
- package/lang/translations/lv.po +21 -0
- package/lang/translations/ur.po +21 -0
- package/package.json +31 -31
- package/src/conversionutils.js +48 -5
- package/src/converters.js +41 -22
- package/src/datafilter.js +141 -17
- package/src/dataschema.js +3 -0
- package/src/generalhtmlsupport.js +231 -1
- package/src/integrations/codeblock.js +6 -4
- package/src/integrations/documentlist.js +200 -0
- package/src/integrations/dualcontent.js +12 -3
- package/src/integrations/image.js +57 -27
- package/src/integrations/mediaembed.js +32 -8
- package/src/integrations/script.js +2 -1
- package/src/integrations/style.js +74 -0
- package/src/integrations/table.js +29 -7
- package/src/schemadefinitions.js +67 -67
- package/theme/datafilter.css +5 -0
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Plugin } from 'ckeditor5/src/core';
|
|
11
|
+
import { toArray } from 'ckeditor5/src/utils';
|
|
11
12
|
|
|
12
13
|
import DataFilter from './datafilter';
|
|
13
14
|
import CodeBlockElementSupport from './integrations/codeblock';
|
|
@@ -17,6 +18,8 @@ import ImageElementSupport from './integrations/image';
|
|
|
17
18
|
import MediaEmbedElementSupport from './integrations/mediaembed';
|
|
18
19
|
import ScriptElementSupport from './integrations/script';
|
|
19
20
|
import TableElementSupport from './integrations/table';
|
|
21
|
+
import StyleElementSupport from './integrations/style';
|
|
22
|
+
import DocumentListElementSupport from './integrations/documentlist';
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* The General HTML Support feature.
|
|
@@ -46,7 +49,9 @@ export default class GeneralHtmlSupport extends Plugin {
|
|
|
46
49
|
ImageElementSupport,
|
|
47
50
|
MediaEmbedElementSupport,
|
|
48
51
|
ScriptElementSupport,
|
|
49
|
-
TableElementSupport
|
|
52
|
+
TableElementSupport,
|
|
53
|
+
StyleElementSupport,
|
|
54
|
+
DocumentListElementSupport
|
|
50
55
|
];
|
|
51
56
|
}
|
|
52
57
|
|
|
@@ -61,6 +66,231 @@ export default class GeneralHtmlSupport extends Plugin {
|
|
|
61
66
|
dataFilter.loadAllowedConfig( editor.config.get( 'htmlSupport.allow' ) || [] );
|
|
62
67
|
dataFilter.loadDisallowedConfig( editor.config.get( 'htmlSupport.disallow' ) || [] );
|
|
63
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns a GHS model attribute name related to a given view element name.
|
|
72
|
+
*
|
|
73
|
+
* @protected
|
|
74
|
+
* @param {String} viewElementName A view element name.
|
|
75
|
+
* @returns {String}
|
|
76
|
+
*/
|
|
77
|
+
getGhsAttributeNameForElement( viewElementName ) {
|
|
78
|
+
const dataSchema = this.editor.plugins.get( 'DataSchema' );
|
|
79
|
+
const definitions = Array.from( dataSchema.getDefinitionsForView( viewElementName, false ) );
|
|
80
|
+
|
|
81
|
+
if ( definitions && definitions.length && definitions[ 0 ].isInline && !definitions[ 0 ].isObject ) {
|
|
82
|
+
return definitions[ 0 ].model;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return 'htmlAttributes';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Updates GHS model attribute for a specified view element name, so it includes the given class name.
|
|
90
|
+
*
|
|
91
|
+
* @protected
|
|
92
|
+
* @param {String} viewElementName A view element name.
|
|
93
|
+
* @param {String|Array.<String>} className The css class to add.
|
|
94
|
+
* @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
|
|
95
|
+
*/
|
|
96
|
+
addModelHtmlClass( viewElementName, className, selectable ) {
|
|
97
|
+
const model = this.editor.model;
|
|
98
|
+
const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );
|
|
99
|
+
|
|
100
|
+
model.change( writer => {
|
|
101
|
+
for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
|
|
102
|
+
modifyGhsAttribute( writer, item, ghsAttributeName, 'classes', classes => {
|
|
103
|
+
for ( const value of toArray( className ) ) {
|
|
104
|
+
classes.add( value );
|
|
105
|
+
}
|
|
106
|
+
} );
|
|
107
|
+
}
|
|
108
|
+
} );
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Updates GHS model attribute for a specified view element name, so it does not include the given class name.
|
|
113
|
+
*
|
|
114
|
+
* @protected
|
|
115
|
+
* @param {String} viewElementName A view element name.
|
|
116
|
+
* @param {String|Array.<String>} className The css class to remove.
|
|
117
|
+
* @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
|
|
118
|
+
*/
|
|
119
|
+
removeModelHtmlClass( viewElementName, className, selectable ) {
|
|
120
|
+
const model = this.editor.model;
|
|
121
|
+
const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );
|
|
122
|
+
|
|
123
|
+
model.change( writer => {
|
|
124
|
+
for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
|
|
125
|
+
modifyGhsAttribute( writer, item, ghsAttributeName, 'classes', classes => {
|
|
126
|
+
for ( const value of toArray( className ) ) {
|
|
127
|
+
classes.delete( value );
|
|
128
|
+
}
|
|
129
|
+
} );
|
|
130
|
+
}
|
|
131
|
+
} );
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Updates GHS model attribute for a specified view element name, so it includes the given attribute.
|
|
136
|
+
*
|
|
137
|
+
* @protected
|
|
138
|
+
* @param {String} viewElementName A view element name.
|
|
139
|
+
* @param {Object} attributes The object with attributes to set.
|
|
140
|
+
* @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
|
|
141
|
+
*/
|
|
142
|
+
setModelHtmlAttributes( viewElementName, attributes, selectable ) {
|
|
143
|
+
const model = this.editor.model;
|
|
144
|
+
const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );
|
|
145
|
+
|
|
146
|
+
model.change( writer => {
|
|
147
|
+
for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
|
|
148
|
+
modifyGhsAttribute( writer, item, ghsAttributeName, 'attributes', attributesMap => {
|
|
149
|
+
for ( const [ key, value ] of Object.entries( attributes ) ) {
|
|
150
|
+
attributesMap.set( key, value );
|
|
151
|
+
}
|
|
152
|
+
} );
|
|
153
|
+
}
|
|
154
|
+
} );
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Updates GHS model attribute for a specified view element name, so it does not include the given attribute.
|
|
159
|
+
*
|
|
160
|
+
* @protected
|
|
161
|
+
* @param {String} viewElementName A view element name.
|
|
162
|
+
* @param {String|Array.<String>} attributeName The attribute name (or names) to remove.
|
|
163
|
+
* @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
|
|
164
|
+
*/
|
|
165
|
+
removeModelHtmlAttributes( viewElementName, attributeName, selectable ) {
|
|
166
|
+
const model = this.editor.model;
|
|
167
|
+
const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );
|
|
168
|
+
|
|
169
|
+
model.change( writer => {
|
|
170
|
+
for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
|
|
171
|
+
modifyGhsAttribute( writer, item, ghsAttributeName, 'attributes', attributesMap => {
|
|
172
|
+
for ( const key of toArray( attributeName ) ) {
|
|
173
|
+
attributesMap.delete( key );
|
|
174
|
+
}
|
|
175
|
+
} );
|
|
176
|
+
}
|
|
177
|
+
} );
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Updates GHS model attribute for a specified view element name, so it includes a given style.
|
|
182
|
+
*
|
|
183
|
+
* @protected
|
|
184
|
+
* @param {String} viewElementName A view element name.
|
|
185
|
+
* @param {Object} styles The object with styles to set.
|
|
186
|
+
* @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
|
|
187
|
+
*/
|
|
188
|
+
setModelHtmlStyles( viewElementName, styles, selectable ) {
|
|
189
|
+
const model = this.editor.model;
|
|
190
|
+
const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );
|
|
191
|
+
|
|
192
|
+
model.change( writer => {
|
|
193
|
+
for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
|
|
194
|
+
modifyGhsAttribute( writer, item, ghsAttributeName, 'styles', stylesMap => {
|
|
195
|
+
for ( const [ key, value ] of Object.entries( styles ) ) {
|
|
196
|
+
stylesMap.set( key, value );
|
|
197
|
+
}
|
|
198
|
+
} );
|
|
199
|
+
}
|
|
200
|
+
} );
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Updates GHS model attribute for a specified view element name, so it does not include a given style.
|
|
205
|
+
*
|
|
206
|
+
* @protected
|
|
207
|
+
* @param {String} viewElementName A view element name.
|
|
208
|
+
* @param {String|Array.<String>} properties The style (or styles list) to remove.
|
|
209
|
+
* @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
|
|
210
|
+
*/
|
|
211
|
+
removeModelHtmlStyles( viewElementName, properties, selectable ) {
|
|
212
|
+
const model = this.editor.model;
|
|
213
|
+
const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );
|
|
214
|
+
|
|
215
|
+
model.change( writer => {
|
|
216
|
+
for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
|
|
217
|
+
modifyGhsAttribute( writer, item, ghsAttributeName, 'styles', stylesMap => {
|
|
218
|
+
for ( const key of toArray( properties ) ) {
|
|
219
|
+
stylesMap.delete( key );
|
|
220
|
+
}
|
|
221
|
+
} );
|
|
222
|
+
}
|
|
223
|
+
} );
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Returns an iterator over an items in the selectable that accept given GHS attribute.
|
|
228
|
+
function* getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) {
|
|
229
|
+
if ( selectable.is( 'documentSelection' ) && selectable.isCollapsed ) {
|
|
230
|
+
if ( model.schema.checkAttributeInSelection( selectable, ghsAttributeName ) ) {
|
|
231
|
+
yield selectable;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
for ( const range of getValidRangesForSelectable( model, selectable, ghsAttributeName ) ) {
|
|
235
|
+
yield* range.getItems( { shallow: true } );
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Translates a given selectable to an iterable of ranges.
|
|
241
|
+
function getValidRangesForSelectable( model, selectable, ghsAttributeName ) {
|
|
242
|
+
if ( selectable.is( 'node' ) || selectable.is( '$text' ) || selectable.is( '$textProxy' ) ) {
|
|
243
|
+
if ( model.schema.checkAttribute( selectable, ghsAttributeName ) ) {
|
|
244
|
+
return [ model.createRangeOn( selectable ) ];
|
|
245
|
+
} else {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
return model.schema.getValidRanges( model.createSelection( selectable ).getRanges(), ghsAttributeName );
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Updates a GHS attribute on a specified item.
|
|
254
|
+
// @param {module:engine/model/writer~Writer} writer
|
|
255
|
+
// @param {module:engine/model/item~Item|module:engine/model/documentselection~DocumentSelection} item
|
|
256
|
+
// @param {String} ghsAttributeName
|
|
257
|
+
// @param {'classes'|'attributes'|'styles'} subject
|
|
258
|
+
// @param {Function} callback That receives a map or set as an argument and should modify it (add or remove entries).
|
|
259
|
+
function modifyGhsAttribute( writer, item, ghsAttributeName, subject, callback ) {
|
|
260
|
+
const oldValue = item.getAttribute( ghsAttributeName );
|
|
261
|
+
const newValue = {};
|
|
262
|
+
|
|
263
|
+
for ( const kind of [ 'attributes', 'styles', 'classes' ] ) {
|
|
264
|
+
if ( kind != subject ) {
|
|
265
|
+
if ( oldValue && oldValue[ kind ] ) {
|
|
266
|
+
newValue[ kind ] = oldValue[ kind ];
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
const values = kind == 'classes' ?
|
|
270
|
+
new Set( oldValue && oldValue[ kind ] || [] ) :
|
|
271
|
+
new Map( Object.entries( oldValue && oldValue[ kind ] || {} ) );
|
|
272
|
+
|
|
273
|
+
callback( values );
|
|
274
|
+
|
|
275
|
+
if ( values.size ) {
|
|
276
|
+
newValue[ kind ] = kind == 'classes' ? Array.from( values ) : Object.fromEntries( values );
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if ( Object.keys( newValue ).length ) {
|
|
282
|
+
if ( item.is( 'documentSelection' ) ) {
|
|
283
|
+
writer.setSelectionAttribute( ghsAttributeName, newValue );
|
|
284
|
+
} else {
|
|
285
|
+
writer.setAttribute( ghsAttributeName, newValue, item );
|
|
286
|
+
}
|
|
287
|
+
} else if ( oldValue ) {
|
|
288
|
+
if ( item.is( 'documentSelection' ) ) {
|
|
289
|
+
writer.removeSelectionAttribute( ghsAttributeName );
|
|
290
|
+
} else {
|
|
291
|
+
writer.removeAttribute( ghsAttributeName, item );
|
|
292
|
+
}
|
|
293
|
+
}
|
|
64
294
|
}
|
|
65
295
|
|
|
66
296
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Plugin } from 'ckeditor5/src/core';
|
|
11
|
-
import {
|
|
11
|
+
import { updateViewAttributes } from '../conversionutils.js';
|
|
12
12
|
|
|
13
13
|
import DataFilter from '../datafilter';
|
|
14
14
|
|
|
@@ -79,7 +79,7 @@ function viewToModelCodeBlockAttributeConverter( dataFilter ) {
|
|
|
79
79
|
preserveElementAttributes( viewCodeElement, 'htmlContentAttributes' );
|
|
80
80
|
|
|
81
81
|
function preserveElementAttributes( viewElement, attributeName ) {
|
|
82
|
-
const viewAttributes = dataFilter.
|
|
82
|
+
const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
|
|
83
83
|
|
|
84
84
|
if ( viewAttributes ) {
|
|
85
85
|
conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
|
|
@@ -101,10 +101,11 @@ function modelToViewCodeBlockAttributeConverter() {
|
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
const { attributeOldValue, attributeNewValue } = data;
|
|
104
105
|
const viewCodeElement = conversionApi.mapper.toViewElement( data.item );
|
|
105
106
|
const viewPreElement = viewCodeElement.parent;
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
updateViewAttributes( conversionApi.writer, attributeOldValue, attributeNewValue, viewPreElement );
|
|
108
109
|
} );
|
|
109
110
|
|
|
110
111
|
dispatcher.on( 'attribute:htmlContentAttributes:codeBlock', ( evt, data, conversionApi ) => {
|
|
@@ -112,9 +113,10 @@ function modelToViewCodeBlockAttributeConverter() {
|
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
const { attributeOldValue, attributeNewValue } = data;
|
|
115
117
|
const viewCodeElement = conversionApi.mapper.toViewElement( data.item );
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
updateViewAttributes( conversionApi.writer, attributeOldValue, attributeNewValue, viewCodeElement );
|
|
118
120
|
} );
|
|
119
121
|
};
|
|
120
122
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
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/documentlist
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { isEqual } from 'lodash-es';
|
|
11
|
+
import { Plugin } from 'ckeditor5/src/core';
|
|
12
|
+
import { setViewAttributes } from '../conversionutils.js';
|
|
13
|
+
|
|
14
|
+
import DataFilter from '../datafilter';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Provides the General HTML Support integration with the {@link module:list/documentlist~DocumentList Document List} feature.
|
|
18
|
+
*
|
|
19
|
+
* @extends module:core/plugin~Plugin
|
|
20
|
+
*/
|
|
21
|
+
export default class DocumentListElementSupport extends Plugin {
|
|
22
|
+
/**
|
|
23
|
+
* @inheritDoc
|
|
24
|
+
*/
|
|
25
|
+
static get requires() {
|
|
26
|
+
return [ DataFilter ];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @inheritDoc
|
|
31
|
+
*/
|
|
32
|
+
init() {
|
|
33
|
+
const editor = this.editor;
|
|
34
|
+
|
|
35
|
+
if ( !editor.plugins.has( 'DocumentListEditing' ) ) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const schema = editor.model.schema;
|
|
40
|
+
const conversion = editor.conversion;
|
|
41
|
+
const dataFilter = editor.plugins.get( DataFilter );
|
|
42
|
+
const documentListEditing = editor.plugins.get( 'DocumentListEditing' );
|
|
43
|
+
|
|
44
|
+
// Register downcast strategy.
|
|
45
|
+
// Note that this must be done before document list editing registers conversion in afterInit.
|
|
46
|
+
documentListEditing.registerDowncastStrategy( {
|
|
47
|
+
scope: 'item',
|
|
48
|
+
attributeName: 'htmlLiAttributes',
|
|
49
|
+
|
|
50
|
+
setAttributeOnDowncast( writer, attributeValue, viewElement ) {
|
|
51
|
+
setViewAttributes( writer, attributeValue, viewElement );
|
|
52
|
+
}
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
documentListEditing.registerDowncastStrategy( {
|
|
56
|
+
scope: 'list',
|
|
57
|
+
attributeName: 'htmlListAttributes',
|
|
58
|
+
|
|
59
|
+
setAttributeOnDowncast( writer, viewAttributes, viewElement ) {
|
|
60
|
+
setViewAttributes( writer, viewAttributes, viewElement );
|
|
61
|
+
}
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
dataFilter.on( 'register', ( evt, definition ) => {
|
|
65
|
+
if ( ![ 'ul', 'ol', 'li' ].includes( definition.view ) ) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
evt.stop();
|
|
70
|
+
|
|
71
|
+
// Do not register same converters twice.
|
|
72
|
+
if ( schema.checkAttribute( '$block', 'htmlListAttributes' ) ) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
schema.extend( '$block', { allowAttributes: [ 'htmlListAttributes', 'htmlLiAttributes' ] } );
|
|
77
|
+
schema.extend( '$blockObject', { allowAttributes: [ 'htmlListAttributes', 'htmlLiAttributes' ] } );
|
|
78
|
+
schema.extend( '$container', { allowAttributes: [ 'htmlListAttributes', 'htmlLiAttributes' ] } );
|
|
79
|
+
|
|
80
|
+
conversion.for( 'upcast' ).add( dispatcher => {
|
|
81
|
+
dispatcher.on( 'element:ul', viewToModelListAttributeConverter( 'htmlListAttributes', dataFilter ), { priority: 'low' } );
|
|
82
|
+
dispatcher.on( 'element:ol', viewToModelListAttributeConverter( 'htmlListAttributes', dataFilter ), { priority: 'low' } );
|
|
83
|
+
dispatcher.on( 'element:li', viewToModelListAttributeConverter( 'htmlLiAttributes', dataFilter ), { priority: 'low' } );
|
|
84
|
+
} );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
// Make sure that all items in a single list (items at the same level & listType) have the same properties.
|
|
88
|
+
// Note: This is almost an exact copy from DocumentListPropertiesEditing.
|
|
89
|
+
documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => {
|
|
90
|
+
const previousNodesByIndent = []; // Last seen nodes of lower indented lists.
|
|
91
|
+
|
|
92
|
+
for ( const { node, previous } of listNodes ) {
|
|
93
|
+
// For the first list block there is nothing to compare with.
|
|
94
|
+
if ( !previous ) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const nodeIndent = node.getAttribute( 'listIndent' );
|
|
99
|
+
const previousNodeIndent = previous.getAttribute( 'listIndent' );
|
|
100
|
+
|
|
101
|
+
let previousNodeInList = null; // It's like `previous` but has the same indent as current node.
|
|
102
|
+
|
|
103
|
+
// Let's find previous node for the same indent.
|
|
104
|
+
// We're going to need that when we get back to previous indent.
|
|
105
|
+
if ( nodeIndent > previousNodeIndent ) {
|
|
106
|
+
previousNodesByIndent[ previousNodeIndent ] = previous;
|
|
107
|
+
}
|
|
108
|
+
// Restore the one for given indent.
|
|
109
|
+
else if ( nodeIndent < previousNodeIndent ) {
|
|
110
|
+
previousNodeInList = previousNodesByIndent[ nodeIndent ];
|
|
111
|
+
previousNodesByIndent.length = nodeIndent;
|
|
112
|
+
}
|
|
113
|
+
// Same indent.
|
|
114
|
+
else {
|
|
115
|
+
previousNodeInList = previous;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// This is a first item of a nested list.
|
|
119
|
+
if ( !previousNodeInList ) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if ( previousNodeInList.getAttribute( 'listType' ) == node.getAttribute( 'listType' ) ) {
|
|
124
|
+
const value = previousNodeInList.getAttribute( 'htmlListAttributes' );
|
|
125
|
+
|
|
126
|
+
if ( !isEqual( node.getAttribute( 'htmlListAttributes' ), value ) ) {
|
|
127
|
+
writer.setAttribute( 'htmlListAttributes', value, node );
|
|
128
|
+
evt.return = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if ( previousNodeInList.getAttribute( 'listItemId' ) == node.getAttribute( 'listItemId' ) ) {
|
|
133
|
+
const value = previousNodeInList.getAttribute( 'htmlLiAttributes' );
|
|
134
|
+
|
|
135
|
+
if ( !isEqual( node.getAttribute( 'htmlLiAttributes' ), value ) ) {
|
|
136
|
+
writer.setAttribute( 'htmlLiAttributes', value, node );
|
|
137
|
+
evt.return = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} );
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @inheritDoc
|
|
146
|
+
*/
|
|
147
|
+
afterInit() {
|
|
148
|
+
const editor = this.editor;
|
|
149
|
+
|
|
150
|
+
if ( !editor.commands.get( 'indentList' ) ) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Reset list attributes after indenting list items.
|
|
155
|
+
this.listenTo( editor.commands.get( 'indentList' ), 'afterExecute', ( evt, changedBlocks ) => {
|
|
156
|
+
editor.model.change( writer => {
|
|
157
|
+
for ( const node of changedBlocks ) {
|
|
158
|
+
// Just reset the attribute.
|
|
159
|
+
// If there is a previous indented list that this node should be merged into,
|
|
160
|
+
// the postfixer will unify all the attributes of both sub-lists.
|
|
161
|
+
writer.setAttribute( 'htmlListAttributes', {}, node );
|
|
162
|
+
}
|
|
163
|
+
} );
|
|
164
|
+
} );
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// View-to-model conversion helper preserving allowed attributes on {@link TODO}
|
|
169
|
+
// feature model element.
|
|
170
|
+
//
|
|
171
|
+
// @private
|
|
172
|
+
// @param {String} attributeName
|
|
173
|
+
// @param {module:html-support/datafilter~DataFilter} dataFilter
|
|
174
|
+
// @returns {Function} Returns a conversion callback.
|
|
175
|
+
function viewToModelListAttributeConverter( attributeName, dataFilter ) {
|
|
176
|
+
return ( evt, data, conversionApi ) => {
|
|
177
|
+
const viewElement = data.viewItem;
|
|
178
|
+
|
|
179
|
+
if ( !data.modelRange ) {
|
|
180
|
+
Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
|
|
184
|
+
|
|
185
|
+
for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
|
|
186
|
+
// Apply only to list item blocks.
|
|
187
|
+
if ( !item.hasAttribute( 'listItemId' ) ) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Set list attributes only on same level items, those nested deeper are already handled
|
|
192
|
+
// by the recursive conversion.
|
|
193
|
+
if ( item.hasAttribute( attributeName ) ) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
conversionApi.writer.setAttribute( attributeName, viewAttributes || {}, item );
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -111,10 +111,19 @@ export default class DualContentModelElementSupport extends Plugin {
|
|
|
111
111
|
* @returns {Boolean}
|
|
112
112
|
*/
|
|
113
113
|
_hasBlockContent( viewElement ) {
|
|
114
|
-
const
|
|
114
|
+
const view = this.editor.editing.view;
|
|
115
|
+
const blockElements = view.domConverter.blockElements;
|
|
116
|
+
|
|
117
|
+
// Traversing the viewElement subtree looking for block elements.
|
|
118
|
+
// Especially for the cases like <div><a href="#"><p>foo</p></a></div>.
|
|
119
|
+
// https://github.com/ckeditor/ckeditor5/issues/11513
|
|
120
|
+
for ( const viewItem of view.createRangeIn( viewElement ).getItems() ) {
|
|
121
|
+
if ( viewItem.is( 'element' ) && blockElements.includes( viewItem.name ) ) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
115
125
|
|
|
116
|
-
return
|
|
117
|
-
.some( node => blockElements.includes( node.name ) );
|
|
126
|
+
return false;
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
/**
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
import { Plugin } from 'ckeditor5/src/core';
|
|
11
11
|
|
|
12
12
|
import DataFilter from '../datafilter';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
setViewAttributes,
|
|
15
|
+
updateViewAttributes
|
|
16
|
+
} from '../conversionutils.js';
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Provides the General HTML Support integration with the {@link module:image/image~Image Image} feature.
|
|
@@ -40,6 +43,10 @@ export default class ImageElementSupport extends Plugin {
|
|
|
40
43
|
const conversion = editor.conversion;
|
|
41
44
|
const dataFilter = editor.plugins.get( DataFilter );
|
|
42
45
|
|
|
46
|
+
dataFilter.on( 'register:figure', () => {
|
|
47
|
+
conversion.for( 'upcast' ).add( viewToModelFigureAttributeConverter( dataFilter ) );
|
|
48
|
+
} );
|
|
49
|
+
|
|
43
50
|
dataFilter.on( 'register:img', ( evt, definition ) => {
|
|
44
51
|
if ( definition.model !== 'imageBlock' && definition.model !== 'imageInline' ) {
|
|
45
52
|
return;
|
|
@@ -84,36 +91,55 @@ export default class ImageElementSupport extends Plugin {
|
|
|
84
91
|
function viewToModelImageAttributeConverter( dataFilter ) {
|
|
85
92
|
return dispatcher => {
|
|
86
93
|
dispatcher.on( 'element:img', ( evt, data, conversionApi ) => {
|
|
94
|
+
if ( !data.modelRange ) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
const viewImageElement = data.viewItem;
|
|
88
99
|
const viewContainerElement = viewImageElement.parent;
|
|
89
100
|
|
|
90
101
|
preserveElementAttributes( viewImageElement, 'htmlAttributes' );
|
|
91
102
|
|
|
92
|
-
if ( viewContainerElement.is( 'element', '
|
|
93
|
-
preserveElementAttributes( viewContainerElement, 'htmlFigureAttributes' );
|
|
94
|
-
} else if ( viewContainerElement.is( 'element', 'a' ) ) {
|
|
103
|
+
if ( viewContainerElement.is( 'element', 'a' ) ) {
|
|
95
104
|
preserveLinkAttributes( viewContainerElement );
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
function preserveElementAttributes( viewElement, attributeName ) {
|
|
99
|
-
const viewAttributes = dataFilter.
|
|
108
|
+
const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
|
|
100
109
|
|
|
101
110
|
if ( viewAttributes ) {
|
|
102
111
|
conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
|
|
103
112
|
}
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
// For a block image, we want to preserve the attributes on our own.
|
|
107
|
-
// The inline image attributes will be handled by the GHS automatically.
|
|
108
115
|
function preserveLinkAttributes( viewContainerElement ) {
|
|
109
116
|
if ( data.modelRange && data.modelRange.getContainedElement().is( 'element', 'imageBlock' ) ) {
|
|
110
117
|
preserveElementAttributes( viewContainerElement, 'htmlLinkAttributes' );
|
|
111
118
|
}
|
|
119
|
+
}
|
|
120
|
+
}, { priority: 'low' } );
|
|
121
|
+
};
|
|
122
|
+
}
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
// View-to-model conversion helper preserving allowed attributes on {@link module:image/image~Image Image}
|
|
125
|
+
// feature model element from figure view element.
|
|
126
|
+
//
|
|
127
|
+
// @private
|
|
128
|
+
// @param {module:html-support/datafilter~DataFilter} dataFilter
|
|
129
|
+
// @returns {Function} Returns a conversion callback.
|
|
130
|
+
function viewToModelFigureAttributeConverter( dataFilter ) {
|
|
131
|
+
return dispatcher => {
|
|
132
|
+
dispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
|
|
133
|
+
const viewFigureElement = data.viewItem;
|
|
134
|
+
|
|
135
|
+
if ( !data.modelRange || !viewFigureElement.hasClass( 'image' ) ) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const viewAttributes = dataFilter.processViewAttributes( viewFigureElement, conversionApi );
|
|
140
|
+
|
|
141
|
+
if ( viewAttributes ) {
|
|
142
|
+
conversionApi.writer.setAttribute( 'htmlFigureAttributes', viewAttributes, data.modelRange );
|
|
117
143
|
}
|
|
118
144
|
}, { priority: 'low' } );
|
|
119
145
|
};
|
|
@@ -130,7 +156,7 @@ function modelToViewImageAttributeConverter() {
|
|
|
130
156
|
|
|
131
157
|
addBlockAttributeConversion( 'img', 'htmlAttributes' );
|
|
132
158
|
addBlockAttributeConversion( 'figure', 'htmlFigureAttributes' );
|
|
133
|
-
|
|
159
|
+
addBlockAttributeConversion( 'a', 'htmlLinkAttributes' );
|
|
134
160
|
|
|
135
161
|
function addInlineAttributeConversion( attributeName ) {
|
|
136
162
|
dispatcher.on( `attribute:${ attributeName }:imageInline`, ( evt, data, conversionApi ) => {
|
|
@@ -138,38 +164,42 @@ function modelToViewImageAttributeConverter() {
|
|
|
138
164
|
return;
|
|
139
165
|
}
|
|
140
166
|
|
|
167
|
+
const { attributeOldValue, attributeNewValue } = data;
|
|
141
168
|
const viewElement = conversionApi.mapper.toViewElement( data.item );
|
|
142
169
|
|
|
143
|
-
|
|
170
|
+
updateViewAttributes( conversionApi.writer, attributeOldValue, attributeNewValue, viewElement );
|
|
144
171
|
}, { priority: 'low' } );
|
|
145
172
|
}
|
|
146
173
|
|
|
147
174
|
function addBlockAttributeConversion( elementName, attributeName ) {
|
|
148
175
|
dispatcher.on( `attribute:${ attributeName }:imageBlock`, ( evt, data, conversionApi ) => {
|
|
149
|
-
if ( !conversionApi.consumable.
|
|
176
|
+
if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
|
|
150
177
|
return;
|
|
151
178
|
}
|
|
152
179
|
|
|
180
|
+
const { attributeOldValue, attributeNewValue } = data;
|
|
153
181
|
const containerElement = conversionApi.mapper.toViewElement( data.item );
|
|
154
182
|
const viewElement = getDescendantElement( conversionApi.writer, containerElement, elementName );
|
|
155
183
|
|
|
156
|
-
|
|
184
|
+
if ( viewElement ) {
|
|
185
|
+
updateViewAttributes( conversionApi.writer, attributeOldValue, attributeNewValue, viewElement );
|
|
186
|
+
conversionApi.consumable.consume( data.item, evt.name );
|
|
187
|
+
}
|
|
157
188
|
}, { priority: 'low' } );
|
|
158
|
-
}
|
|
159
189
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
190
|
+
if ( elementName === 'a' ) {
|
|
191
|
+
// To have a link element in the view, we need to attach a converter to the `linkHref` attribute as well.
|
|
192
|
+
dispatcher.on( 'attribute:linkHref:imageBlock', ( evt, data, conversionApi ) => {
|
|
193
|
+
if ( !conversionApi.consumable.consume( data.item, 'attribute:htmlLinkAttributes:imageBlock' ) ) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
167
196
|
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
const containerElement = conversionApi.mapper.toViewElement( data.item );
|
|
198
|
+
const viewElement = getDescendantElement( conversionApi.writer, containerElement, 'a' );
|
|
170
199
|
|
|
171
|
-
|
|
172
|
-
|
|
200
|
+
setViewAttributes( conversionApi.writer, data.item.getAttribute( 'htmlLinkAttributes' ), viewElement );
|
|
201
|
+
}, { priority: 'low' } );
|
|
202
|
+
}
|
|
173
203
|
}
|
|
174
204
|
};
|
|
175
205
|
}
|