]> git.immae.eu Git - perso/Immae/Projets/packagist/connexionswing-ckeditor-component.git/blame - sources/core/htmlparser/element.js
Upgrade to 4.5.7 and add some plugin
[perso/Immae/Projets/packagist/connexionswing-ckeditor-component.git] / sources / core / htmlparser / element.js
CommitLineData
3b35bd27
IB
1/**
2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
7adcb81e
IB
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8/**
9 * A lightweight representation of an HTML element.
10 *
11 * @class
12 * @extends CKEDITOR.htmlParser.node
13 * @constructor Creates an element class instance.
14 * @param {String} name The element name.
15 * @param {Object} attributes An object storing all attributes defined for
16 * this element.
17 */
18CKEDITOR.htmlParser.element = function( name, attributes ) {
19 /**
20 * The element name.
21 *
22 * @property {String}
23 */
24 this.name = name;
25
26 /**
27 * Stores the attributes defined for this element.
28 *
29 * @property {Object}
30 */
31 this.attributes = attributes || {};
32
33 /**
34 * The nodes that are direct children of this element.
35 */
36 this.children = [];
37
38 // Reveal the real semantic of our internal custom tag name (#6639),
39 // when resolving whether it's block like.
40 var realName = name || '',
41 prefixed = realName.match( /^cke:(.*)/ );
42 prefixed && ( realName = prefixed[ 1 ] );
43
44 var isBlockLike = !!( CKEDITOR.dtd.$nonBodyContent[ realName ] || CKEDITOR.dtd.$block[ realName ] ||
45 CKEDITOR.dtd.$listItem[ realName ] || CKEDITOR.dtd.$tableContent[ realName ] ||
46 CKEDITOR.dtd.$nonEditable[ realName ] || realName == 'br' );
47
48 this.isEmpty = !!CKEDITOR.dtd.$empty[ name ];
49 this.isUnknown = !CKEDITOR.dtd[ name ];
50
51 /** @private */
52 this._ = {
53 isBlockLike: isBlockLike,
54 hasInlineStarted: this.isEmpty || !isBlockLike
55 };
56};
57
58/**
59 * Object presentation of CSS style declaration text.
60 *
61 * @class
62 * @constructor Creates a `cssStyle` class instance.
63 * @param {CKEDITOR.htmlParser.element/String} elementOrStyleText
64 * An HTML parser element or the inline style text.
65 */
66CKEDITOR.htmlParser.cssStyle = function() {
67 var styleText,
68 arg = arguments[ 0 ],
69 rules = {};
70
71 styleText = arg instanceof CKEDITOR.htmlParser.element ? arg.attributes.style : arg;
72
73 // html-encoded quote might be introduced by 'font-family'
74 // from MS-Word which confused the following regexp. e.g.
75 //'font-family: "Lucida, Console"'
76 // TODO reuse CSS methods from tools.
77 ( styleText || '' ).replace( /"/g, '"' ).replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value ) {
78 name == 'font-family' && ( value = value.replace( /["']/g, '' ) );
79 rules[ name.toLowerCase() ] = value;
80 } );
81
82 return {
83
84 rules: rules,
85
86 /**
87 * Applies the styles to the specified element or object.
88 *
89 * @param {CKEDITOR.htmlParser.element/CKEDITOR.dom.element/Object} obj
90 */
91 populate: function( obj ) {
92 var style = this.toString();
93 if ( style )
94 obj instanceof CKEDITOR.dom.element ? obj.setAttribute( 'style', style ) : obj instanceof CKEDITOR.htmlParser.element ? obj.attributes.style = style : obj.style = style;
95
96 },
97
98 /**
99 * Serializes CSS style declaration to a string.
100 *
101 * @returns {String}
102 */
103 toString: function() {
104 var output = [];
105 for ( var i in rules )
106 rules[ i ] && output.push( i, ':', rules[ i ], ';' );
107 return output.join( '' );
108 }
109 };
110};
111
112/** @class CKEDITOR.htmlParser.element */
113( function() {
114 // Used to sort attribute entries in an array, where the first element of
115 // each object is the attribute name.
116 var sortAttribs = function( a, b ) {
117 a = a[ 0 ];
118 b = b[ 0 ];
119 return a < b ? -1 : a > b ? 1 : 0;
120 },
121 fragProto = CKEDITOR.htmlParser.fragment.prototype;
122
123 CKEDITOR.htmlParser.element.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
124 /**
125 * The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}.
126 *
127 * @readonly
128 * @property {Number} [=CKEDITOR.NODE_ELEMENT]
129 */
130 type: CKEDITOR.NODE_ELEMENT,
131
132 /**
133 * Adds a node to the element children list.
134 *
135 * @method
136 * @param {CKEDITOR.htmlParser.node} node The node to be added.
137 * @param {Number} [index] From where the insertion happens.
138 */
139 add: fragProto.add,
140
141 /**
142 * Clones this element.
143 *
144 * @returns {CKEDITOR.htmlParser.element} The element clone.
145 */
146 clone: function() {
147 return new CKEDITOR.htmlParser.element( this.name, this.attributes );
148 },
149
150 /**
151 * Filters this element and its children with the given filter.
152 *
153 * @since 4.1
154 * @param {CKEDITOR.htmlParser.filter} filter
155 * @returns {Boolean} The method returns `false` when this element has
156 * been removed or replaced with another. This information means that
157 * {@link #filterChildren} has to repeat the filter on the current
158 * position in parent's children array.
159 */
160 filter: function( filter, context ) {
161 var element = this,
162 originalName, name;
163
164 context = element.getFilterContext( context );
165
166 // Do not process elements with data-cke-processor attribute set to off.
167 if ( context.off )
168 return true;
169
170 // Filtering if it's the root node.
171 if ( !element.parent )
172 filter.onRoot( context, element );
173
174 while ( true ) {
175 originalName = element.name;
176
177 if ( !( name = filter.onElementName( context, originalName ) ) ) {
178 this.remove();
179 return false;
180 }
181
182 element.name = name;
183
184 if ( !( element = filter.onElement( context, element ) ) ) {
185 this.remove();
186 return false;
187 }
188
189 // New element has been returned - replace current one
190 // and process it (stop processing this and return false, what
191 // means that element has been removed).
192 if ( element !== this ) {
193 this.replaceWith( element );
194 return false;
195 }
196
197 // If name has been changed - continue loop, so in next iteration
198 // filters for new name will be applied to this element.
199 // If name hasn't been changed - stop.
200 if ( element.name == originalName )
201 break;
202
203 // If element has been replaced with something of a
204 // different type, then make the replacement filter itself.
205 if ( element.type != CKEDITOR.NODE_ELEMENT ) {
206 this.replaceWith( element );
207 return false;
208 }
209
210 // This indicate that the element has been dropped by
211 // filter but not the children.
212 if ( !element.name ) {
213 this.replaceWithChildren();
214 return false;
215 }
216 }
217
218 var attributes = element.attributes,
219 a, value, newAttrName;
220
221 for ( a in attributes ) {
222 newAttrName = a;
223 value = attributes[ a ];
224
225 // Loop until name isn't modified.
226 // A little bit senseless, but IE would do that anyway
227 // because it iterates with for-in loop even over properties
228 // created during its run.
229 while ( true ) {
230 if ( !( newAttrName = filter.onAttributeName( context, a ) ) ) {
231 delete attributes[ a ];
232 break;
233 } else if ( newAttrName != a ) {
234 delete attributes[ a ];
235 a = newAttrName;
236 continue;
237 } else {
238 break;
239 }
240 }
241
242 if ( newAttrName ) {
243 if ( ( value = filter.onAttribute( context, element, newAttrName, value ) ) === false )
244 delete attributes[ newAttrName ];
245 else
246 attributes[ newAttrName ] = value;
247 }
248 }
249
250 if ( !element.isEmpty )
251 this.filterChildren( filter, false, context );
252
253 return true;
254 },
255
256 /**
257 * Filters this element's children with the given filter.
258 *
259 * Element's children may only be filtered once by one
260 * instance of the filter.
261 *
262 * @method filterChildren
263 * @param {CKEDITOR.htmlParser.filter} filter
264 */
265 filterChildren: fragProto.filterChildren,
266
267 /**
268 * Writes the element HTML to the CKEDITOR.htmlWriter.
269 *
270 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which HTML will be written.
271 * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
272 * **Note:** It is unsafe to filter an offline (not appended) node.
273 */
274 writeHtml: function( writer, filter ) {
275 if ( filter )
276 this.filter( filter );
277
278 var name = this.name,
279 attribsArray = [],
280 attributes = this.attributes,
281 attrName,
282 attr, i, l;
283
284 // Open element tag.
285 writer.openTag( name, attributes );
286
287 // Copy all attributes to an array.
288 for ( attrName in attributes )
289 attribsArray.push( [ attrName, attributes[ attrName ] ] );
290
291 // Sort the attributes by name.
292 if ( writer.sortAttributes )
293 attribsArray.sort( sortAttribs );
294
295 // Send the attributes.
296 for ( i = 0, l = attribsArray.length; i < l; i++ ) {
297 attr = attribsArray[ i ];
298 writer.attribute( attr[ 0 ], attr[ 1 ] );
299 }
300
301 // Close the tag.
302 writer.openTagClose( name, this.isEmpty );
303
304 this.writeChildrenHtml( writer );
305
306 // Close the element.
307 if ( !this.isEmpty )
308 writer.closeTag( name );
309 },
310
311 /**
312 * Sends children of this element to the writer.
313 *
314 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which HTML will be written.
315 * @param {CKEDITOR.htmlParser.filter} [filter]
316 */
317 writeChildrenHtml: fragProto.writeChildrenHtml,
318
319 /**
320 * Replaces this element with its children.
321 *
322 * @since 4.1
323 */
324 replaceWithChildren: function() {
325 var children = this.children;
326
327 for ( var i = children.length; i; )
328 children[ --i ].insertAfter( this );
329
330 this.remove();
331 },
332
333 /**
334 * Executes a callback on each node (of the given type) in this element.
335 *
336 * // Create a <p> element with foo<b>bar</b>bom as its content.
337 * var elP = CKEDITOR.htmlParser.fragment.fromHtml( 'foo<b>bar</b>bom', 'p' );
338 * elP.forEach( function( node ) {
339 * console.log( node );
340 * } );
341 * // Will log:
342 * // 1. document fragment,
343 * // 2. <p> element,
344 * // 3. "foo" text node,
345 * // 4. <b> element,
346 * // 5. "bar" text node,
347 * // 6. "bom" text node.
348 *
349 * @since 4.1
350 * @param {Function} callback Function to be executed on every node.
351 * **Since 4.3**: If `callback` returned `false`, the descendants of the current node will be ignored.
352 * @param {CKEDITOR.htmlParser.node} callback.node Node passed as an argument.
353 * @param {Number} [type] Whether the specified `callback` will be executed only on nodes of this type.
354 * @param {Boolean} [skipRoot] Do not execute `callback` on this element.
355 */
356 forEach: fragProto.forEach,
357
358 /**
359 * Gets this element's first child. If `condition` is given, this method returns
360 * the first child which satisfies that condition.
361 *
362 * @since 4.3
363 * @param {String/Object/Function} condition Name of a child, a hash of names, or a validator function.
364 * @returns {CKEDITOR.htmlParser.node}
365 */
366 getFirst: function( condition ) {
367 if ( !condition )
368 return this.children.length ? this.children[ 0 ] : null;
369
370 if ( typeof condition != 'function' )
371 condition = nameCondition( condition );
372
373 for ( var i = 0, l = this.children.length; i < l; ++i ) {
374 if ( condition( this.children[ i ] ) )
375 return this.children[ i ];
376 }
377 return null;
378 },
379
380 /**
381 * Gets this element's inner HTML.
382 *
383 * @since 4.3
384 * @returns {String}
385 */
386 getHtml: function() {
387 var writer = new CKEDITOR.htmlParser.basicWriter();
388 this.writeChildrenHtml( writer );
389 return writer.getHtml();
390 },
391
392 /**
393 * Sets this element's inner HTML.
394 *
395 * @since 4.3
396 * @param {String} html
397 */
398 setHtml: function( html ) {
399 var children = this.children = CKEDITOR.htmlParser.fragment.fromHtml( html ).children;
400
401 for ( var i = 0, l = children.length; i < l; ++i )
402 children[ i ].parent = this;
403 },
404
405 /**
406 * Gets this element's outer HTML.
407 *
408 * @since 4.3
409 * @returns {String}
410 */
411 getOuterHtml: function() {
412 var writer = new CKEDITOR.htmlParser.basicWriter();
413 this.writeHtml( writer );
414 return writer.getHtml();
415 },
416
417 /**
418 * Splits this element at the given index.
419 *
420 * @since 4.3
421 * @param {Number} index Index at which the element will be split &mdash; `0` means the beginning,
422 * `1` after first child node, etc.
423 * @returns {CKEDITOR.htmlParser.element} The new element following this one.
424 */
425 split: function( index ) {
426 var cloneChildren = this.children.splice( index, this.children.length - index ),
427 clone = this.clone();
428
429 for ( var i = 0; i < cloneChildren.length; ++i )
430 cloneChildren[ i ].parent = clone;
431
432 clone.children = cloneChildren;
433
434 if ( cloneChildren[ 0 ] )
435 cloneChildren[ 0 ].previous = null;
436
437 if ( index > 0 )
438 this.children[ index - 1 ].next = null;
439
440 this.parent.add( clone, this.getIndex() + 1 );
441
442 return clone;
443 },
444
445 /**
446 * Adds a class name to the list of classes.
447 *
448 * @since 4.4
449 * @param {String} className The class name to be added.
450 */
451 addClass: function( className ) {
452 if ( this.hasClass( className ) )
453 return;
454
455 var c = this.attributes[ 'class' ] || '';
456
457 this.attributes[ 'class' ] = c + ( c ? ' ' : '' ) + className;
458 },
459
460 /**
461 * Removes a class name from the list of classes.
462 *
463 * @since 4.3
464 * @param {String} className The class name to be removed.
465 */
466 removeClass: function( className ) {
467 var classes = this.attributes[ 'class' ];
468
469 if ( !classes )
470 return;
471
472 // We can safely assume that className won't break regexp.
473 // http://stackoverflow.com/questions/448981/what-characters-are-valid-in-css-class-names
474 classes = CKEDITOR.tools.trim( classes.replace( new RegExp( '(?:\\s+|^)' + className + '(?:\\s+|$)' ), ' ' ) );
475
476 if ( classes )
477 this.attributes[ 'class' ] = classes;
478 else
479 delete this.attributes[ 'class' ];
480 },
481
482 /**
483 * Checkes whether this element has a class name.
484 *
485 * @since 4.3
486 * @param {String} className The class name to be checked.
487 * @returns {Boolean} Whether this element has a `className`.
488 */
489 hasClass: function( className ) {
490 var classes = this.attributes[ 'class' ];
491
492 if ( !classes )
493 return false;
494
495 return ( new RegExp( '(?:^|\\s)' + className + '(?=\\s|$)' ) ).test( classes );
496 },
497
498 getFilterContext: function( ctx ) {
499 var changes = [];
500
501 if ( !ctx ) {
502 ctx = {
503 off: false,
504 nonEditable: false,
505 nestedEditable: false
506 };
507 }
508
509 if ( !ctx.off && this.attributes[ 'data-cke-processor' ] == 'off' )
510 changes.push( 'off', true );
511
512 if ( !ctx.nonEditable && this.attributes.contenteditable == 'false' )
513 changes.push( 'nonEditable', true );
514 // A context to be given nestedEditable must be nonEditable first (by inheritance) (#11372, #11698).
515 // Special case: #11504 - filter starts on <body contenteditable=true>,
516 // so ctx.nonEditable has not been yet set to true.
517 else if ( ctx.nonEditable && !ctx.nestedEditable && this.attributes.contenteditable == 'true' )
518 changes.push( 'nestedEditable', true );
519
520 if ( changes.length ) {
521 ctx = CKEDITOR.tools.copy( ctx );
522 for ( var i = 0; i < changes.length; i += 2 )
523 ctx[ changes[ i ] ] = changes[ i + 1 ];
524 }
525
526 return ctx;
527 }
528 }, true );
529
530 function nameCondition( condition ) {
531 return function( el ) {
532 return el.type == CKEDITOR.NODE_ELEMENT &&
533 ( typeof condition == 'string' ? el.name == condition : el.name in condition );
534 };
535 }
536} )();