diff options
Diffstat (limited to 'sources/core/dom/node.js')
-rw-r--r-- | sources/core/dom/node.js | 897 |
1 files changed, 897 insertions, 0 deletions
diff --git a/sources/core/dom/node.js b/sources/core/dom/node.js new file mode 100644 index 00000000..5d791319 --- /dev/null +++ b/sources/core/dom/node.js | |||
@@ -0,0 +1,897 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Defines the {@link CKEDITOR.dom.node} class which is the base | ||
8 | * class for classes that represent DOM nodes. | ||
9 | */ | ||
10 | |||
11 | /** | ||
12 | * Base class for classes representing DOM nodes. This constructor may return | ||
13 | * an instance of a class that inherits from this class, like | ||
14 | * {@link CKEDITOR.dom.element} or {@link CKEDITOR.dom.text}. | ||
15 | * | ||
16 | * @class | ||
17 | * @extends CKEDITOR.dom.domObject | ||
18 | * @constructor Creates a node class instance. | ||
19 | * @param {Object} domNode A native DOM node. | ||
20 | * @see CKEDITOR.dom.element | ||
21 | * @see CKEDITOR.dom.text | ||
22 | */ | ||
23 | CKEDITOR.dom.node = function( domNode ) { | ||
24 | if ( domNode ) { | ||
25 | var type = | ||
26 | domNode.nodeType == CKEDITOR.NODE_DOCUMENT ? 'document' : | ||
27 | domNode.nodeType == CKEDITOR.NODE_ELEMENT ? 'element' : | ||
28 | domNode.nodeType == CKEDITOR.NODE_TEXT ? 'text' : | ||
29 | domNode.nodeType == CKEDITOR.NODE_COMMENT ? 'comment' : | ||
30 | domNode.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ? 'documentFragment' : | ||
31 | 'domObject'; // Call the base constructor otherwise. | ||
32 | |||
33 | return new CKEDITOR.dom[ type ]( domNode ); | ||
34 | } | ||
35 | |||
36 | return this; | ||
37 | }; | ||
38 | |||
39 | CKEDITOR.dom.node.prototype = new CKEDITOR.dom.domObject(); | ||
40 | |||
41 | /** | ||
42 | * Element node type. | ||
43 | * | ||
44 | * @readonly | ||
45 | * @property {Number} [=1] | ||
46 | * @member CKEDITOR | ||
47 | */ | ||
48 | CKEDITOR.NODE_ELEMENT = 1; | ||
49 | |||
50 | /** | ||
51 | * Document node type. | ||
52 | * | ||
53 | * @readonly | ||
54 | * @property {Number} [=9] | ||
55 | * @member CKEDITOR | ||
56 | */ | ||
57 | CKEDITOR.NODE_DOCUMENT = 9; | ||
58 | |||
59 | /** | ||
60 | * Text node type. | ||
61 | * | ||
62 | * @readonly | ||
63 | * @property {Number} [=3] | ||
64 | * @member CKEDITOR | ||
65 | */ | ||
66 | CKEDITOR.NODE_TEXT = 3; | ||
67 | |||
68 | /** | ||
69 | * Comment node type. | ||
70 | * | ||
71 | * @readonly | ||
72 | * @property {Number} [=8] | ||
73 | * @member CKEDITOR | ||
74 | */ | ||
75 | CKEDITOR.NODE_COMMENT = 8; | ||
76 | |||
77 | /** | ||
78 | * Document fragment node type. | ||
79 | * | ||
80 | * @readonly | ||
81 | * @property {Number} [=11] | ||
82 | * @member CKEDITOR | ||
83 | */ | ||
84 | CKEDITOR.NODE_DOCUMENT_FRAGMENT = 11; | ||
85 | |||
86 | /** | ||
87 | * Indicates that positions of both nodes are identical (this is the same node). See {@link CKEDITOR.dom.node#getPosition}. | ||
88 | * | ||
89 | * @readonly | ||
90 | * @property {Number} [=0] | ||
91 | * @member CKEDITOR | ||
92 | */ | ||
93 | CKEDITOR.POSITION_IDENTICAL = 0; | ||
94 | |||
95 | /** | ||
96 | * Indicates that nodes are in different (detached) trees. See {@link CKEDITOR.dom.node#getPosition}. | ||
97 | * | ||
98 | * @readonly | ||
99 | * @property {Number} [=1] | ||
100 | * @member CKEDITOR | ||
101 | */ | ||
102 | CKEDITOR.POSITION_DISCONNECTED = 1; | ||
103 | |||
104 | /** | ||
105 | * Indicates that the context node follows the other node. See {@link CKEDITOR.dom.node#getPosition}. | ||
106 | * | ||
107 | * @readonly | ||
108 | * @property {Number} [=2] | ||
109 | * @member CKEDITOR | ||
110 | */ | ||
111 | CKEDITOR.POSITION_FOLLOWING = 2; | ||
112 | |||
113 | /** | ||
114 | * Indicates that the context node precedes the other node. See {@link CKEDITOR.dom.node#getPosition}. | ||
115 | * | ||
116 | * @readonly | ||
117 | * @property {Number} [=4] | ||
118 | * @member CKEDITOR | ||
119 | */ | ||
120 | CKEDITOR.POSITION_PRECEDING = 4; | ||
121 | |||
122 | /** | ||
123 | * Indicates that the context node is a descendant of the other node. See {@link CKEDITOR.dom.node#getPosition}. | ||
124 | * | ||
125 | * @readonly | ||
126 | * @property {Number} [=8] | ||
127 | * @member CKEDITOR | ||
128 | */ | ||
129 | CKEDITOR.POSITION_IS_CONTAINED = 8; | ||
130 | |||
131 | /** | ||
132 | * Indicates that the context node contains the other node. See {@link CKEDITOR.dom.node#getPosition}. | ||
133 | * | ||
134 | * @readonly | ||
135 | * @property {Number} [=16] | ||
136 | * @member CKEDITOR | ||
137 | */ | ||
138 | CKEDITOR.POSITION_CONTAINS = 16; | ||
139 | |||
140 | CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, { | ||
141 | /** | ||
142 | * Makes this node a child of another element. | ||
143 | * | ||
144 | * var p = new CKEDITOR.dom.element( 'p' ); | ||
145 | * var strong = new CKEDITOR.dom.element( 'strong' ); | ||
146 | * strong.appendTo( p ); | ||
147 | * | ||
148 | * // Result: '<p><strong></strong></p>'. | ||
149 | * | ||
150 | * @param {CKEDITOR.dom.element} element The target element to which this node will be appended. | ||
151 | * @returns {CKEDITOR.dom.element} The target element. | ||
152 | */ | ||
153 | appendTo: function( element, toStart ) { | ||
154 | element.append( this, toStart ); | ||
155 | return element; | ||
156 | }, | ||
157 | |||
158 | /** | ||
159 | * Clones this node. | ||
160 | * | ||
161 | * **Note**: Values set by {#setCustomData} will not be available in the clone. | ||
162 | * | ||
163 | * @param {Boolean} [includeChildren=false] If `true` then all node's | ||
164 | * children will be cloned recursively. | ||
165 | * @param {Boolean} [cloneId=false] Whether ID attributes should be cloned, too. | ||
166 | * @returns {CKEDITOR.dom.node} Clone of this node. | ||
167 | */ | ||
168 | clone: function( includeChildren, cloneId ) { | ||
169 | var $clone = this.$.cloneNode( includeChildren ); | ||
170 | |||
171 | // The "id" attribute should never be cloned to avoid duplication. | ||
172 | removeIds( $clone ); | ||
173 | |||
174 | var node = new CKEDITOR.dom.node( $clone ); | ||
175 | |||
176 | // On IE8 we need to fixed HTML5 node name, see details below. | ||
177 | if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && | ||
178 | ( this.type == CKEDITOR.NODE_ELEMENT || this.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) { | ||
179 | renameNodes( node ); | ||
180 | } | ||
181 | |||
182 | return node; | ||
183 | |||
184 | function removeIds( node ) { | ||
185 | // Reset data-cke-expando only when has been cloned (IE and only for some types of objects). | ||
186 | if ( node[ 'data-cke-expando' ] ) | ||
187 | node[ 'data-cke-expando' ] = false; | ||
188 | |||
189 | if ( node.nodeType != CKEDITOR.NODE_ELEMENT && node.nodeType != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) | ||
190 | return; | ||
191 | |||
192 | if ( !cloneId && node.nodeType == CKEDITOR.NODE_ELEMENT ) | ||
193 | node.removeAttribute( 'id', false ); | ||
194 | |||
195 | if ( includeChildren ) { | ||
196 | var childs = node.childNodes; | ||
197 | for ( var i = 0; i < childs.length; i++ ) | ||
198 | removeIds( childs[ i ] ); | ||
199 | } | ||
200 | } | ||
201 | |||
202 | // IE8 rename HTML5 nodes by adding `:` at the begging of the tag name when the node is cloned, | ||
203 | // so `<figure>` will be `<:figure>` after 'cloneNode'. We need to fix it (#13101). | ||
204 | function renameNodes( node ) { | ||
205 | if ( node.type != CKEDITOR.NODE_ELEMENT && node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) | ||
206 | return; | ||
207 | |||
208 | if ( node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) { | ||
209 | var name = node.getName(); | ||
210 | if ( name[ 0 ] == ':' ) { | ||
211 | node.renameNode( name.substring( 1 ) ); | ||
212 | } | ||
213 | } | ||
214 | |||
215 | if ( includeChildren ) { | ||
216 | for ( var i = 0; i < node.getChildCount(); i++ ) | ||
217 | renameNodes( node.getChild( i ) ); | ||
218 | } | ||
219 | } | ||
220 | }, | ||
221 | |||
222 | /** | ||
223 | * Checks if the node is preceded by any sibling. | ||
224 | * | ||
225 | * @returns {Boolean} | ||
226 | */ | ||
227 | hasPrevious: function() { | ||
228 | return !!this.$.previousSibling; | ||
229 | }, | ||
230 | |||
231 | /** | ||
232 | * Checks if the node is succeeded by any sibling. | ||
233 | * | ||
234 | * @returns {Boolean} | ||
235 | */ | ||
236 | hasNext: function() { | ||
237 | return !!this.$.nextSibling; | ||
238 | }, | ||
239 | |||
240 | /** | ||
241 | * Inserts this element after a node. | ||
242 | * | ||
243 | * var em = new CKEDITOR.dom.element( 'em' ); | ||
244 | * var strong = new CKEDITOR.dom.element( 'strong' ); | ||
245 | * strong.insertAfter( em ); | ||
246 | * | ||
247 | * // Result: '<em></em><strong></strong>' | ||
248 | * | ||
249 | * @param {CKEDITOR.dom.node} node The node that will precede this element. | ||
250 | * @returns {CKEDITOR.dom.node} The node preceding this one after insertion. | ||
251 | */ | ||
252 | insertAfter: function( node ) { | ||
253 | node.$.parentNode.insertBefore( this.$, node.$.nextSibling ); | ||
254 | return node; | ||
255 | }, | ||
256 | |||
257 | /** | ||
258 | * Inserts this element before a node. | ||
259 | * | ||
260 | * var em = new CKEDITOR.dom.element( 'em' ); | ||
261 | * var strong = new CKEDITOR.dom.element( 'strong' ); | ||
262 | * strong.insertBefore( em ); | ||
263 | * | ||
264 | * // result: '<strong></strong><em></em>' | ||
265 | * | ||
266 | * @param {CKEDITOR.dom.node} node The node that will succeed this element. | ||
267 | * @returns {CKEDITOR.dom.node} The node being inserted. | ||
268 | */ | ||
269 | insertBefore: function( node ) { | ||
270 | node.$.parentNode.insertBefore( this.$, node.$ ); | ||
271 | return node; | ||
272 | }, | ||
273 | |||
274 | /** | ||
275 | * Inserts a node before this node. | ||
276 | * | ||
277 | * var em = new CKEDITOR.dom.element( 'em' ); | ||
278 | * var strong = new CKEDITOR.dom.element( 'strong' ); | ||
279 | * strong.insertBeforeMe( em ); | ||
280 | * | ||
281 | * // result: '<em></em><strong></strong>' | ||
282 | * | ||
283 | * @param {CKEDITOR.dom.node} node The node that will preceed this element. | ||
284 | * @returns {CKEDITOR.dom.node} The node being inserted. | ||
285 | */ | ||
286 | insertBeforeMe: function( node ) { | ||
287 | this.$.parentNode.insertBefore( node.$, this.$ ); | ||
288 | return node; | ||
289 | }, | ||
290 | |||
291 | /** | ||
292 | * Retrieves a uniquely identifiable tree address for this node. | ||
293 | * The tree address returned is an array of integers, with each integer | ||
294 | * indicating a child index of a DOM node, starting from | ||
295 | * `document.documentElement`. | ||
296 | * | ||
297 | * For example, assuming `<body>` is the second child | ||
298 | * of `<html>` (`<head>` being the first), | ||
299 | * and we would like to address the third child under the | ||
300 | * fourth child of `<body>`, the tree address returned would be: | ||
301 | * `[1, 3, 2]`. | ||
302 | * | ||
303 | * The tree address cannot be used for finding back the DOM tree node once | ||
304 | * the DOM tree structure has been modified. | ||
305 | * | ||
306 | * @param {Boolean} [normalized=false] See {@link #getIndex}. | ||
307 | * @returns {Array} The address. | ||
308 | */ | ||
309 | getAddress: function( normalized ) { | ||
310 | var address = []; | ||
311 | var $documentElement = this.getDocument().$.documentElement; | ||
312 | var node = this.$; | ||
313 | |||
314 | while ( node && node != $documentElement ) { | ||
315 | var parentNode = node.parentNode; | ||
316 | |||
317 | if ( parentNode ) { | ||
318 | // Get the node index. For performance, call getIndex | ||
319 | // directly, instead of creating a new node object. | ||
320 | address.unshift( this.getIndex.call( { $: node }, normalized ) ); | ||
321 | } | ||
322 | |||
323 | node = parentNode; | ||
324 | } | ||
325 | |||
326 | return address; | ||
327 | }, | ||
328 | |||
329 | /** | ||
330 | * Gets the document containing this element. | ||
331 | * | ||
332 | * var element = CKEDITOR.document.getById( 'example' ); | ||
333 | * alert( element.getDocument().equals( CKEDITOR.document ) ); // true | ||
334 | * | ||
335 | * @returns {CKEDITOR.dom.document} The document. | ||
336 | */ | ||
337 | getDocument: function() { | ||
338 | return new CKEDITOR.dom.document( this.$.ownerDocument || this.$.parentNode.ownerDocument ); | ||
339 | }, | ||
340 | |||
341 | /** | ||
342 | * Gets the index of a node in an array of its `parent.childNodes`. | ||
343 | * Returns `-1` if a node does not have a parent or when the `normalized` argument is set to `true` | ||
344 | * and the text node is empty and will be removed during the normalization. | ||
345 | * | ||
346 | * Let us assume having the following `childNodes` array: | ||
347 | * | ||
348 | * [ emptyText, element1, text, text, element2, emptyText2 ] | ||
349 | * | ||
350 | * emptyText.getIndex() // 0 | ||
351 | * emptyText.getIndex( true ) // -1 | ||
352 | * element1.getIndex(); // 1 | ||
353 | * element1.getIndex( true ); // 0 | ||
354 | * element2.getIndex(); // 4 | ||
355 | * element2.getIndex( true ); // 2 | ||
356 | * emptyText2.getIndex(); // 5 | ||
357 | * emptyText2.getIndex( true ); // -1 | ||
358 | * | ||
359 | * @param {Boolean} normalized When `true`, adjacent text nodes are merged and empty text nodes are removed. | ||
360 | * @returns {Number} Index of a node or `-1` if a node does not have a parent or is removed during the normalization. | ||
361 | */ | ||
362 | getIndex: function( normalized ) { | ||
363 | // Attention: getAddress depends on this.$ | ||
364 | // getIndex is called on a plain object: { $ : node } | ||
365 | |||
366 | var current = this.$, | ||
367 | index = -1, | ||
368 | isNormalizing; | ||
369 | |||
370 | if ( !this.$.parentNode ) | ||
371 | return -1; | ||
372 | |||
373 | // The idea is - all empty text nodes will be virtually merged into their adjacent text nodes. | ||
374 | // If an empty text node does not have an adjacent non-empty text node we can return -1 straight away, | ||
375 | // because it and all its sibling text nodes will be merged into an empty text node and then totally ignored. | ||
376 | if ( normalized && current.nodeType == CKEDITOR.NODE_TEXT && !current.nodeValue ) { | ||
377 | var adjacent = getAdjacentNonEmptyTextNode( current ) || getAdjacentNonEmptyTextNode( current, true ); | ||
378 | |||
379 | if ( !adjacent ) | ||
380 | return -1; | ||
381 | } | ||
382 | |||
383 | do { | ||
384 | // Bypass blank node and adjacent text nodes. | ||
385 | if ( normalized && current != this.$ && current.nodeType == CKEDITOR.NODE_TEXT && ( isNormalizing || !current.nodeValue ) ) | ||
386 | continue; | ||
387 | |||
388 | index++; | ||
389 | isNormalizing = current.nodeType == CKEDITOR.NODE_TEXT; | ||
390 | } | ||
391 | while ( ( current = current.previousSibling ) ); | ||
392 | |||
393 | return index; | ||
394 | |||
395 | function getAdjacentNonEmptyTextNode( node, lookForward ) { | ||
396 | var sibling = lookForward ? node.nextSibling : node.previousSibling; | ||
397 | |||
398 | if ( !sibling || sibling.nodeType != CKEDITOR.NODE_TEXT ) { | ||
399 | return null; | ||
400 | } | ||
401 | |||
402 | // If found a non-empty text node, then return it. | ||
403 | // If not, then continue search. | ||
404 | return sibling.nodeValue ? sibling : getAdjacentNonEmptyTextNode( sibling, lookForward ); | ||
405 | } | ||
406 | }, | ||
407 | |||
408 | /** | ||
409 | * @todo | ||
410 | */ | ||
411 | getNextSourceNode: function( startFromSibling, nodeType, guard ) { | ||
412 | // If "guard" is a node, transform it in a function. | ||
413 | if ( guard && !guard.call ) { | ||
414 | var guardNode = guard; | ||
415 | guard = function( node ) { | ||
416 | return !node.equals( guardNode ); | ||
417 | }; | ||
418 | } | ||
419 | |||
420 | var node = ( !startFromSibling && this.getFirst && this.getFirst() ), | ||
421 | parent; | ||
422 | |||
423 | // Guarding when we're skipping the current element( no children or 'startFromSibling' ). | ||
424 | // send the 'moving out' signal even we don't actually dive into. | ||
425 | if ( !node ) { | ||
426 | if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) | ||
427 | return null; | ||
428 | node = this.getNext(); | ||
429 | } | ||
430 | |||
431 | while ( !node && ( parent = ( parent || this ).getParent() ) ) { | ||
432 | // The guard check sends the "true" paramenter to indicate that | ||
433 | // we are moving "out" of the element. | ||
434 | if ( guard && guard( parent, true ) === false ) | ||
435 | return null; | ||
436 | |||
437 | node = parent.getNext(); | ||
438 | } | ||
439 | |||
440 | if ( !node ) | ||
441 | return null; | ||
442 | |||
443 | if ( guard && guard( node ) === false ) | ||
444 | return null; | ||
445 | |||
446 | if ( nodeType && nodeType != node.type ) | ||
447 | return node.getNextSourceNode( false, nodeType, guard ); | ||
448 | |||
449 | return node; | ||
450 | }, | ||
451 | |||
452 | /** | ||
453 | * @todo | ||
454 | */ | ||
455 | getPreviousSourceNode: function( startFromSibling, nodeType, guard ) { | ||
456 | if ( guard && !guard.call ) { | ||
457 | var guardNode = guard; | ||
458 | guard = function( node ) { | ||
459 | return !node.equals( guardNode ); | ||
460 | }; | ||
461 | } | ||
462 | |||
463 | var node = ( !startFromSibling && this.getLast && this.getLast() ), | ||
464 | parent; | ||
465 | |||
466 | // Guarding when we're skipping the current element( no children or 'startFromSibling' ). | ||
467 | // send the 'moving out' signal even we don't actually dive into. | ||
468 | if ( !node ) { | ||
469 | if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) | ||
470 | return null; | ||
471 | node = this.getPrevious(); | ||
472 | } | ||
473 | |||
474 | while ( !node && ( parent = ( parent || this ).getParent() ) ) { | ||
475 | // The guard check sends the "true" paramenter to indicate that | ||
476 | // we are moving "out" of the element. | ||
477 | if ( guard && guard( parent, true ) === false ) | ||
478 | return null; | ||
479 | |||
480 | node = parent.getPrevious(); | ||
481 | } | ||
482 | |||
483 | if ( !node ) | ||
484 | return null; | ||
485 | |||
486 | if ( guard && guard( node ) === false ) | ||
487 | return null; | ||
488 | |||
489 | if ( nodeType && node.type != nodeType ) | ||
490 | return node.getPreviousSourceNode( false, nodeType, guard ); | ||
491 | |||
492 | return node; | ||
493 | }, | ||
494 | |||
495 | /** | ||
496 | * Gets the node that preceeds this element in its parent's child list. | ||
497 | * | ||
498 | * var element = CKEDITOR.dom.element.createFromHtml( '<div><i>prev</i><b>Example</b></div>' ); | ||
499 | * var first = element.getLast().getPrev(); | ||
500 | * alert( first.getName() ); // 'i' | ||
501 | * | ||
502 | * @param {Function} [evaluator] Filtering the result node. | ||
503 | * @returns {CKEDITOR.dom.node} The previous node or null if not available. | ||
504 | */ | ||
505 | getPrevious: function( evaluator ) { | ||
506 | var previous = this.$, | ||
507 | retval; | ||
508 | do { | ||
509 | previous = previous.previousSibling; | ||
510 | |||
511 | // Avoid returning the doc type node. | ||
512 | // http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927 | ||
513 | retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous ); | ||
514 | } | ||
515 | while ( retval && evaluator && !evaluator( retval ) ); | ||
516 | return retval; | ||
517 | }, | ||
518 | |||
519 | /** | ||
520 | * Gets the node that follows this element in its parent's child list. | ||
521 | * | ||
522 | * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b><i>next</i></div>' ); | ||
523 | * var last = element.getFirst().getNext(); | ||
524 | * alert( last.getName() ); // 'i' | ||
525 | * | ||
526 | * @param {Function} [evaluator] Filtering the result node. | ||
527 | * @returns {CKEDITOR.dom.node} The next node or null if not available. | ||
528 | */ | ||
529 | getNext: function( evaluator ) { | ||
530 | var next = this.$, | ||
531 | retval; | ||
532 | do { | ||
533 | next = next.nextSibling; | ||
534 | retval = next && new CKEDITOR.dom.node( next ); | ||
535 | } | ||
536 | while ( retval && evaluator && !evaluator( retval ) ); | ||
537 | return retval; | ||
538 | }, | ||
539 | |||
540 | /** | ||
541 | * Gets the parent element for this node. | ||
542 | * | ||
543 | * var node = editor.document.getBody().getFirst(); | ||
544 | * var parent = node.getParent(); | ||
545 | * alert( parent.getName() ); // 'body' | ||
546 | * | ||
547 | * @param {Boolean} [allowFragmentParent=false] Consider also parent node that is of | ||
548 | * fragment type {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}. | ||
549 | * @returns {CKEDITOR.dom.element} The parent element. | ||
550 | */ | ||
551 | getParent: function( allowFragmentParent ) { | ||
552 | var parent = this.$.parentNode; | ||
553 | return ( parent && ( parent.nodeType == CKEDITOR.NODE_ELEMENT || allowFragmentParent && parent.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) ? new CKEDITOR.dom.node( parent ) : null; | ||
554 | }, | ||
555 | |||
556 | /** | ||
557 | * Returns an array containing node parents and the node itself. By default nodes are in _descending_ order. | ||
558 | * | ||
559 | * // Assuming that body has paragraph as the first child. | ||
560 | * var node = editor.document.getBody().getFirst(); | ||
561 | * var parents = node.getParents(); | ||
562 | * alert( parents[ 0 ].getName() + ',' + parents[ 2 ].getName() ); // 'html,p' | ||
563 | * | ||
564 | * @param {Boolean} [closerFirst=false] Determines the order of returned nodes. | ||
565 | * @returns {Array} Returns an array of {@link CKEDITOR.dom.node}. | ||
566 | */ | ||
567 | getParents: function( closerFirst ) { | ||
568 | var node = this; | ||
569 | var parents = []; | ||
570 | |||
571 | do { | ||
572 | parents[ closerFirst ? 'push' : 'unshift' ]( node ); | ||
573 | } | ||
574 | while ( ( node = node.getParent() ) ); | ||
575 | |||
576 | return parents; | ||
577 | }, | ||
578 | |||
579 | /** | ||
580 | * @todo | ||
581 | */ | ||
582 | getCommonAncestor: function( node ) { | ||
583 | if ( node.equals( this ) ) | ||
584 | return this; | ||
585 | |||
586 | if ( node.contains && node.contains( this ) ) | ||
587 | return node; | ||
588 | |||
589 | var start = this.contains ? this : this.getParent(); | ||
590 | |||
591 | do { | ||
592 | if ( start.contains( node ) ) return start; | ||
593 | } | ||
594 | while ( ( start = start.getParent() ) ); | ||
595 | |||
596 | return null; | ||
597 | }, | ||
598 | |||
599 | /** | ||
600 | * Determines the position relation between this node and the given {@link CKEDITOR.dom.node} in the document. | ||
601 | * This node can be preceding ({@link CKEDITOR#POSITION_PRECEDING}) or following ({@link CKEDITOR#POSITION_FOLLOWING}) | ||
602 | * the given node. This node can also contain ({@link CKEDITOR#POSITION_CONTAINS}) or be contained by | ||
603 | * ({@link CKEDITOR#POSITION_IS_CONTAINED}) the given node. The function returns a bitmask of constants | ||
604 | * listed above or {@link CKEDITOR#POSITION_IDENTICAL} if the given node is the same as this node. | ||
605 | * | ||
606 | * @param {CKEDITOR.dom.node} otherNode A node to check relation with. | ||
607 | * @returns {Number} Position relation between this node and given node. | ||
608 | */ | ||
609 | getPosition: function( otherNode ) { | ||
610 | var $ = this.$; | ||
611 | var $other = otherNode.$; | ||
612 | |||
613 | if ( $.compareDocumentPosition ) | ||
614 | return $.compareDocumentPosition( $other ); | ||
615 | |||
616 | // IE and Safari have no support for compareDocumentPosition. | ||
617 | |||
618 | if ( $ == $other ) | ||
619 | return CKEDITOR.POSITION_IDENTICAL; | ||
620 | |||
621 | // Only element nodes support contains and sourceIndex. | ||
622 | if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) { | ||
623 | if ( $.contains ) { | ||
624 | if ( $.contains( $other ) ) | ||
625 | return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING; | ||
626 | |||
627 | if ( $other.contains( $ ) ) | ||
628 | return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; | ||
629 | } | ||
630 | |||
631 | if ( 'sourceIndex' in $ ) | ||
632 | return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; | ||
633 | |||
634 | } | ||
635 | |||
636 | // For nodes that don't support compareDocumentPosition, contains | ||
637 | // or sourceIndex, their "address" is compared. | ||
638 | |||
639 | var addressOfThis = this.getAddress(), | ||
640 | addressOfOther = otherNode.getAddress(), | ||
641 | minLevel = Math.min( addressOfThis.length, addressOfOther.length ); | ||
642 | |||
643 | // Determinate preceding/following relationship. | ||
644 | for ( var i = 0; i < minLevel; i++ ) { | ||
645 | if ( addressOfThis[ i ] != addressOfOther[ i ] ) { | ||
646 | return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; | ||
647 | } | ||
648 | } | ||
649 | |||
650 | // Determinate contains/contained relationship. | ||
651 | return ( addressOfThis.length < addressOfOther.length ) ? CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; | ||
652 | }, | ||
653 | |||
654 | /** | ||
655 | * Gets the closest ancestor node of this node, specified by its name or using an evaluator function. | ||
656 | * | ||
657 | * // Suppose we have the following HTML structure: | ||
658 | * // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div> | ||
659 | * // If node == <b> | ||
660 | * ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner"> | ||
661 | * ascendant = node.getAscendant( 'b' ); // ascendant == null | ||
662 | * ascendant = node.getAscendant( 'b', true ); // ascendant == <b> | ||
663 | * ascendant = node.getAscendant( { div:1, p:1 } ); // Searches for the first 'div' or 'p': ascendant == <div id="inner"> | ||
664 | * | ||
665 | * // Using custom evaluator: | ||
666 | * ascendant = node.getAscendant( function( el ) { | ||
667 | * return el.getId() == 'inner'; | ||
668 | * } ); | ||
669 | * // ascendant == <div id="inner"> | ||
670 | * | ||
671 | * @since 3.6.1 | ||
672 | * @param {String/Function/Object} query The name of the ancestor node to search or | ||
673 | * an object with the node names to search for or an evaluator function. | ||
674 | * @param {Boolean} [includeSelf] Whether to include the current | ||
675 | * node in the search. | ||
676 | * @returns {CKEDITOR.dom.node} The located ancestor node or `null` if not found. | ||
677 | */ | ||
678 | getAscendant: function( query, includeSelf ) { | ||
679 | var $ = this.$, | ||
680 | evaluator, | ||
681 | isCustomEvaluator; | ||
682 | |||
683 | if ( !includeSelf ) { | ||
684 | $ = $.parentNode; | ||
685 | } | ||
686 | |||
687 | // Custom checker provided in an argument. | ||
688 | if ( typeof query == 'function' ) { | ||
689 | isCustomEvaluator = true; | ||
690 | evaluator = query; | ||
691 | } else { | ||
692 | // Predefined tag name checker. | ||
693 | isCustomEvaluator = false; | ||
694 | evaluator = function( $ ) { | ||
695 | var name = ( typeof $.nodeName == 'string' ? $.nodeName.toLowerCase() : '' ); | ||
696 | |||
697 | return ( typeof query == 'string' ? name == query : name in query ); | ||
698 | }; | ||
699 | } | ||
700 | |||
701 | while ( $ ) { | ||
702 | // For user provided checker we use CKEDITOR.dom.node. | ||
703 | if ( evaluator( isCustomEvaluator ? new CKEDITOR.dom.node( $ ) : $ ) ) { | ||
704 | return new CKEDITOR.dom.node( $ ); | ||
705 | } | ||
706 | |||
707 | try { | ||
708 | $ = $.parentNode; | ||
709 | } catch ( e ) { | ||
710 | $ = null; | ||
711 | } | ||
712 | } | ||
713 | |||
714 | return null; | ||
715 | }, | ||
716 | |||
717 | /** | ||
718 | * @todo | ||
719 | */ | ||
720 | hasAscendant: function( name, includeSelf ) { | ||
721 | var $ = this.$; | ||
722 | |||
723 | if ( !includeSelf ) | ||
724 | $ = $.parentNode; | ||
725 | |||
726 | while ( $ ) { | ||
727 | if ( $.nodeName && $.nodeName.toLowerCase() == name ) | ||
728 | return true; | ||
729 | |||
730 | $ = $.parentNode; | ||
731 | } | ||
732 | return false; | ||
733 | }, | ||
734 | |||
735 | /** | ||
736 | * @todo | ||
737 | */ | ||
738 | move: function( target, toStart ) { | ||
739 | target.append( this.remove(), toStart ); | ||
740 | }, | ||
741 | |||
742 | /** | ||
743 | * Removes this node from the document DOM. | ||
744 | * | ||
745 | * var element = CKEDITOR.document.getById( 'MyElement' ); | ||
746 | * element.remove(); | ||
747 | * | ||
748 | * @param {Boolean} [preserveChildren=false] Indicates that the children | ||
749 | * elements must remain in the document, removing only the outer tags. | ||
750 | */ | ||
751 | remove: function( preserveChildren ) { | ||
752 | var $ = this.$; | ||
753 | var parent = $.parentNode; | ||
754 | |||
755 | if ( parent ) { | ||
756 | if ( preserveChildren ) { | ||
757 | // Move all children before the node. | ||
758 | for ( var child; | ||
759 | ( child = $.firstChild ); ) { | ||
760 | parent.insertBefore( $.removeChild( child ), $ ); | ||
761 | } | ||
762 | } | ||
763 | |||
764 | parent.removeChild( $ ); | ||
765 | } | ||
766 | |||
767 | return this; | ||
768 | }, | ||
769 | |||
770 | /** | ||
771 | * @todo | ||
772 | */ | ||
773 | replace: function( nodeToReplace ) { | ||
774 | this.insertBefore( nodeToReplace ); | ||
775 | nodeToReplace.remove(); | ||
776 | }, | ||
777 | |||
778 | /** | ||
779 | * @todo | ||
780 | */ | ||
781 | trim: function() { | ||
782 | this.ltrim(); | ||
783 | this.rtrim(); | ||
784 | }, | ||
785 | |||
786 | /** | ||
787 | * @todo | ||
788 | */ | ||
789 | ltrim: function() { | ||
790 | var child; | ||
791 | while ( this.getFirst && ( child = this.getFirst() ) ) { | ||
792 | if ( child.type == CKEDITOR.NODE_TEXT ) { | ||
793 | var trimmed = CKEDITOR.tools.ltrim( child.getText() ), | ||
794 | originalLength = child.getLength(); | ||
795 | |||
796 | if ( !trimmed ) { | ||
797 | child.remove(); | ||
798 | continue; | ||
799 | } else if ( trimmed.length < originalLength ) { | ||
800 | child.split( originalLength - trimmed.length ); | ||
801 | |||
802 | // IE BUG: child.remove() may raise JavaScript errors here. (#81) | ||
803 | this.$.removeChild( this.$.firstChild ); | ||
804 | } | ||
805 | } | ||
806 | break; | ||
807 | } | ||
808 | }, | ||
809 | |||
810 | /** | ||
811 | * @todo | ||
812 | */ | ||
813 | rtrim: function() { | ||
814 | var child; | ||
815 | while ( this.getLast && ( child = this.getLast() ) ) { | ||
816 | if ( child.type == CKEDITOR.NODE_TEXT ) { | ||
817 | var trimmed = CKEDITOR.tools.rtrim( child.getText() ), | ||
818 | originalLength = child.getLength(); | ||
819 | |||
820 | if ( !trimmed ) { | ||
821 | child.remove(); | ||
822 | continue; | ||
823 | } else if ( trimmed.length < originalLength ) { | ||
824 | child.split( trimmed.length ); | ||
825 | |||
826 | // IE BUG: child.getNext().remove() may raise JavaScript errors here. | ||
827 | // (#81) | ||
828 | this.$.lastChild.parentNode.removeChild( this.$.lastChild ); | ||
829 | } | ||
830 | } | ||
831 | break; | ||
832 | } | ||
833 | |||
834 | if ( CKEDITOR.env.needsBrFiller ) { | ||
835 | child = this.$.lastChild; | ||
836 | |||
837 | if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) { | ||
838 | // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324). | ||
839 | child.parentNode.removeChild( child ); | ||
840 | } | ||
841 | } | ||
842 | }, | ||
843 | |||
844 | /** | ||
845 | * Checks if this node is read-only (should not be changed). | ||
846 | * | ||
847 | * // For the following HTML: | ||
848 | * // <b>foo</b><div contenteditable="false"><i>bar</i></div> | ||
849 | * | ||
850 | * elB.isReadOnly(); // -> false | ||
851 | * foo.isReadOnly(); // -> false | ||
852 | * elDiv.isReadOnly(); // -> true | ||
853 | * elI.isReadOnly(); // -> true | ||
854 | * | ||
855 | * This method works in two modes depending on browser support for the `element.isContentEditable` property and | ||
856 | * the value of the `checkOnlyAttributes` parameter. The `element.isContentEditable` check is faster, but it is known | ||
857 | * to malfunction in hidden or detached nodes. Additionally, when processing some detached DOM tree you may want to imitate | ||
858 | * that this happens inside an editable container (like it would happen inside the {@link CKEDITOR.editable}). To do so, | ||
859 | * you can temporarily attach this tree to an element with the `data-cke-editable` attribute and use the | ||
860 | * `checkOnlyAttributes` mode. | ||
861 | * | ||
862 | * @since 3.5 | ||
863 | * @param {Boolean} [checkOnlyAttributes=false] If `true`, only attributes will be checked, native methods will not | ||
864 | * be used. This parameter needs to be `true` to check hidden or detached elements. Introduced in 4.5. | ||
865 | * @returns {Boolean} | ||
866 | */ | ||
867 | isReadOnly: function( checkOnlyAttributes ) { | ||
868 | var element = this; | ||
869 | if ( this.type != CKEDITOR.NODE_ELEMENT ) | ||
870 | element = this.getParent(); | ||
871 | |||
872 | // Prevent Edge crash (#13609, #13919). | ||
873 | if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) { | ||
874 | checkOnlyAttributes = true; | ||
875 | } | ||
876 | |||
877 | if ( !checkOnlyAttributes && element && typeof element.$.isContentEditable != 'undefined' ) { | ||
878 | return !( element.$.isContentEditable || element.data( 'cke-editable' ) ); | ||
879 | } | ||
880 | else { | ||
881 | // Degrade for old browsers which don't support "isContentEditable", e.g. FF3 | ||
882 | |||
883 | while ( element ) { | ||
884 | if ( element.data( 'cke-editable' ) ) { | ||
885 | return false; | ||
886 | } else if ( element.hasAttribute( 'contenteditable' ) ) { | ||
887 | return element.getAttribute( 'contenteditable' ) == 'false'; | ||
888 | } | ||
889 | |||
890 | element = element.getParent(); | ||
891 | } | ||
892 | |||
893 | // Reached the root of DOM tree, no editable found. | ||
894 | return true; | ||
895 | } | ||
896 | } | ||
897 | } ); | ||