]>
Commit | Line | Data |
---|---|---|
1 | /** | |
2 | * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | |
4 | */ | |
5 | ||
6 | ( function() { | |
7 | // This function is to be called under a "walker" instance scope. | |
8 | function iterate( rtl, breakOnFalse ) { | |
9 | var range = this.range; | |
10 | ||
11 | // Return null if we have reached the end. | |
12 | if ( this._.end ) | |
13 | return null; | |
14 | ||
15 | // This is the first call. Initialize it. | |
16 | if ( !this._.start ) { | |
17 | this._.start = 1; | |
18 | ||
19 | // A collapsed range must return null at first call. | |
20 | if ( range.collapsed ) { | |
21 | this.end(); | |
22 | return null; | |
23 | } | |
24 | ||
25 | // Move outside of text node edges. | |
26 | range.optimize(); | |
27 | } | |
28 | ||
29 | var node, | |
30 | startCt = range.startContainer, | |
31 | endCt = range.endContainer, | |
32 | startOffset = range.startOffset, | |
33 | endOffset = range.endOffset, | |
34 | guard, | |
35 | userGuard = this.guard, | |
36 | type = this.type, | |
37 | getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' ); | |
38 | ||
39 | // Create the LTR guard function, if necessary. | |
40 | if ( !rtl && !this._.guardLTR ) { | |
41 | // The node that stops walker from moving up. | |
42 | var limitLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt : endCt.getParent(); | |
43 | ||
44 | // The node that stops the walker from going to next. | |
45 | var blockerLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt.getChild( endOffset ) : endCt.getNext(); | |
46 | ||
47 | this._.guardLTR = function( node, movingOut ) { | |
48 | return ( ( !movingOut || !limitLTR.equals( node ) ) && ( !blockerLTR || !node.equals( blockerLTR ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) ); | |
49 | }; | |
50 | } | |
51 | ||
52 | // Create the RTL guard function, if necessary. | |
53 | if ( rtl && !this._.guardRTL ) { | |
54 | // The node that stops walker from moving up. | |
55 | var limitRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startCt : startCt.getParent(); | |
56 | ||
57 | // The node that stops the walker from going to next. | |
58 | var blockerRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startOffset ? startCt.getChild( startOffset - 1 ) : null : startCt.getPrevious(); | |
59 | ||
60 | this._.guardRTL = function( node, movingOut ) { | |
61 | return ( ( !movingOut || !limitRTL.equals( node ) ) && ( !blockerRTL || !node.equals( blockerRTL ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) ); | |
62 | }; | |
63 | } | |
64 | ||
65 | // Define which guard function to use. | |
66 | var stopGuard = rtl ? this._.guardRTL : this._.guardLTR; | |
67 | ||
68 | // Make the user defined guard function participate in the process, | |
69 | // otherwise simply use the boundary guard. | |
70 | if ( userGuard ) { | |
71 | guard = function( node, movingOut ) { | |
72 | if ( stopGuard( node, movingOut ) === false ) | |
73 | return false; | |
74 | ||
75 | return userGuard( node, movingOut ); | |
76 | }; | |
77 | } else { | |
78 | guard = stopGuard; | |
79 | } | |
80 | ||
81 | if ( this.current ) | |
82 | node = this.current[ getSourceNodeFn ]( false, type, guard ); | |
83 | else { | |
84 | // Get the first node to be returned. | |
85 | if ( rtl ) { | |
86 | node = endCt; | |
87 | ||
88 | if ( node.type == CKEDITOR.NODE_ELEMENT ) { | |
89 | if ( endOffset > 0 ) | |
90 | node = node.getChild( endOffset - 1 ); | |
91 | else | |
92 | node = ( guard( node, true ) === false ) ? null : node.getPreviousSourceNode( true, type, guard ); | |
93 | } | |
94 | } else { | |
95 | node = startCt; | |
96 | ||
97 | if ( node.type == CKEDITOR.NODE_ELEMENT ) { | |
98 | if ( !( node = node.getChild( startOffset ) ) ) | |
99 | node = ( guard( startCt, true ) === false ) ? null : startCt.getNextSourceNode( true, type, guard ); | |
100 | } | |
101 | } | |
102 | ||
103 | if ( node && guard( node ) === false ) | |
104 | node = null; | |
105 | } | |
106 | ||
107 | while ( node && !this._.end ) { | |
108 | this.current = node; | |
109 | ||
110 | if ( !this.evaluator || this.evaluator( node ) !== false ) { | |
111 | if ( !breakOnFalse ) | |
112 | return node; | |
113 | } else if ( breakOnFalse && this.evaluator ) { | |
114 | return false; | |
115 | } | |
116 | ||
117 | node = node[ getSourceNodeFn ]( false, type, guard ); | |
118 | } | |
119 | ||
120 | this.end(); | |
121 | return this.current = null; | |
122 | } | |
123 | ||
124 | function iterateToLast( rtl ) { | |
125 | var node, | |
126 | last = null; | |
127 | ||
128 | while ( ( node = iterate.call( this, rtl ) ) ) | |
129 | last = node; | |
130 | ||
131 | return last; | |
132 | } | |
133 | ||
134 | /** | |
135 | * Utility class to "walk" the DOM inside range boundaries. If the | |
136 | * range starts or ends in the middle of the text node, this node will | |
137 | * be included as a whole. Outside changes to the range may break the walker. | |
138 | * | |
139 | * The walker may return nodes that are not totally included in the | |
140 | * range boundaries. Let us take the following range representation, | |
141 | * where the square brackets indicate the boundaries: | |
142 | * | |
143 | * [<p>Some <b>sample] text</b> | |
144 | * | |
145 | * While walking forward into the above range, the following nodes are | |
146 | * returned: `<p>`, `"Some "`, `<b>` and `"sample"`. Going | |
147 | * backwards instead we have: `"sample"` and `"Some "`. So note that the | |
148 | * walker always returns nodes when "entering" them, but not when | |
149 | * "leaving" them. The {@link #guard} function is instead called both when | |
150 | * entering and when leaving nodes. | |
151 | * | |
152 | * @class | |
153 | */ | |
154 | CKEDITOR.dom.walker = CKEDITOR.tools.createClass( { | |
155 | /** | |
156 | * Creates a walker class instance. | |
157 | * | |
158 | * @constructor | |
159 | * @param {CKEDITOR.dom.range} range The range within which to walk. | |
160 | */ | |
161 | $: function( range ) { | |
162 | this.range = range; | |
163 | ||
164 | /** | |
165 | * A function executed for every matched node to check whether | |
166 | * it is to be considered in the walk or not. If not provided, all | |
167 | * matched nodes are considered good. | |
168 | * | |
169 | * If the function returns `false`, the node is ignored. | |
170 | * | |
171 | * @property {Function} evaluator | |
172 | */ | |
173 | // this.evaluator = null; | |
174 | ||
175 | /** | |
176 | * A function executed for every node the walk passes by to check | |
177 | * whether the walk is to be finished. It is called both when | |
178 | * entering and when exiting nodes, as well as for the matched nodes. | |
179 | * | |
180 | * If this function returns `false`, the walking ends and no more | |
181 | * nodes are evaluated. | |
182 | ||
183 | * @property {Function} guard | |
184 | */ | |
185 | // this.guard = null; | |
186 | ||
187 | /** @private */ | |
188 | this._ = {}; | |
189 | }, | |
190 | ||
191 | // statics : | |
192 | // { | |
193 | // /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes. | |
194 | // * @param {CKEDITOR.dom.node} startNode The node from which the walk | |
195 | // * will start. | |
196 | // * @param {CKEDITOR.dom.node} [endNode] The last node to be considered | |
197 | // * in the walk. No more nodes are retrieved after touching or | |
198 | // * passing it. If not provided, the walker stops at the | |
199 | // * <body> closing boundary. | |
200 | // * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the | |
201 | // * provided nodes. | |
202 | // */ | |
203 | // createOnNodes : function( startNode, endNode, startInclusive, endInclusive ) | |
204 | // { | |
205 | // var range = new CKEDITOR.dom.range(); | |
206 | // if ( startNode ) | |
207 | // range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ; | |
208 | // else | |
209 | // range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ; | |
210 | // | |
211 | // if ( endNode ) | |
212 | // range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ; | |
213 | // else | |
214 | // range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ; | |
215 | // | |
216 | // return new CKEDITOR.dom.walker( range ); | |
217 | // } | |
218 | // }, | |
219 | // | |
220 | proto: { | |
221 | /** | |
222 | * Stops walking. No more nodes are retrieved if this function is called. | |
223 | */ | |
224 | end: function() { | |
225 | this._.end = 1; | |
226 | }, | |
227 | ||
228 | /** | |
229 | * Retrieves the next node (on the right). | |
230 | * | |
231 | * @returns {CKEDITOR.dom.node} The next node or `null` if no more | |
232 | * nodes are available. | |
233 | */ | |
234 | next: function() { | |
235 | return iterate.call( this ); | |
236 | }, | |
237 | ||
238 | /** | |
239 | * Retrieves the previous node (on the left). | |
240 | * | |
241 | * @returns {CKEDITOR.dom.node} The previous node or `null` if no more | |
242 | * nodes are available. | |
243 | */ | |
244 | previous: function() { | |
245 | return iterate.call( this, 1 ); | |
246 | }, | |
247 | ||
248 | /** | |
249 | * Checks all nodes on the right, executing the evaluation function. | |
250 | * | |
251 | * @returns {Boolean} `false` if the evaluator function returned | |
252 | * `false` for any of the matched nodes. Otherwise `true`. | |
253 | */ | |
254 | checkForward: function() { | |
255 | return iterate.call( this, 0, 1 ) !== false; | |
256 | }, | |
257 | ||
258 | /** | |
259 | * Check all nodes on the left, executing the evaluation function. | |
260 | * | |
261 | * @returns {Boolean} `false` if the evaluator function returned | |
262 | * `false` for any of the matched nodes. Otherwise `true`. | |
263 | */ | |
264 | checkBackward: function() { | |
265 | return iterate.call( this, 1, 1 ) !== false; | |
266 | }, | |
267 | ||
268 | /** | |
269 | * Executes a full walk forward (to the right), until no more nodes | |
270 | * are available, returning the last valid node. | |
271 | * | |
272 | * @returns {CKEDITOR.dom.node} The last node on the right or `null` | |
273 | * if no valid nodes are available. | |
274 | */ | |
275 | lastForward: function() { | |
276 | return iterateToLast.call( this ); | |
277 | }, | |
278 | ||
279 | /** | |
280 | * Executes a full walk backwards (to the left), until no more nodes | |
281 | * are available, returning the last valid node. | |
282 | * | |
283 | * @returns {CKEDITOR.dom.node} The last node on the left or `null` | |
284 | * if no valid nodes are available. | |
285 | */ | |
286 | lastBackward: function() { | |
287 | return iterateToLast.call( this, 1 ); | |
288 | }, | |
289 | ||
290 | /** | |
291 | * Resets the walker. | |
292 | */ | |
293 | reset: function() { | |
294 | delete this.current; | |
295 | this._ = {}; | |
296 | } | |
297 | ||
298 | } | |
299 | } ); | |
300 | ||
301 | // Anything whose display computed style is block, list-item, table, | |
302 | // table-row-group, table-header-group, table-footer-group, table-row, | |
303 | // table-column-group, table-column, table-cell, table-caption, or whose node | |
304 | // name is hr, br (when enterMode is br only) is a block boundary. | |
305 | var blockBoundaryDisplayMatch = { | |
306 | block: 1, 'list-item': 1, table: 1, 'table-row-group': 1, | |
307 | 'table-header-group': 1, 'table-footer-group': 1, 'table-row': 1, 'table-column-group': 1, | |
308 | 'table-column': 1, 'table-cell': 1, 'table-caption': 1 | |
309 | }, | |
310 | outOfFlowPositions = { absolute: 1, fixed: 1 }; | |
311 | ||
312 | /** | |
313 | * Checks whether an element is displayed as a block. | |
314 | * | |
315 | * @member CKEDITOR.dom.element | |
316 | * @param [customNodeNames] Custom list of nodes which will extend | |
317 | * the default {@link CKEDITOR.dtd#$block} list. | |
318 | * @returns {Boolean} | |
319 | */ | |
320 | CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) { | |
321 | // Whether element is in normal page flow. Floated or positioned elements are out of page flow. | |
322 | // Don't consider floated or positioned formatting as block boundary, fall back to dtd check in that case. (http://dev.ckeditor.com/ticket/6297) | |
323 | var inPageFlow = this.getComputedStyle( 'float' ) == 'none' && !( this.getComputedStyle( 'position' ) in outOfFlowPositions ); | |
324 | ||
325 | if ( inPageFlow && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] ) | |
326 | return true; | |
327 | ||
328 | // Either in $block or in customNodeNames if defined. | |
329 | return !!( this.is( CKEDITOR.dtd.$block ) || customNodeNames && this.is( customNodeNames ) ); | |
330 | }; | |
331 | ||
332 | /** | |
333 | * Returns a function which checks whether the node is a block boundary. | |
334 | * See {@link CKEDITOR.dom.element#isBlockBoundary}. | |
335 | * | |
336 | * @static | |
337 | * @param customNodeNames | |
338 | * @returns {Function} | |
339 | */ | |
340 | CKEDITOR.dom.walker.blockBoundary = function( customNodeNames ) { | |
341 | return function( node ) { | |
342 | return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary( customNodeNames ) ); | |
343 | }; | |
344 | }; | |
345 | ||
346 | /** | |
347 | * @static | |
348 | * @todo | |
349 | */ | |
350 | CKEDITOR.dom.walker.listItemBoundary = function() { | |
351 | return this.blockBoundary( { br: 1 } ); | |
352 | }; | |
353 | ||
354 | /** | |
355 | * Returns a function which checks whether the node is a bookmark node or the bookmark node | |
356 | * inner content. | |
357 | * | |
358 | * @static | |
359 | * @param {Boolean} [contentOnly=false] Whether only test against the text content of | |
360 | * a bookmark node instead of the element itself (default). | |
361 | * @param {Boolean} [isReject=false] Whether to return `false` for the bookmark | |
362 | * node instead of `true` (default). | |
363 | * @returns {Function} | |
364 | */ | |
365 | CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject ) { | |
366 | function isBookmarkNode( node ) { | |
367 | return ( node && node.getName && node.getName() == 'span' && node.data( 'cke-bookmark' ) ); | |
368 | } | |
369 | ||
370 | return function( node ) { | |
371 | var isBookmark, parent; | |
372 | // Is bookmark inner text node? | |
373 | isBookmark = ( node && node.type != CKEDITOR.NODE_ELEMENT && ( parent = node.getParent() ) && isBookmarkNode( parent ) ); | |
374 | // Is bookmark node? | |
375 | isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node ); | |
376 | return !!( isReject ^ isBookmark ); | |
377 | }; | |
378 | }; | |
379 | ||
380 | /** | |
381 | * Returns a function which checks whether the node is a text node containing only whitespace characters. | |
382 | * | |
383 | * @static | |
384 | * @param {Boolean} [isReject=false] | |
385 | * @returns {Function} | |
386 | */ | |
387 | CKEDITOR.dom.walker.whitespaces = function( isReject ) { | |
388 | return function( node ) { | |
389 | var isWhitespace; | |
390 | if ( node && node.type == CKEDITOR.NODE_TEXT ) { | |
391 | // Whitespace, as well as the Filling Char Sequence text node used in Webkit. (http://dev.ckeditor.com/ticket/9384, http://dev.ckeditor.com/ticket/13816) | |
392 | isWhitespace = !CKEDITOR.tools.trim( node.getText() ) || | |
393 | CKEDITOR.env.webkit && node.getText() == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE; | |
394 | } | |
395 | ||
396 | return !!( isReject ^ isWhitespace ); | |
397 | }; | |
398 | }; | |
399 | ||
400 | /** | |
401 | * Returns a function which checks whether the node is invisible in the WYSIWYG mode. | |
402 | * | |
403 | * @static | |
404 | * @param {Boolean} [isReject=false] | |
405 | * @returns {Function} | |
406 | */ | |
407 | CKEDITOR.dom.walker.invisible = function( isReject ) { | |
408 | var whitespace = CKEDITOR.dom.walker.whitespaces(), | |
409 | // http://dev.ckeditor.com/ticket/12221 (Chrome) plus http://dev.ckeditor.com/ticket/11111 (Safari). | |
410 | offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0; | |
411 | ||
412 | return function( node ) { | |
413 | var invisible; | |
414 | ||
415 | if ( whitespace( node ) ) | |
416 | invisible = 1; | |
417 | else { | |
418 | // Visibility should be checked on element. | |
419 | if ( node.type == CKEDITOR.NODE_TEXT ) | |
420 | node = node.getParent(); | |
421 | ||
422 | // Nodes that take no spaces in wysiwyg: | |
423 | // 1. White-spaces but not including NBSP. | |
424 | // 2. Empty inline elements, e.g. <b></b>. | |
425 | // 3. <br> elements (bogus, surrounded by text) (http://dev.ckeditor.com/ticket/12423). | |
426 | invisible = node.$.offsetWidth <= offsetWidth0; | |
427 | } | |
428 | ||
429 | return !!( isReject ^ invisible ); | |
430 | }; | |
431 | }; | |
432 | ||
433 | /** | |
434 | * Returns a function which checks whether the node type is equal to the passed one. | |
435 | * | |
436 | * @static | |
437 | * @param {Number} type | |
438 | * @param {Boolean} [isReject=false] | |
439 | * @returns {Function} | |
440 | */ | |
441 | CKEDITOR.dom.walker.nodeType = function( type, isReject ) { | |
442 | return function( node ) { | |
443 | return !!( isReject ^ ( node.type == type ) ); | |
444 | }; | |
445 | }; | |
446 | ||
447 | /** | |
448 | * Returns a function which checks whether the node is a bogus (filler) node from | |
449 | * `contenteditable` element's point of view. | |
450 | * | |
451 | * @static | |
452 | * @param {Boolean} [isReject=false] | |
453 | * @returns {Function} | |
454 | */ | |
455 | CKEDITOR.dom.walker.bogus = function( isReject ) { | |
456 | function nonEmpty( node ) { | |
457 | return !isWhitespaces( node ) && !isBookmark( node ); | |
458 | } | |
459 | ||
460 | return function( node ) { | |
461 | var isBogus = CKEDITOR.env.needsBrFiller ? node.is && node.is( 'br' ) : node.getText && tailNbspRegex.test( node.getText() ); | |
462 | ||
463 | if ( isBogus ) { | |
464 | var parent = node.getParent(), | |
465 | next = node.getNext( nonEmpty ); | |
466 | ||
467 | isBogus = parent.isBlockBoundary() && ( !next || next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() ); | |
468 | } | |
469 | ||
470 | return !!( isReject ^ isBogus ); | |
471 | }; | |
472 | }; | |
473 | ||
474 | /** | |
475 | * Returns a function which checks whether the node is a temporary element | |
476 | * (element with the `data-cke-temp` attribute) or its child. | |
477 | * | |
478 | * @since 4.3 | |
479 | * @static | |
480 | * @param {Boolean} [isReject=false] Whether to return `false` for the | |
481 | * temporary element instead of `true` (default). | |
482 | * @returns {Function} | |
483 | */ | |
484 | CKEDITOR.dom.walker.temp = function( isReject ) { | |
485 | return function( node ) { | |
486 | if ( node.type != CKEDITOR.NODE_ELEMENT ) | |
487 | node = node.getParent(); | |
488 | ||
489 | var isTemp = node && node.hasAttribute( 'data-cke-temp' ); | |
490 | ||
491 | return !!( isReject ^ isTemp ); | |
492 | }; | |
493 | }; | |
494 | ||
495 | var tailNbspRegex = /^[\t\r\n ]*(?: |\xa0)$/, | |
496 | isWhitespaces = CKEDITOR.dom.walker.whitespaces(), | |
497 | isBookmark = CKEDITOR.dom.walker.bookmark(), | |
498 | isTemp = CKEDITOR.dom.walker.temp(), | |
499 | toSkip = function( node ) { | |
500 | return isBookmark( node ) || | |
501 | isWhitespaces( node ) || | |
502 | node.type == CKEDITOR.NODE_ELEMENT && node.is( CKEDITOR.dtd.$inline ) && !node.is( CKEDITOR.dtd.$empty ); | |
503 | }; | |
504 | ||
505 | /** | |
506 | * Returns a function which checks whether the node should be ignored in terms of "editability". | |
507 | * | |
508 | * This includes: | |
509 | * | |
510 | * * whitespaces (see {@link CKEDITOR.dom.walker#whitespaces}), | |
511 | * * bookmarks (see {@link CKEDITOR.dom.walker#bookmark}), | |
512 | * * temporary elements (see {@link CKEDITOR.dom.walker#temp}). | |
513 | * | |
514 | * @since 4.3 | |
515 | * @static | |
516 | * @param {Boolean} [isReject=false] Whether to return `false` for the | |
517 | * ignored element instead of `true` (default). | |
518 | * @returns {Function} | |
519 | */ | |
520 | CKEDITOR.dom.walker.ignored = function( isReject ) { | |
521 | return function( node ) { | |
522 | var isIgnored = isWhitespaces( node ) || isBookmark( node ) || isTemp( node ); | |
523 | ||
524 | return !!( isReject ^ isIgnored ); | |
525 | }; | |
526 | }; | |
527 | ||
528 | var isIgnored = CKEDITOR.dom.walker.ignored(); | |
529 | ||
530 | /** | |
531 | * Returns a function which checks whether the node is empty. | |
532 | * | |
533 | * @since 4.5 | |
534 | * @static | |
535 | * @param {Boolean} [isReject=false] Whether to return `false` for the | |
536 | * ignored element instead of `true` (default). | |
537 | * @returns {Function} | |
538 | */ | |
539 | CKEDITOR.dom.walker.empty = function( isReject ) { | |
540 | return function( node ) { | |
541 | var i = 0, | |
542 | l = node.getChildCount(); | |
543 | ||
544 | for ( ; i < l; ++i ) { | |
545 | if ( !isIgnored( node.getChild( i ) ) ) { | |
546 | return !!isReject; | |
547 | } | |
548 | } | |
549 | ||
550 | return !isReject; | |
551 | }; | |
552 | }; | |
553 | ||
554 | var isEmpty = CKEDITOR.dom.walker.empty(); | |
555 | ||
556 | function filterTextContainers( dtd ) { | |
557 | var hash = {}, | |
558 | name; | |
559 | ||
560 | for ( name in dtd ) { | |
561 | if ( CKEDITOR.dtd[ name ][ '#' ] ) | |
562 | hash[ name ] = 1; | |
563 | } | |
564 | return hash; | |
565 | } | |
566 | ||
567 | /** | |
568 | * A hash of element names which in browsers that {@link CKEDITOR.env#needsBrFiller do not need `<br>` fillers} | |
569 | * can be selection containers despite being empty. | |
570 | * | |
571 | * @since 4.5 | |
572 | * @static | |
573 | * @property {Object} validEmptyBlockContainers | |
574 | */ | |
575 | var validEmptyBlocks = CKEDITOR.dom.walker.validEmptyBlockContainers = CKEDITOR.tools.extend( | |
576 | filterTextContainers( CKEDITOR.dtd.$block ), | |
577 | { caption: 1, td: 1, th: 1 } | |
578 | ); | |
579 | ||
580 | function isEditable( node ) { | |
581 | // Skip temporary elements, bookmarks and whitespaces. | |
582 | if ( isIgnored( node ) ) | |
583 | return false; | |
584 | ||
585 | if ( node.type == CKEDITOR.NODE_TEXT ) | |
586 | return true; | |
587 | ||
588 | if ( node.type == CKEDITOR.NODE_ELEMENT ) { | |
589 | // All inline and non-editable elements are valid editable places. | |
590 | // Note: the <hr> is currently the only element in CKEDITOR.dtd.$empty and CKEDITOR.dtd.$block, | |
591 | // but generally speaking we need an intersection of these two sets. | |
592 | // Note: non-editable block has to be treated differently (should be selected entirely). | |
593 | if ( node.is( CKEDITOR.dtd.$inline ) || node.is( 'hr' ) || node.getAttribute( 'contenteditable' ) == 'false' ) | |
594 | return true; | |
595 | ||
596 | // Empty blocks are editable on IE. | |
597 | if ( !CKEDITOR.env.needsBrFiller && node.is( validEmptyBlocks ) && isEmpty( node ) ) | |
598 | return true; | |
599 | } | |
600 | ||
601 | // Skip all other nodes. | |
602 | return false; | |
603 | } | |
604 | ||
605 | /** | |
606 | * Returns a function which checks whether the node can be a container or a sibling | |
607 | * of the selection end. | |
608 | * | |
609 | * This includes: | |
610 | * | |
611 | * * text nodes (but not whitespaces), | |
612 | * * inline elements, | |
613 | * * intersection of {@link CKEDITOR.dtd#$empty} and {@link CKEDITOR.dtd#$block} (currently | |
614 | * it is only `<hr>`), | |
615 | * * non-editable blocks (special case — such blocks cannot be containers nor | |
616 | * siblings, they need to be selected entirely), | |
617 | * * empty {@link #validEmptyBlockContainers blocks} which can contain text | |
618 | * ({@link CKEDITOR.env#needsBrFiller old IEs only}). | |
619 | * | |
620 | * @since 4.3 | |
621 | * @static | |
622 | * @param {Boolean} [isReject=false] Whether to return `false` for the | |
623 | * ignored element instead of `true` (default). | |
624 | * @returns {Function} | |
625 | */ | |
626 | CKEDITOR.dom.walker.editable = function( isReject ) { | |
627 | return function( node ) { | |
628 | return !!( isReject ^ isEditable( node ) ); | |
629 | }; | |
630 | }; | |
631 | ||
632 | /** | |
633 | * Checks if there is a filler node at the end of an element, and returns it. | |
634 | * | |
635 | * @member CKEDITOR.dom.element | |
636 | * @returns {CKEDITOR.dom.node/Boolean} Bogus node or `false`. | |
637 | */ | |
638 | CKEDITOR.dom.element.prototype.getBogus = function() { | |
639 | // Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (http://dev.ckeditor.com/ticket/7070). | |
640 | var tail = this; | |
641 | do { | |
642 | tail = tail.getPreviousSourceNode(); | |
643 | } | |
644 | while ( toSkip( tail ) ); | |
645 | ||
646 | if ( tail && ( CKEDITOR.env.needsBrFiller ? tail.is && tail.is( 'br' ) : tail.getText && tailNbspRegex.test( tail.getText() ) ) ) | |
647 | return tail; | |
648 | ||
649 | return false; | |
650 | }; | |
651 | ||
652 | } )(); |