]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blob - sources/core/editable.js
Validation initiale
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / core / editable.js
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 var isNotWhitespace, isNotBookmark, isEmpty, isBogus, emptyParagraphRegexp,
8 insert, fixTableAfterContentsDeletion, fixListAfterContentsDelete, getHtmlFromRangeHelpers, extractHtmlFromRangeHelpers;
9
10 /**
11 * Editable class which provides all editing related activities by
12 * the `contenteditable` element, dynamically get attached to editor instance.
13 *
14 * @class CKEDITOR.editable
15 * @extends CKEDITOR.dom.element
16 */
17 CKEDITOR.editable = CKEDITOR.tools.createClass( {
18 base: CKEDITOR.dom.element,
19 /**
20 * The constructor only stores generic editable creation logic that is commonly shared among
21 * all different editable elements.
22 *
23 * @constructor Creates an editable class instance.
24 * @param {CKEDITOR.editor} editor The editor instance on which the editable operates.
25 * @param {HTMLElement/CKEDITOR.dom.element} element Any DOM element that was as the editor's
26 * editing container, e.g. it could be either an HTML element with the `contenteditable` attribute
27 * set to the `true` that handles WYSIWYG editing or a `<textarea>` element that handles source editing.
28 */
29 $: function( editor, element ) {
30 // Transform the element into a CKEDITOR.dom.element instance.
31 this.base( element.$ || element );
32
33 this.editor = editor;
34
35 /**
36 * Indicates the initialization status of the editable element. The following statuses are available:
37 *
38 * * **unloaded** &ndash; the initial state. The editable's instance was created but
39 * is not fully loaded (in particular it has no data).
40 * * **ready** &ndash; the editable is fully initialized. The `ready` status is set after
41 * the first {@link CKEDITOR.editor#method-setData} is called.
42 * * **detached** &ndash; the editable was detached.
43 *
44 * @since 4.3.3
45 * @readonly
46 * @property {String}
47 */
48 this.status = 'unloaded';
49
50 /**
51 * Indicates whether the editable element gained focus.
52 *
53 * @property {Boolean} hasFocus
54 */
55 this.hasFocus = false;
56
57 // The bootstrapping logic.
58 this.setup();
59 },
60
61 proto: {
62 focus: function() {
63
64 var active;
65
66 // [Webkit] When DOM focus is inside of nested contenteditable elements,
67 // apply focus on the main editable will compromise it's text selection.
68 if ( CKEDITOR.env.webkit && !this.hasFocus ) {
69 // Restore focus on element which we cached (on selectionCheck) as previously active.
70 active = this.editor._.previousActive || this.getDocument().getActive();
71 if ( this.contains( active ) ) {
72 active.focus();
73 return;
74 }
75 }
76
77 // [Edge] Starting from EdgeHTML 14.14393, it does not support `setActive`. We need to use focus which
78 // causes unexpected scroll. Store scrollTop value so it can be restored after focusing editor.
79 // Scroll only happens if the editor is focused for the first time. (#14825)
80 if ( CKEDITOR.env.edge && CKEDITOR.env.version > 14 && !this.hasFocus && this.getDocument().equals( CKEDITOR.document ) ) {
81 this.editor._.previousScrollTop = this.$.scrollTop;
82 }
83
84 // [IE] Use instead "setActive" method to focus the editable if it belongs to the host page document,
85 // to avoid bringing an unexpected scroll.
86 try {
87 if ( CKEDITOR.env.ie && !( CKEDITOR.env.edge && CKEDITOR.env.version > 14 ) && this.getDocument().equals( CKEDITOR.document ) ) {
88 this.$.setActive();
89 } else {
90 this.$.focus();
91 }
92 } catch ( e ) {
93 // IE throws unspecified error when focusing editable after closing dialog opened on nested editable.
94 if ( !CKEDITOR.env.ie )
95 throw e;
96 }
97
98 // Remedy if Safari doens't applies focus properly. (#279)
99 if ( CKEDITOR.env.safari && !this.isInline() ) {
100 active = CKEDITOR.document.getActive();
101 if ( !active.equals( this.getWindow().getFrame() ) )
102 this.getWindow().focus();
103
104 }
105 },
106
107 /**
108 * Overrides {@link CKEDITOR.dom.element#on} to have special `focus/blur` handling.
109 * The `focusin/focusout` events are used in IE to replace regular `focus/blur` events
110 * because we want to avoid the asynchronous nature of later ones.
111 */
112 on: function( name, fn ) {
113 var args = Array.prototype.slice.call( arguments, 0 );
114
115 if ( CKEDITOR.env.ie && ( /^focus|blur$/ ).exec( name ) ) {
116 name = name == 'focus' ? 'focusin' : 'focusout';
117
118 // The "focusin/focusout" events bubbled, e.g. If there are elements with layout
119 // they fire this event when clicking in to edit them but it must be ignored
120 // to allow edit their contents. (#4682)
121 fn = isNotBubbling( fn, this );
122 args[ 0 ] = name;
123 args[ 1 ] = fn;
124 }
125
126 return CKEDITOR.dom.element.prototype.on.apply( this, args );
127 },
128
129 /**
130 * Registers an event listener that needs to be removed when detaching this editable.
131 * This means that it will be automatically removed when {@link #detach} is executed,
132 * for example on {@link CKEDITOR.editor#setMode changing editor mode} or destroying editor.
133 *
134 * Except for `obj` all other arguments have the same meaning as in {@link CKEDITOR.event#on}.
135 *
136 * This method is strongly related to the {@link CKEDITOR.editor#contentDom} and
137 * {@link CKEDITOR.editor#contentDomUnload} events, because they are fired
138 * when an editable is being attached and detached. Therefore, this method is usually used
139 * in the following way:
140 *
141 * editor.on( 'contentDom', function() {
142 * var editable = editor.editable();
143 * editable.attachListener( editable, 'mousedown', function() {
144 * // ...
145 * } );
146 * } );
147 *
148 * This code will attach the `mousedown` listener every time a new editable is attached
149 * to the editor, which in classic (`iframe`-based) editor happens every time the
150 * data or the mode is set. This listener will also be removed when that editable is detached.
151 *
152 * It is also possible to attach a listener to another object (e.g. to a document).
153 *
154 * editor.on( 'contentDom', function() {
155 * editor.editable().attachListener( editor.document, 'mousedown', function() {
156 * // ...
157 * } );
158 * } );
159 *
160 * @param {CKEDITOR.event} obj The element/object to which the listener will be attached. Every object
161 * which inherits from {@link CKEDITOR.event} may be used including {@link CKEDITOR.dom.element},
162 * {@link CKEDITOR.dom.document}, and {@link CKEDITOR.editable}.
163 * @param {String} eventName The name of the event that will be listened to.
164 * @param {Function} listenerFunction The function listening to the
165 * event. A single {@link CKEDITOR.eventInfo} object instance
166 * containing all the event data is passed to this function.
167 * @param {Object} [scopeObj] The object used to scope the listener
168 * call (the `this` object). If omitted, the current object is used.
169 * @param {Object} [listenerData] Data to be sent as the
170 * {@link CKEDITOR.eventInfo#listenerData} when calling the listener.
171 * @param {Number} [priority=10] The listener priority. Lower priority
172 * listeners are called first. Listeners with the same priority
173 * value are called in the registration order.
174 * @returns {Object} An object containing the `removeListener`
175 * function that can be used to remove the listener at any time.
176 */
177 attachListener: function( obj /*, event, fn, scope, listenerData, priority*/ ) {
178 !this._.listeners && ( this._.listeners = [] );
179 // Register the listener.
180 var args = Array.prototype.slice.call( arguments, 1 ),
181 listener = obj.on.apply( obj, args );
182
183 this._.listeners.push( listener );
184
185 return listener;
186 },
187
188 /**
189 * Remove all event listeners registered from {@link #attachListener}.
190 */
191 clearListeners: function() {
192 var listeners = this._.listeners;
193 // Don't get broken by this.
194 try {
195 while ( listeners.length )
196 listeners.pop().removeListener();
197 } catch ( e ) {}
198 },
199
200 /**
201 * Restore all attribution changes made by {@link #changeAttr }.
202 */
203 restoreAttrs: function() {
204 var changes = this._.attrChanges, orgVal;
205 for ( var attr in changes ) {
206 if ( changes.hasOwnProperty( attr ) ) {
207 orgVal = changes[ attr ];
208 // Restore original attribute.
209 orgVal !== null ? this.setAttribute( attr, orgVal ) : this.removeAttribute( attr );
210 }
211 }
212 },
213
214 /**
215 * Adds a CSS class name to this editable that needs to be removed on detaching.
216 *
217 * @param {String} className The class name to be added.
218 * @see CKEDITOR.dom.element#addClass
219 */
220 attachClass: function( cls ) {
221 var classes = this.getCustomData( 'classes' );
222 if ( !this.hasClass( cls ) ) {
223 !classes && ( classes = [] ), classes.push( cls );
224 this.setCustomData( 'classes', classes );
225 this.addClass( cls );
226 }
227 },
228
229 /**
230 * Make an attribution change that would be reverted on editable detaching.
231 * @param {String} attr The attribute name to be changed.
232 * @param {String} val The value of specified attribute.
233 */
234 changeAttr: function( attr, val ) {
235 var orgVal = this.getAttribute( attr );
236 if ( val !== orgVal ) {
237 !this._.attrChanges && ( this._.attrChanges = {} );
238
239 // Saved the original attribute val.
240 if ( !( attr in this._.attrChanges ) )
241 this._.attrChanges[ attr ] = orgVal;
242
243 this.setAttribute( attr, val );
244 }
245 },
246
247 /**
248 * Low-level method for inserting text into the editable.
249 * See the {@link CKEDITOR.editor#method-insertText} method which is the editor-level API
250 * for this purpose.
251 *
252 * @param {String} text
253 */
254 insertText: function( text ) {
255 // Focus the editor before calling transformPlainTextToHtml. (#12726)
256 this.editor.focus();
257 this.insertHtml( this.transformPlainTextToHtml( text ), 'text' );
258 },
259
260 /**
261 * Transforms plain text to HTML based on current selection and {@link CKEDITOR.editor#activeEnterMode}.
262 *
263 * @since 4.5
264 * @param {String} text Text to transform.
265 * @returns {String} HTML generated from the text.
266 */
267 transformPlainTextToHtml: function( text ) {
268 var enterMode = this.editor.getSelection().getStartElement().hasAscendant( 'pre', true ) ?
269 CKEDITOR.ENTER_BR :
270 this.editor.activeEnterMode;
271
272 return CKEDITOR.tools.transformPlainTextToHtml( text, enterMode );
273 },
274
275 /**
276 * Low-level method for inserting HTML into the editable.
277 * See the {@link CKEDITOR.editor#method-insertHtml} method which is the editor-level API
278 * for this purpose.
279 *
280 * This method will insert HTML into the current selection or a given range. It also creates an undo snapshot,
281 * scrolls the viewport to the insertion and selects the range next to the inserted content.
282 * If you want to insert HTML without additional operations use {@link #method-insertHtmlIntoRange}.
283 *
284 * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
285 *
286 * @param {String} data The HTML to be inserted.
287 * @param {String} [mode='html'] See {@link CKEDITOR.editor#method-insertHtml}'s param.
288 * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range
289 * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case).
290 * Introduced in CKEditor 4.5.
291 */
292 insertHtml: function( data, mode, range ) {
293 var editor = this.editor;
294
295 editor.focus();
296 editor.fire( 'saveSnapshot' );
297
298 if ( !range ) {
299 // HTML insertion only considers the first range.
300 // Note: getRanges will be overwritten for tests since we want to test
301 // custom ranges and bypass native selections.
302 range = editor.getSelection().getRanges()[ 0 ];
303 }
304
305 // Default mode is 'html'.
306 insert( this, mode || 'html', data, range );
307
308 // Make the final range selection.
309 range.select();
310
311 afterInsert( this );
312
313 this.editor.fire( 'afterInsertHtml', {} );
314 },
315
316 /**
317 * Inserts HTML into the position in the editor determined by the range.
318 *
319 * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects inserted
320 * HTML. If you want to do it, use {@link #method-insertHtml}.
321 *
322 * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
323 *
324 * @since 4.5
325 * @param {String} data HTML code to be inserted into the editor.
326 * @param {CKEDITOR.dom.range} range The range as a place of insertion.
327 * @param {String} [mode='html'] Mode in which HTML will be inserted.
328 * See {@link CKEDITOR.editor#method-insertHtml}.
329 */
330 insertHtmlIntoRange: function( data, range, mode ) {
331 // Default mode is 'html'
332 insert( this, mode || 'html', data, range );
333
334 this.editor.fire( 'afterInsertHtml', { intoRange: range } );
335 },
336
337 /**
338 * Low-level method for inserting an element into the editable.
339 * See the {@link CKEDITOR.editor#method-insertElement} method which is the editor-level API
340 * for this purpose.
341 *
342 * This method will insert the element into the current selection or a given range. It also creates an undo
343 * snapshot, scrolls the viewport to the insertion and selects the range next to the inserted content.
344 * If you want to insert an element without additional operations use {@link #method-insertElementIntoRange}.
345 *
346 * @param {CKEDITOR.dom.element} element The element to insert.
347 * @param {CKEDITOR.dom.range} [range] If specified, the element will be inserted into the range
348 * instead of into the selection.
349 */
350 insertElement: function( element, range ) {
351 var editor = this.editor;
352
353 // Prepare for the insertion. For example - focus editor (#11848).
354 editor.focus();
355 editor.fire( 'saveSnapshot' );
356
357 var enterMode = editor.activeEnterMode,
358 selection = editor.getSelection(),
359 elementName = element.getName(),
360 isBlock = CKEDITOR.dtd.$block[ elementName ];
361
362 if ( !range ) {
363 range = selection.getRanges()[ 0 ];
364 }
365
366 // Insert element into first range only and ignore the rest (#11183).
367 if ( this.insertElementIntoRange( element, range ) ) {
368 range.moveToPosition( element, CKEDITOR.POSITION_AFTER_END );
369
370 // If we're inserting a block element, the new cursor position must be
371 // optimized. (#3100,#5436,#8950)
372 if ( isBlock ) {
373 // Find next, meaningful element.
374 var next = element.getNext( function( node ) {
375 return isNotEmpty( node ) && !isBogus( node );
376 } );
377
378 if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.is( CKEDITOR.dtd.$block ) ) {
379 // If the next one is a text block, move cursor to the start of it's content.
380 if ( next.getDtd()[ '#' ] )
381 range.moveToElementEditStart( next );
382 // Otherwise move cursor to the before end of the last element.
383 else
384 range.moveToElementEditEnd( element );
385 }
386 // Open a new line if the block is inserted at the end of parent.
387 else if ( !next && enterMode != CKEDITOR.ENTER_BR ) {
388 next = range.fixBlock( true, enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' );
389 range.moveToElementEditStart( next );
390 }
391 }
392 }
393
394 // Set up the correct selection.
395 selection.selectRanges( [ range ] );
396
397 afterInsert( this );
398 },
399
400 /**
401 * Alias for {@link #insertElement}.
402 *
403 * @deprecated
404 * @param {CKEDITOR.dom.element} element The element to be inserted.
405 */
406 insertElementIntoSelection: function( element ) {
407 this.insertElement( element );
408 },
409
410 /**
411 * Inserts an element into the position in the editor determined by the range.
412 *
413 * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects the inserted
414 * element. If you want to do it, use the {@link #method-insertElement} method.
415 *
416 * @param {CKEDITOR.dom.element} element The element to be inserted.
417 * @param {CKEDITOR.dom.range} range The range as a place of insertion.
418 * @returns {Boolean} Informs whether the insertion was successful.
419 */
420 insertElementIntoRange: function( element, range ) {
421 var editor = this.editor,
422 enterMode = editor.config.enterMode,
423 elementName = element.getName(),
424 isBlock = CKEDITOR.dtd.$block[ elementName ];
425
426 if ( range.checkReadOnly() )
427 return false;
428
429 // Remove the original contents, merge split nodes.
430 range.deleteContents( 1 );
431
432 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT ) {
433 // If range is placed in intermediate element (not td or th), we need to do three things:
434 // * fill emptied <td/th>s with if browser needs them,
435 // * remove empty text nodes so IE8 won't crash
436 // (http://dev.ckeditor.com/ticket/11183#comment:8),
437 // * fix structure and move range into the <td/th> element.
438 if ( range.startContainer.is( { tr: 1, table: 1, tbody: 1, thead: 1, tfoot: 1 } ) ) {
439 fixTableAfterContentsDeletion( range );
440 } else if ( range.startContainer.is( CKEDITOR.dtd.$list ) ) {
441 // Similarly there's a need for lists.
442 fixListAfterContentsDelete( range );
443 }
444 }
445
446 // If we're inserting a block at dtd-violated position, split
447 // the parent blocks until we reach blockLimit.
448 var current, dtd;
449
450 if ( isBlock ) {
451 while ( ( current = range.getCommonAncestor( 0, 1 ) ) &&
452 ( dtd = CKEDITOR.dtd[ current.getName() ] ) &&
453 !( dtd && dtd[ elementName ] ) ) {
454 // Split up inline elements.
455 if ( current.getName() in CKEDITOR.dtd.span )
456 range.splitElement( current );
457
458 // If we're in an empty block which indicate a new paragraph,
459 // simply replace it with the inserting block.(#3664)
460 else if ( range.checkStartOfBlock() && range.checkEndOfBlock() ) {
461 range.setStartBefore( current );
462 range.collapse( true );
463 current.remove();
464 } else {
465 range.splitBlock( enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p', editor.editable() );
466 }
467 }
468 }
469
470 // Insert the new node.
471 range.insertNode( element );
472
473 // Return true if insertion was successful.
474 return true;
475 },
476
477 /**
478 * @see CKEDITOR.editor#setData
479 */
480 setData: function( data, isSnapshot ) {
481 if ( !isSnapshot )
482 data = this.editor.dataProcessor.toHtml( data );
483
484 this.setHtml( data );
485 this.fixInitialSelection();
486
487 // Editable is ready after first setData.
488 if ( this.status == 'unloaded' )
489 this.status = 'ready';
490
491 this.editor.fire( 'dataReady' );
492 },
493
494 /**
495 * @see CKEDITOR.editor#getData
496 */
497 getData: function( isSnapshot ) {
498 var data = this.getHtml();
499
500 if ( !isSnapshot )
501 data = this.editor.dataProcessor.toDataFormat( data );
502
503 return data;
504 },
505
506 /**
507 * Changes the read-only state of this editable.
508 *
509 * @param {Boolean} isReadOnly
510 */
511 setReadOnly: function( isReadOnly ) {
512 this.setAttribute( 'contenteditable', !isReadOnly );
513 },
514
515 /**
516 * Detaches this editable object from the DOM (removes classes, listeners, etc.)
517 */
518 detach: function() {
519 // Cleanup the element.
520 this.removeClass( 'cke_editable' );
521
522 this.status = 'detached';
523
524 // Save the editor reference which will be lost after
525 // calling detach from super class.
526 var editor = this.editor;
527
528 this._.detach();
529
530 delete editor.document;
531 delete editor.window;
532 },
533
534 /**
535 * Checks if the editable is one of the host page elements, indicates
536 * an inline editing environment.
537 *
538 * @returns {Boolean}
539 */
540 isInline: function() {
541 return this.getDocument().equals( CKEDITOR.document );
542 },
543
544 /**
545 * Fixes the selection and focus which may be in incorrect state after
546 * editable's inner HTML was overwritten.
547 *
548 * If the editable did not have focus, then the selection will be fixed when the editable
549 * is focused for the first time. If the editable already had focus, then the selection will
550 * be fixed immediately.
551 *
552 * To understand the problem see:
553 *
554 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
555 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusafterundoing
556 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/selectionafterfocusing
557 * * http://tests.ckeditor.dev:1030/tests/plugins/newpage/manual/selectionafternewpage
558 *
559 * @since 4.4.6
560 * @private
561 */
562 fixInitialSelection: function() {
563 var that = this;
564
565 // Deal with IE8- IEQM (the old MS selection) first.
566 if ( CKEDITOR.env.ie && ( CKEDITOR.env.version < 9 || CKEDITOR.env.quirks ) ) {
567 if ( this.hasFocus ) {
568 this.focus();
569 fixMSSelection();
570 }
571
572 return;
573 }
574
575 // If editable did not have focus, fix the selection when it is first focused.
576 if ( !this.hasFocus ) {
577 this.once( 'focus', function() {
578 fixSelection();
579 }, null, null, -999 );
580 // If editable had focus, fix the selection immediately.
581 } else {
582 this.focus();
583 fixSelection();
584 }
585
586 function fixSelection() {
587 var $doc = that.getDocument().$,
588 $sel = $doc.getSelection();
589
590 if ( requiresFix( $sel ) ) {
591 var range = new CKEDITOR.dom.range( that );
592 range.moveToElementEditStart( that );
593
594 var $range = $doc.createRange();
595 $range.setStart( range.startContainer.$, range.startOffset );
596 $range.collapse( true );
597
598 $sel.removeAllRanges();
599 $sel.addRange( $range );
600 }
601 }
602
603 function requiresFix( $sel ) {
604 // This condition covers most broken cases after setting data.
605 if ( $sel.anchorNode && $sel.anchorNode == that.$ ) {
606 return true;
607 }
608
609 // Fix for:
610 // http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
611 // (the inline editor TC)
612 if ( CKEDITOR.env.webkit ) {
613 var active = that.getDocument().getActive();
614 if ( active && active.equals( that ) && !$sel.anchorNode ) {
615 return true;
616 }
617 }
618 }
619
620 function fixMSSelection() {
621 var $doc = that.getDocument().$,
622 $sel = $doc.selection,
623 active = that.getDocument().getActive();
624
625 if ( $sel.type == 'None' && active.equals( that ) ) {
626 var range = new CKEDITOR.dom.range( that ),
627 parentElement,
628 $range = $doc.body.createTextRange();
629
630 range.moveToElementEditStart( that );
631
632 parentElement = range.startContainer;
633 if ( parentElement.type != CKEDITOR.NODE_ELEMENT ) {
634 parentElement = parentElement.getParent();
635 }
636
637 $range.moveToElementText( parentElement.$ );
638 $range.collapse( true );
639 $range.select();
640 }
641 }
642 },
643
644 /**
645 * The base of the {@link CKEDITOR.editor#getSelectedHtml} method.
646 *
647 * @since 4.5
648 * @method getHtmlFromRange
649 * @param {CKEDITOR.dom.range} range
650 * @returns {CKEDITOR.dom.documentFragment}
651 */
652 getHtmlFromRange: function( range ) {
653 // There's nothing to return if range is collapsed.
654 if ( range.collapsed )
655 return new CKEDITOR.dom.documentFragment( range.document );
656
657 // Info object passed between methods.
658 var that = {
659 doc: this.getDocument(),
660 // Leave original range object untouched.
661 range: range.clone()
662 };
663
664 getHtmlFromRangeHelpers.eol.detect( that, this );
665 getHtmlFromRangeHelpers.bogus.exclude( that );
666 getHtmlFromRangeHelpers.cell.shrink( that );
667
668 that.fragment = that.range.cloneContents();
669
670 getHtmlFromRangeHelpers.tree.rebuild( that, this );
671 getHtmlFromRangeHelpers.eol.fix( that, this );
672
673 return new CKEDITOR.dom.documentFragment( that.fragment.$ );
674 },
675
676 /**
677 * The base of the {@link CKEDITOR.editor#extractSelectedHtml} method.
678 *
679 * **Note:** The range is modified so it matches the desired selection after extraction
680 * even though the selection is not made.
681 *
682 * @since 4.5
683 * @param {CKEDITOR.dom.range} range
684 * @param {Boolean} [removeEmptyBlock=false] See {@link CKEDITOR.editor#extractSelectedHtml}'s parameter.
685 * Note that the range will not be modified if this parameter is set to `true`.
686 * @returns {CKEDITOR.dom.documentFragment} The extracted fragment of the editable content.
687 */
688 extractHtmlFromRange: function( range, removeEmptyBlock ) {
689 var helpers = extractHtmlFromRangeHelpers,
690 that = {
691 range: range,
692 doc: range.document
693 },
694 // Since it is quite hard to build a valid documentFragment
695 // out of extracted contents because DOM changes, let's mimic
696 // extracted HTML with #getHtmlFromRange. Yep. It's a hack.
697 extractedFragment = this.getHtmlFromRange( range );
698
699 // Collapsed range means that there's nothing to extract.
700 if ( range.collapsed ) {
701 range.optimize();
702 return extractedFragment;
703 }
704
705 // Include inline element if possible.
706 range.enlarge( CKEDITOR.ENLARGE_INLINE, 1 );
707
708 // This got to be done before bookmarks are created because purging
709 // depends on the position of the range at the boundaries of the table,
710 // usually distorted by bookmark spans.
711 helpers.table.detectPurge( that );
712
713 // We'll play with DOM, let's hold the position of the range.
714 that.bookmark = range.createBookmark();
715 // While bookmarked, make unaccessible, to make sure that none of the methods
716 // will try to use it (they should use that.bookmark).
717 // This is done because ranges get desynchronized with the DOM when more bookmarks
718 // is created (as for instance that.targetBookmark).
719 delete that.range;
720
721 // The range to be restored after extraction should be kept
722 // outside of the range, so it's not removed by range.extractContents.
723 var targetRange = this.editor.createRange();
724 targetRange.moveToPosition( that.bookmark.startNode, CKEDITOR.POSITION_BEFORE_START );
725 that.targetBookmark = targetRange.createBookmark();
726
727 // Execute content-specific detections.
728 helpers.list.detectMerge( that, this );
729 helpers.table.detectRanges( that, this );
730 helpers.block.detectMerge( that, this );
731
732 // Simply, do the job.
733 if ( that.tableContentsRanges ) {
734 helpers.table.deleteRanges( that );
735
736 // Done here only to remove bookmark's spans.
737 range.moveToBookmark( that.bookmark );
738 that.range = range;
739 } else {
740 // To use the range we need to restore the bookmark and make
741 // the range accessible again.
742 range.moveToBookmark( that.bookmark );
743 that.range = range;
744 range.extractContents( helpers.detectExtractMerge( that ) );
745 }
746
747 // Move working range to desired, pre-computed position.
748 range.moveToBookmark( that.targetBookmark );
749
750 // Make sure range is always anchored in an element. For consistency.
751 range.optimize();
752
753 // It my happen that the uncollapsed range which referred to a valid selection,
754 // will be placed in an uneditable location after being collapsed:
755 // <tr>[<td>x</td>]</tr> -> <tr>[]<td>x</td></tr> -> <tr><td>[]x</td></tr>
756 helpers.fixUneditableRangePosition( range );
757
758 // Execute content-specific post-extract routines.
759 helpers.list.merge( that, this );
760 helpers.table.purge( that, this );
761 helpers.block.merge( that, this );
762
763 // Remove empty block, duh!
764 if ( removeEmptyBlock ) {
765 var path = range.startPath();
766
767 // <p><b>^</b></p> is empty block.
768 if (
769 range.checkStartOfBlock() &&
770 range.checkEndOfBlock() &&
771 path.block &&
772 !range.root.equals( path.block ) &&
773 // Do not remove a block with bookmarks. (#13465)
774 !hasBookmarks( path.block ) ) {
775 range.moveToPosition( path.block, CKEDITOR.POSITION_BEFORE_START );
776 path.block.remove();
777 }
778 } else {
779 // Auto paragraph, if needed.
780 helpers.autoParagraph( this.editor, range );
781
782 // Let's have a bogus next to the caret, if needed.
783 if ( isEmpty( range.startContainer ) )
784 range.startContainer.appendBogus();
785 }
786
787 // Merge inline siblings if any around the caret.
788 range.startContainer.mergeSiblings();
789
790 return extractedFragment;
791 },
792
793 /**
794 * Editable element bootstrapping.
795 *
796 * @private
797 */
798 setup: function() {
799 var editor = this.editor;
800
801 // Handle the load/read of editor data/snapshot.
802 this.attachListener( editor, 'beforeGetData', function() {
803 var data = this.getData();
804
805 // Post processing html output of wysiwyg editable.
806 if ( !this.is( 'textarea' ) ) {
807 // Reset empty if the document contains only one empty paragraph.
808 if ( editor.config.ignoreEmptyParagraph !== false )
809 data = data.replace( emptyParagraphRegexp, function( match, lookback ) {
810 return lookback;
811 } );
812 }
813
814 editor.setData( data, null, 1 );
815 }, this );
816
817 this.attachListener( editor, 'getSnapshot', function( evt ) {
818 evt.data = this.getData( 1 );
819 }, this );
820
821 this.attachListener( editor, 'afterSetData', function() {
822 this.setData( editor.getData( 1 ) );
823 }, this );
824 this.attachListener( editor, 'loadSnapshot', function( evt ) {
825 this.setData( evt.data, 1 );
826 }, this );
827
828 // Delegate editor focus/blur to editable.
829 this.attachListener( editor, 'beforeFocus', function() {
830 var sel = editor.getSelection(),
831 ieSel = sel && sel.getNative();
832
833 // IE considers control-type element as separate
834 // focus host when selected, avoid destroying the
835 // selection in such case. (#5812) (#8949)
836 if ( ieSel && ieSel.type == 'Control' )
837 return;
838
839 this.focus();
840 }, this );
841
842 this.attachListener( editor, 'insertHtml', function( evt ) {
843 this.insertHtml( evt.data.dataValue, evt.data.mode, evt.data.range );
844 }, this );
845 this.attachListener( editor, 'insertElement', function( evt ) {
846 this.insertElement( evt.data );
847 }, this );
848 this.attachListener( editor, 'insertText', function( evt ) {
849 this.insertText( evt.data );
850 }, this );
851
852 // Update editable state.
853 this.setReadOnly( editor.readOnly );
854
855 // The editable class.
856 this.attachClass( 'cke_editable' );
857
858 // The element mode css class.
859 if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) {
860 this.attachClass( 'cke_editable_inline' );
861 } else if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ||
862 editor.elementMode == CKEDITOR.ELEMENT_MODE_APPENDTO ) {
863 this.attachClass( 'cke_editable_themed' );
864 }
865
866 this.attachClass( 'cke_contents_' + editor.config.contentsLangDirection );
867
868 // Setup editor keystroke handlers on this element.
869 var keystrokeHandler = editor.keystrokeHandler;
870
871 // If editor is read-only, then make sure that BACKSPACE key
872 // is blocked to prevent browser history navigation.
873 keystrokeHandler.blockedKeystrokes[ 8 ] = +editor.readOnly;
874
875 editor.keystrokeHandler.attach( this );
876
877 // Update focus states.
878 this.on( 'blur', function() {
879 this.hasFocus = false;
880 }, null, null, -1 );
881
882 this.on( 'focus', function() {
883 this.hasFocus = true;
884 }, null, null, -1 );
885
886 if ( CKEDITOR.env.webkit ) {
887 // [WebKit] Save scrollTop value so it can be used when restoring locked selection. (#14659)
888 this.on( 'scroll', function() {
889 editor._.previousScrollTop = editor.editable().$.scrollTop;
890 }, null, null, -1 );
891 }
892
893 // [Edge] This is the other part of the workaround for Edge which restores saved
894 // scrollTop value and removes listener which is not needed anymore. (#14825)
895 if ( CKEDITOR.env.edge && CKEDITOR.env.version > 14 ) {
896
897 var fixScrollOnFocus = function() {
898 var editable = editor.editable();
899
900 if ( editor._.previousScrollTop != null && editable.getDocument().equals( CKEDITOR.document ) ) {
901 editable.$.scrollTop = editor._.previousScrollTop;
902 editor._.previousScrollTop = null;
903 this.removeListener( 'scroll', fixScrollOnFocus );
904 }
905 };
906
907 this.on( 'scroll', fixScrollOnFocus );
908 }
909
910 // Register to focus manager.
911 editor.focusManager.add( this );
912
913 // Inherit the initial focus on editable element.
914 if ( this.equals( CKEDITOR.document.getActive() ) ) {
915 this.hasFocus = true;
916 // Pending until this editable has attached.
917 editor.once( 'contentDom', function() {
918 editor.focusManager.focus( this );
919 }, this );
920 }
921
922 // Apply tab index on demand, with original direction saved.
923 if ( this.isInline() ) {
924
925 // tabIndex of the editable is different than editor's one.
926 // Update the attribute of the editable.
927 this.changeAttr( 'tabindex', editor.tabIndex );
928 }
929
930 // The above is all we'll be doing for a <textarea> editable.
931 if ( this.is( 'textarea' ) )
932 return;
933
934 // The DOM document which the editing acts upon.
935 editor.document = this.getDocument();
936 editor.window = this.getWindow();
937
938 var doc = editor.document;
939
940 this.changeAttr( 'spellcheck', !editor.config.disableNativeSpellChecker );
941
942 // Apply contents direction on demand, with original direction saved.
943 var dir = editor.config.contentsLangDirection;
944 if ( this.getDirection( 1 ) != dir )
945 this.changeAttr( 'dir', dir );
946
947 // Create the content stylesheet for this document.
948 var styles = CKEDITOR.getCss();
949 if ( styles ) {
950 var head = doc.getHead(),
951 stylesElement = head.getCustomData( 'stylesheet' );
952
953 if ( !stylesElement ) {
954 var sheet = doc.appendStyleText( styles );
955 sheet = new CKEDITOR.dom.element( sheet.ownerNode || sheet.owningElement );
956 head.setCustomData( 'stylesheet', sheet );
957 sheet.data( 'cke-temp', 1 );
958 } else if ( styles != stylesElement.getText() ) {
959 CKEDITOR.env.ie && CKEDITOR.env.version < 9 ? stylesElement.$.styleSheet.cssText = styles : stylesElement.setText( styles );
960 }
961 }
962
963 // Update the stylesheet sharing count.
964 var ref = doc.getCustomData( 'stylesheet_ref' ) || 0;
965 doc.setCustomData( 'stylesheet_ref', ref + 1 );
966
967 // Pass this configuration to styles system.
968 this.setCustomData( 'cke_includeReadonly', !editor.config.disableReadonlyStyling );
969
970 // Prevent the browser opening read-only links. (#6032 & #10912)
971 this.attachListener( this, 'click', function( evt ) {
972 evt = evt.data;
973
974 var link = new CKEDITOR.dom.elementPath( evt.getTarget(), this ).contains( 'a' );
975
976 if ( link && evt.$.button != 2 && link.isReadOnly() )
977 evt.preventDefault();
978 } );
979
980 var backspaceOrDelete = { 8: 1, 46: 1 };
981
982 // Override keystrokes which should have deletion behavior
983 // on fully selected element . (#4047) (#7645)
984 this.attachListener( editor, 'key', function( evt ) {
985 if ( editor.readOnly )
986 return true;
987
988 // Use getKey directly in order to ignore modifiers.
989 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
990 var keyCode = evt.data.domEvent.getKey(),
991 isHandled;
992
993 // Backspace OR Delete.
994 if ( keyCode in backspaceOrDelete ) {
995 var sel = editor.getSelection(),
996 selected,
997 range = sel.getRanges()[ 0 ],
998 path = range.startPath(),
999 block,
1000 parent,
1001 next,
1002 rtl = keyCode == 8;
1003
1004 if (
1005 // [IE<11] Remove selected image/anchor/etc here to avoid going back in history. (#10055)
1006 ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 && ( selected = sel.getSelectedElement() ) ) ||
1007 // Remove the entire list/table on fully selected content. (#7645)
1008 ( selected = getSelectedTableList( sel ) ) ) {
1009 // Make undo snapshot.
1010 editor.fire( 'saveSnapshot' );
1011
1012 // Delete any element that 'hasLayout' (e.g. hr,table) in IE8 will
1013 // break up the selection, safely manage it here. (#4795)
1014 range.moveToPosition( selected, CKEDITOR.POSITION_BEFORE_START );
1015 // Remove the control manually.
1016 selected.remove();
1017 range.select();
1018
1019 editor.fire( 'saveSnapshot' );
1020
1021 isHandled = 1;
1022 } else if ( range.collapsed ) {
1023 // Handle the following special cases: (#6217)
1024 // 1. Del/Backspace key before/after table;
1025 // 2. Backspace Key after start of table.
1026 if ( ( block = path.block ) &&
1027 ( next = block[ rtl ? 'getPrevious' : 'getNext' ]( isNotWhitespace ) ) &&
1028 ( next.type == CKEDITOR.NODE_ELEMENT ) &&
1029 next.is( 'table' ) &&
1030 range[ rtl ? 'checkStartOfBlock' : 'checkEndOfBlock' ]() ) {
1031 editor.fire( 'saveSnapshot' );
1032
1033 // Remove the current empty block.
1034 if ( range[ rtl ? 'checkEndOfBlock' : 'checkStartOfBlock' ]() )
1035 block.remove();
1036
1037 // Move cursor to the beginning/end of table cell.
1038 range[ 'moveToElementEdit' + ( rtl ? 'End' : 'Start' ) ]( next );
1039 range.select();
1040
1041 editor.fire( 'saveSnapshot' );
1042
1043 isHandled = 1;
1044 }
1045 else if ( path.blockLimit && path.blockLimit.is( 'td' ) &&
1046 ( parent = path.blockLimit.getAscendant( 'table' ) ) &&
1047 range.checkBoundaryOfElement( parent, rtl ? CKEDITOR.START : CKEDITOR.END ) &&
1048 ( next = parent[ rtl ? 'getPrevious' : 'getNext' ]( isNotWhitespace ) ) ) {
1049 editor.fire( 'saveSnapshot' );
1050
1051 // Move cursor to the end of previous block.
1052 range[ 'moveToElementEdit' + ( rtl ? 'End' : 'Start' ) ]( next );
1053
1054 // Remove any previous empty block.
1055 if ( range.checkStartOfBlock() && range.checkEndOfBlock() )
1056 next.remove();
1057 else
1058 range.select();
1059
1060 editor.fire( 'saveSnapshot' );
1061
1062 isHandled = 1;
1063 }
1064 // BACKSPACE/DEL pressed at the start/end of table cell.
1065 else if ( ( parent = path.contains( [ 'td', 'th', 'caption' ] ) ) &&
1066 range.checkBoundaryOfElement( parent, rtl ? CKEDITOR.START : CKEDITOR.END ) ) {
1067 isHandled = 1;
1068 }
1069 }
1070
1071 }
1072
1073 return !isHandled;
1074 } );
1075
1076 // On IE>=11 we need to fill blockless editable with <br> if it was deleted.
1077 if ( editor.blockless && CKEDITOR.env.ie && CKEDITOR.env.needsBrFiller ) {
1078 this.attachListener( this, 'keyup', function( evt ) {
1079 if ( evt.data.getKeystroke() in backspaceOrDelete && !this.getFirst( isNotEmpty ) ) {
1080 this.appendBogus();
1081
1082 // Set the selection before bogus, because IE tends to put it after.
1083 var range = editor.createRange();
1084 range.moveToPosition( this, CKEDITOR.POSITION_AFTER_START );
1085 range.select();
1086 }
1087 } );
1088 }
1089
1090 this.attachListener( this, 'dblclick', function( evt ) {
1091 if ( editor.readOnly )
1092 return false;
1093
1094 var data = { element: evt.data.getTarget() };
1095 editor.fire( 'doubleclick', data );
1096 } );
1097
1098 // Prevent automatic submission in IE #6336
1099 CKEDITOR.env.ie && this.attachListener( this, 'click', blockInputClick );
1100
1101 // Gecko/Webkit need some help when selecting control type elements. (#3448)
1102 // We apply same behavior for IE Edge. (#13386)
1103 if ( !CKEDITOR.env.ie || CKEDITOR.env.edge ) {
1104 this.attachListener( this, 'mousedown', function( ev ) {
1105 var control = ev.data.getTarget();
1106 // #11727. Note: htmlDP assures that input/textarea/select have contenteditable=false
1107 // attributes. However, they also have data-cke-editable attribute, so isReadOnly() returns false,
1108 // and therefore those elements are correctly selected by this code.
1109 if ( control.is( 'img', 'hr', 'input', 'textarea', 'select' ) && !control.isReadOnly() ) {
1110 editor.getSelection().selectElement( control );
1111
1112 // Prevent focus from stealing from the editable. (#9515)
1113 if ( control.is( 'input', 'textarea', 'select' ) )
1114 ev.data.preventDefault();
1115 }
1116 } );
1117 }
1118
1119 // For some reason, after click event is done, IE Edge loses focus on the selected element. (#13386)
1120 if ( CKEDITOR.env.edge ) {
1121 this.attachListener( this, 'mouseup', function( ev ) {
1122 var selectedElement = ev.data.getTarget();
1123 if ( selectedElement && selectedElement.is( 'img' ) ) {
1124 editor.getSelection().selectElement( selectedElement );
1125 }
1126 } );
1127 }
1128
1129 // Prevent right click from selecting an empty block even
1130 // when selection is anchored inside it. (#5845)
1131 if ( CKEDITOR.env.gecko ) {
1132 this.attachListener( this, 'mouseup', function( ev ) {
1133 if ( ev.data.$.button == 2 ) {
1134 var target = ev.data.getTarget();
1135
1136 if ( !target.getOuterHtml().replace( emptyParagraphRegexp, '' ) ) {
1137 var range = editor.createRange();
1138 range.moveToElementEditStart( target );
1139 range.select( true );
1140 }
1141 }
1142 } );
1143 }
1144
1145 // Webkit: avoid from editing form control elements content.
1146 if ( CKEDITOR.env.webkit ) {
1147 // Prevent from tick checkbox/radiobox/select
1148 this.attachListener( this, 'click', function( ev ) {
1149 if ( ev.data.getTarget().is( 'input', 'select' ) )
1150 ev.data.preventDefault();
1151 } );
1152
1153 // Prevent from editig textfield/textarea value.
1154 this.attachListener( this, 'mouseup', function( ev ) {
1155 if ( ev.data.getTarget().is( 'input', 'textarea' ) )
1156 ev.data.preventDefault();
1157 } );
1158 }
1159
1160 // Prevent Webkit/Blink from going rogue when joining
1161 // blocks on BACKSPACE/DEL (#11861,#9998).
1162 if ( CKEDITOR.env.webkit ) {
1163 this.attachListener( editor, 'key', function( evt ) {
1164 if ( editor.readOnly ) {
1165 return true;
1166 }
1167
1168 // Use getKey directly in order to ignore modifiers.
1169 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
1170 var key = evt.data.domEvent.getKey();
1171
1172 if ( !( key in backspaceOrDelete ) )
1173 return;
1174
1175 var backspace = key == 8,
1176 range = editor.getSelection().getRanges()[ 0 ],
1177 startPath = range.startPath();
1178
1179 if ( range.collapsed ) {
1180 if ( !mergeBlocksCollapsedSelection( editor, range, backspace, startPath ) )
1181 return;
1182 } else {
1183 if ( !mergeBlocksNonCollapsedSelection( editor, range, startPath ) )
1184 return;
1185 }
1186
1187 // Scroll to the new position of the caret (#11960).
1188 editor.getSelection().scrollIntoView();
1189 editor.fire( 'saveSnapshot' );
1190
1191 return false;
1192 }, this, null, 100 ); // Later is better – do not override existing listeners.
1193 }
1194 }
1195 },
1196
1197 _: {
1198 detach: function() {
1199 // Update the editor cached data with current data.
1200 this.editor.setData( this.editor.getData(), 0, 1 );
1201
1202 this.clearListeners();
1203 this.restoreAttrs();
1204
1205 // Cleanup our custom classes.
1206 var classes;
1207 if ( ( classes = this.removeCustomData( 'classes' ) ) ) {
1208 while ( classes.length )
1209 this.removeClass( classes.pop() );
1210 }
1211
1212 // Remove contents stylesheet from document if it's the last usage.
1213 if ( !this.is( 'textarea' ) ) {
1214 var doc = this.getDocument(),
1215 head = doc.getHead();
1216 if ( head.getCustomData( 'stylesheet' ) ) {
1217 var refs = doc.getCustomData( 'stylesheet_ref' );
1218 if ( !( --refs ) ) {
1219 doc.removeCustomData( 'stylesheet_ref' );
1220 var sheet = head.removeCustomData( 'stylesheet' );
1221 sheet.remove();
1222 } else {
1223 doc.setCustomData( 'stylesheet_ref', refs );
1224 }
1225 }
1226 }
1227
1228 this.editor.fire( 'contentDomUnload' );
1229
1230 // Free up the editor reference.
1231 delete this.editor;
1232 }
1233 }
1234 } );
1235
1236 /**
1237 * Creates, retrieves or detaches an editable element of the editor.
1238 * This method should always be used instead of calling {@link CKEDITOR.editable} directly.
1239 *
1240 * @method editable
1241 * @member CKEDITOR.editor
1242 * @param {CKEDITOR.dom.element/CKEDITOR.editable} [elementOrEditable] The
1243 * DOM element to become the editable or a {@link CKEDITOR.editable} object.
1244 */
1245 CKEDITOR.editor.prototype.editable = function( element ) {
1246 var editable = this._.editable;
1247
1248 // This editor has already associated with
1249 // an editable element, silently fails.
1250 if ( editable && element )
1251 return 0;
1252
1253 if ( arguments.length ) {
1254 editable = this._.editable = element ? ( element instanceof CKEDITOR.editable ? element : new CKEDITOR.editable( this, element ) ) :
1255 // Detach the editable from editor.
1256 ( editable && editable.detach(), null );
1257 }
1258
1259 // Just retrieve the editable.
1260 return editable;
1261 };
1262
1263 CKEDITOR.on( 'instanceLoaded', function( evt ) {
1264 var editor = evt.editor;
1265
1266 // and flag that the element was locked by our code so it'll be editable by the editor functions (#6046).
1267 editor.on( 'insertElement', function( evt ) {
1268 var element = evt.data;
1269 if ( element.type == CKEDITOR.NODE_ELEMENT && ( element.is( 'input' ) || element.is( 'textarea' ) ) ) {
1270 // // The element is still not inserted yet, force attribute-based check.
1271 if ( element.getAttribute( 'contentEditable' ) != 'false' )
1272 element.data( 'cke-editable', element.hasAttribute( 'contenteditable' ) ? 'true' : '1' );
1273 element.setAttribute( 'contentEditable', false );
1274 }
1275 } );
1276
1277 editor.on( 'selectionChange', function( evt ) {
1278 if ( editor.readOnly )
1279 return;
1280
1281 // Auto fixing on some document structure weakness to enhance usabilities. (#3190 and #3189)
1282 var sel = editor.getSelection();
1283 // Do it only when selection is not locked. (#8222)
1284 if ( sel && !sel.isLocked ) {
1285 var isDirty = editor.checkDirty();
1286
1287 // Lock undoM before touching DOM to prevent
1288 // recording these changes as separate snapshot.
1289 editor.fire( 'lockSnapshot' );
1290 fixDom( evt );
1291 editor.fire( 'unlockSnapshot' );
1292
1293 !isDirty && editor.resetDirty();
1294 }
1295 } );
1296 } );
1297
1298 CKEDITOR.on( 'instanceCreated', function( evt ) {
1299 var editor = evt.editor;
1300
1301 editor.on( 'mode', function() {
1302
1303 var editable = editor.editable();
1304
1305 // Setup proper ARIA roles and properties for inline editable, classic
1306 // (iframe-based) editable is instead handled by plugin.
1307 if ( editable && editable.isInline() ) {
1308
1309 var ariaLabel = editor.title;
1310
1311 editable.changeAttr( 'role', 'textbox' );
1312 editable.changeAttr( 'aria-label', ariaLabel );
1313
1314 if ( ariaLabel )
1315 editable.changeAttr( 'title', ariaLabel );
1316
1317 var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
1318 if ( helpLabel ) {
1319 // Put the voice label in different spaces, depending on element mode, so
1320 // the DOM element get auto detached on mode reload or editor destroy.
1321 var ct = this.ui.space( this.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ? 'top' : 'contents' );
1322 if ( ct ) {
1323 var ariaDescId = CKEDITOR.tools.getNextId(),
1324 desc = CKEDITOR.dom.element.createFromHtml( '<span id="' + ariaDescId + '" class="cke_voice_label">' + helpLabel + '</span>' );
1325 ct.append( desc );
1326 editable.changeAttr( 'aria-describedby', ariaDescId );
1327 }
1328 }
1329 }
1330 } );
1331 } );
1332
1333 // #9222: Show text cursor in Gecko.
1334 // Show default cursor over control elements on all non-IEs.
1335 CKEDITOR.addCss( '.cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}' );
1336
1337 //
1338 //
1339 // Bazillion helpers for the editable class and above listeners.
1340 //
1341 //
1342
1343 isNotWhitespace = CKEDITOR.dom.walker.whitespaces( true ),
1344 isNotBookmark = CKEDITOR.dom.walker.bookmark( false, true ),
1345 isEmpty = CKEDITOR.dom.walker.empty(),
1346 isBogus = CKEDITOR.dom.walker.bogus(),
1347 // Matching an empty paragraph at the end of document.
1348 emptyParagraphRegexp = /(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>|&nbsp;|\u00A0|&#160;)?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi;
1349
1350 // Auto-fixing block-less content by wrapping paragraph (#3190), prevent
1351 // non-exitable-block by padding extra br.(#3189)
1352 // Returns truly value when dom was changed, falsy otherwise.
1353 function fixDom( evt ) {
1354 var editor = evt.editor,
1355 path = evt.data.path,
1356 blockLimit = path.blockLimit,
1357 selection = evt.data.selection,
1358 range = selection.getRanges()[ 0 ],
1359 selectionUpdateNeeded;
1360
1361 if ( CKEDITOR.env.gecko || ( CKEDITOR.env.ie && CKEDITOR.env.needsBrFiller ) ) {
1362 var blockNeedsFiller = needsBrFiller( selection, path );
1363 if ( blockNeedsFiller ) {
1364 blockNeedsFiller.appendBogus();
1365 // IE tends to place selection after appended bogus, so we need to
1366 // select the original range (placed before bogus).
1367 selectionUpdateNeeded = CKEDITOR.env.ie;
1368 }
1369 }
1370
1371 // When we're in block enter mode, a new paragraph will be established
1372 // to encapsulate inline contents inside editable. (#3657)
1373 // Don't autoparagraph if browser (namely - IE) incorrectly anchored selection
1374 // inside non-editable content. This happens e.g. if non-editable block is the only
1375 // content of editable.
1376 if ( shouldAutoParagraph( editor, path.block, blockLimit ) && range.collapsed && !range.getCommonAncestor().isReadOnly() ) {
1377 var testRng = range.clone();
1378 testRng.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
1379 var walker = new CKEDITOR.dom.walker( testRng );
1380 walker.guard = function( node ) {
1381 return !isNotEmpty( node ) ||
1382 node.type == CKEDITOR.NODE_COMMENT ||
1383 node.isReadOnly();
1384 };
1385
1386 // 1. Inline content discovered under cursor;
1387 // 2. Empty editable.
1388 if ( !walker.checkForward() || testRng.checkStartOfBlock() && testRng.checkEndOfBlock() ) {
1389 var fixedBlock = range.fixBlock( true, editor.activeEnterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' );
1390
1391 // For IE<11, we should remove any filler node which was introduced before.
1392 if ( !CKEDITOR.env.needsBrFiller ) {
1393 var first = fixedBlock.getFirst( isNotEmpty );
1394 if ( first && isNbsp( first ) )
1395 first.remove();
1396 }
1397
1398 selectionUpdateNeeded = 1;
1399
1400 // Cancel this selection change in favor of the next (correct). (#6811)
1401 evt.cancel();
1402 }
1403 }
1404
1405 if ( selectionUpdateNeeded )
1406 range.select();
1407 }
1408
1409 // Checks whether current selection requires br filler to be appended.
1410 // @returns Block which needs filler or falsy value.
1411 function needsBrFiller( selection, path ) {
1412 // Fake selection does not need filler, because it is fake.
1413 if ( selection.isFake )
1414 return 0;
1415
1416 // Ensure bogus br could help to move cursor (out of styles) to the end of block. (#7041)
1417 var pathBlock = path.block || path.blockLimit,
1418 lastNode = pathBlock && pathBlock.getLast( isNotEmpty );
1419
1420 // Check some specialities of the current path block:
1421 // 1. It is really displayed as block; (#7221)
1422 // 2. It doesn't end with one inner block; (#7467)
1423 // 3. It doesn't have bogus br yet.
1424 if (
1425 pathBlock && pathBlock.isBlockBoundary() &&
1426 !( lastNode && lastNode.type == CKEDITOR.NODE_ELEMENT && lastNode.isBlockBoundary() ) &&
1427 !pathBlock.is( 'pre' ) && !pathBlock.getBogus()
1428 )
1429 return pathBlock;
1430 }
1431
1432 function blockInputClick( evt ) {
1433 var element = evt.data.getTarget();
1434 if ( element.is( 'input' ) ) {
1435 var type = element.getAttribute( 'type' );
1436 if ( type == 'submit' || type == 'reset' )
1437 evt.data.preventDefault();
1438 }
1439 }
1440
1441 function isNotEmpty( node ) {
1442 return isNotWhitespace( node ) && isNotBookmark( node );
1443 }
1444
1445 function isNbsp( node ) {
1446 return node.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( node.getText() ).match( /^(?:&nbsp;|\xa0)$/ );
1447 }
1448
1449 function isNotBubbling( fn, src ) {
1450 return function( evt ) {
1451 var other = evt.data.$.toElement || evt.data.$.fromElement || evt.data.$.relatedTarget;
1452
1453 // First of all, other may simply be null/undefined.
1454 // Second of all, at least early versions of Spartan returned empty objects from evt.relatedTarget,
1455 // so let's also check the node type.
1456 other = ( other && other.nodeType == CKEDITOR.NODE_ELEMENT ) ? new CKEDITOR.dom.element( other ) : null;
1457
1458 if ( !( other && ( src.equals( other ) || src.contains( other ) ) ) )
1459 fn.call( this, evt );
1460 };
1461 }
1462
1463 function hasBookmarks( element ) {
1464 // We use getElementsByTag() instead of find() to retain compatibility with IE quirks mode.
1465 var potentialBookmarks = element.getElementsByTag( 'span' ),
1466 i = 0,
1467 child;
1468
1469 if ( potentialBookmarks ) {
1470 while ( ( child = potentialBookmarks.getItem( i++ ) ) ) {
1471 if ( !isNotBookmark( child ) ) {
1472 return true;
1473 }
1474 }
1475 }
1476
1477 return false;
1478 }
1479
1480 // Check if the entire table/list contents is selected.
1481 function getSelectedTableList( sel ) {
1482 var selected,
1483 range = sel.getRanges()[ 0 ],
1484 editable = sel.root,
1485 path = range.startPath(),
1486 structural = { table: 1, ul: 1, ol: 1, dl: 1 };
1487
1488 if ( path.contains( structural ) ) {
1489 // Clone the original range.
1490 var walkerRng = range.clone();
1491
1492 // Enlarge the range: X<ul><li>[Y]</li></ul>X => [X<ul><li>]Y</li></ul>X
1493 walkerRng.collapse( 1 );
1494 walkerRng.setStartAt( editable, CKEDITOR.POSITION_AFTER_START );
1495
1496 // Create a new walker.
1497 var walker = new CKEDITOR.dom.walker( walkerRng );
1498
1499 // Assign a new guard to the walker.
1500 walker.guard = guard();
1501
1502 // Go backwards checking for selected structural node.
1503 walker.checkBackward();
1504
1505 // If there's a selected structured element when checking backwards,
1506 // then check the same forwards.
1507 if ( selected ) {
1508 // Clone the original range.
1509 walkerRng = range.clone();
1510
1511 // Enlarge the range (assuming <ul> is selected element from guard):
1512 //
1513 // X<ul><li>[Y]</li></ul>X => X<ul><li>Y[</li></ul>]X
1514 //
1515 // If the walker went deeper down DOM than a while ago when traversing
1516 // backwards, then it doesn't make sense: an element must be selected
1517 // symmetrically. By placing range end **after previously selected node**,
1518 // we make sure we don't go no deeper in DOM when going forwards.
1519 walkerRng.collapse();
1520 walkerRng.setEndAt( selected, CKEDITOR.POSITION_AFTER_END );
1521
1522 // Create a new walker.
1523 walker = new CKEDITOR.dom.walker( walkerRng );
1524
1525 // Assign a new guard to the walker.
1526 walker.guard = guard( true );
1527
1528 // Reset selected node.
1529 selected = false;
1530
1531 // Go forwards checking for selected structural node.
1532 walker.checkForward();
1533
1534 return selected;
1535 }
1536 }
1537
1538 return null;
1539
1540 function guard( forwardGuard ) {
1541 return function( node, isWalkOut ) {
1542 // Save the encountered node as selected if going down the DOM structure
1543 // and the node is structured element.
1544 if ( isWalkOut && node.type == CKEDITOR.NODE_ELEMENT && node.is( structural ) )
1545 selected = node;
1546
1547 // Stop the walker when either traversing another non-empty node at the same
1548 // DOM level as in previous step.
1549 // NOTE: When going forwards, stop if encountered a bogus.
1550 if ( !isWalkOut && isNotEmpty( node ) && !( forwardGuard && isBogus( node ) ) )
1551 return false;
1552 };
1553 }
1554 }
1555
1556 // Whether in given context (pathBlock, pathBlockLimit and editor settings)
1557 // editor should automatically wrap inline contents with blocks.
1558 function shouldAutoParagraph( editor, pathBlock, pathBlockLimit ) {
1559 // Check whether pathBlock equals pathBlockLimit to support nested editable (#12162).
1560 return editor.config.autoParagraph !== false &&
1561 editor.activeEnterMode != CKEDITOR.ENTER_BR &&
1562 (
1563 ( editor.editable().equals( pathBlockLimit ) && !pathBlock ) ||
1564 ( pathBlock && pathBlock.getAttribute( 'contenteditable' ) == 'true' )
1565 );
1566 }
1567
1568 function autoParagraphTag( editor ) {
1569 return ( editor.activeEnterMode != CKEDITOR.ENTER_BR && editor.config.autoParagraph !== false ) ? editor.activeEnterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' : false;
1570 }
1571
1572 //
1573 // Functions related to insertXXX methods
1574 //
1575 insert = ( function() {
1576 'use strict';
1577
1578 var DTD = CKEDITOR.dtd;
1579
1580 // Inserts the given (valid) HTML into the range position (with range content deleted),
1581 // guarantee it's result to be a valid DOM tree.
1582 function insert( editable, type, data, range ) {
1583 var editor = editable.editor,
1584 dontFilter = false;
1585
1586 if ( type == 'unfiltered_html' ) {
1587 type = 'html';
1588 dontFilter = true;
1589 }
1590
1591 // Check range spans in non-editable.
1592 if ( range.checkReadOnly() )
1593 return;
1594
1595 // RANGE PREPARATIONS
1596
1597 var path = new CKEDITOR.dom.elementPath( range.startContainer, range.root ),
1598 // Let root be the nearest block that's impossible to be split
1599 // during html processing.
1600 blockLimit = path.blockLimit || range.root,
1601 // The "state" value.
1602 that = {
1603 type: type,
1604 dontFilter: dontFilter,
1605 editable: editable,
1606 editor: editor,
1607 range: range,
1608 blockLimit: blockLimit,
1609 // During pre-processing / preparations startContainer of affectedRange should be placed
1610 // in this element in which inserted or moved (in case when we merge blocks) content
1611 // could create situation that will need merging inline elements.
1612 // Examples:
1613 // <div><b>A</b>^B</div> + <b>C</b> => <div><b>A</b><b>C</b>B</div> - affected container is <div>.
1614 // <p><b>A[B</b></p><p><b>C]D</b></p> + E => <p><b>AE</b></p><p><b>D</b></p> =>
1615 // <p><b>AE</b><b>D</b></p> - affected container is <p> (in text mode).
1616 mergeCandidates: [],
1617 zombies: []
1618 };
1619
1620 prepareRangeToDataInsertion( that );
1621
1622 // DATA PROCESSING
1623
1624 // Select range and stop execution.
1625 // If data has been totally emptied after the filtering,
1626 // any insertion is pointless (#10339).
1627 if ( data && processDataForInsertion( that, data ) ) {
1628 // DATA INSERTION
1629 insertDataIntoRange( that );
1630 }
1631
1632 // FINAL CLEANUP
1633 // Set final range position and clean up.
1634
1635 cleanupAfterInsertion( that );
1636 }
1637
1638 // Prepare range to its data deletion.
1639 // Delete its contents.
1640 // Prepare it to insertion.
1641 function prepareRangeToDataInsertion( that ) {
1642 var range = that.range,
1643 mergeCandidates = that.mergeCandidates,
1644 node, marker, path, startPath, endPath, previous, bm;
1645
1646 // If range starts in inline element then insert a marker, so empty
1647 // inline elements won't be removed while range.deleteContents
1648 // and we will be able to move range back into this element.
1649 // E.g. 'aa<b>[bb</b>]cc' -> (after deleting) 'aa<b><span/></b>cc'
1650 if ( that.type == 'text' && range.shrink( CKEDITOR.SHRINK_ELEMENT, true, false ) ) {
1651 marker = CKEDITOR.dom.element.createFromHtml( '<span>&nbsp;</span>', range.document );
1652 range.insertNode( marker );
1653 range.setStartAfter( marker );
1654 }
1655
1656 // By using path we can recover in which element was startContainer
1657 // before deleting contents.
1658 // Start and endPathElements will be used to squash selected blocks, after removing
1659 // selection contents. See rule 5.
1660 startPath = new CKEDITOR.dom.elementPath( range.startContainer );
1661 that.endPath = endPath = new CKEDITOR.dom.elementPath( range.endContainer );
1662
1663 if ( !range.collapsed ) {
1664 // Anticipate the possibly empty block at the end of range after deletion.
1665 node = endPath.block || endPath.blockLimit;
1666 var ancestor = range.getCommonAncestor();
1667 if ( node && !( node.equals( ancestor ) || node.contains( ancestor ) ) && range.checkEndOfBlock() ) {
1668 that.zombies.push( node );
1669 }
1670
1671 range.deleteContents();
1672 }
1673
1674 // Rule 4.
1675 // Move range into the previous block.
1676 while (
1677 ( previous = getRangePrevious( range ) ) && checkIfElement( previous ) && previous.isBlockBoundary() &&
1678 // Check if previousNode was parent of range's startContainer before deleteContents.
1679 startPath.contains( previous )
1680 )
1681 range.moveToPosition( previous, CKEDITOR.POSITION_BEFORE_END );
1682
1683 // Rule 5.
1684 mergeAncestorElementsOfSelectionEnds( range, that.blockLimit, startPath, endPath );
1685
1686 // Rule 1.
1687 if ( marker ) {
1688 // If marker was created then move collapsed range into its place.
1689 range.setEndBefore( marker );
1690 range.collapse();
1691 marker.remove();
1692 }
1693
1694 // Split inline elements so HTML will be inserted with its own styles.
1695 path = range.startPath();
1696 if ( ( node = path.contains( isInline, false, 1 ) ) ) {
1697 range.splitElement( node );
1698 that.inlineStylesRoot = node;
1699 that.inlineStylesPeak = path.lastElement;
1700 }
1701
1702 // Record inline merging candidates for later cleanup in place.
1703 bm = range.createBookmark();
1704
1705 // 1. Inline siblings.
1706 node = bm.startNode.getPrevious( isNotEmpty );
1707 node && checkIfElement( node ) && isInline( node ) && mergeCandidates.push( node );
1708 node = bm.startNode.getNext( isNotEmpty );
1709 node && checkIfElement( node ) && isInline( node ) && mergeCandidates.push( node );
1710
1711 // 2. Inline parents.
1712 node = bm.startNode;
1713 while ( ( node = node.getParent() ) && isInline( node ) )
1714 mergeCandidates.push( node );
1715
1716 range.moveToBookmark( bm );
1717 }
1718
1719 function processDataForInsertion( that, data ) {
1720 var range = that.range;
1721
1722 // Rule 8. - wrap entire data in inline styles.
1723 // (e.g. <p><b>x^z</b></p> + <p>a</p><p>b</p> -> <b><p>a</p><p>b</p></b>)
1724 // Incorrect tags order will be fixed by htmlDataProcessor.
1725 if ( that.type == 'text' && that.inlineStylesRoot )
1726 data = wrapDataWithInlineStyles( data, that );
1727
1728
1729 var context = that.blockLimit.getName();
1730
1731 // Wrap data to be inserted, to avoid losing leading whitespaces
1732 // when going through the below procedure.
1733 if ( /^\s+|\s+$/.test( data ) && 'span' in CKEDITOR.dtd[ context ] ) {
1734 var protect = '<span data-cke-marker="1">&nbsp;</span>';
1735 data = protect + data + protect;
1736 }
1737
1738 // Process the inserted html, in context of the insertion root.
1739 // Don't use the "fix for body" feature as auto paragraphing must
1740 // be handled during insertion.
1741 data = that.editor.dataProcessor.toHtml( data, {
1742 context: null,
1743 fixForBody: false,
1744 protectedWhitespaces: !!protect,
1745 dontFilter: that.dontFilter,
1746 // Use the current, contextual settings.
1747 filter: that.editor.activeFilter,
1748 enterMode: that.editor.activeEnterMode
1749 } );
1750
1751
1752 // Build the node list for insertion.
1753 var doc = range.document,
1754 wrapper = doc.createElement( 'body' );
1755
1756 wrapper.setHtml( data );
1757
1758 // Eventually remove the temporaries.
1759 if ( protect ) {
1760 wrapper.getFirst().remove();
1761 wrapper.getLast().remove();
1762 }
1763
1764 // Rule 7.
1765 var block = range.startPath().block;
1766 if ( block && // Apply when there exists path block after deleting selection's content...
1767 !( block.getChildCount() == 1 && block.getBogus() ) ) { // ... and the only content of this block isn't a bogus.
1768 stripBlockTagIfSingleLine( wrapper );
1769 }
1770
1771 that.dataWrapper = wrapper;
1772
1773 return data;
1774 }
1775
1776 function insertDataIntoRange( that ) {
1777 var range = that.range,
1778 doc = range.document,
1779 path,
1780 blockLimit = that.blockLimit,
1781 nodesData, nodeData, node,
1782 nodeIndex = 0,
1783 bogus,
1784 bogusNeededBlocks = [],
1785 pathBlock, fixBlock,
1786 splittingContainer = 0,
1787 dontMoveCaret = 0,
1788 insertionContainer, toSplit, newContainer,
1789 startContainer = range.startContainer,
1790 endContainer = that.endPath.elements[ 0 ],
1791 filteredNodes,
1792 // If endContainer was merged into startContainer: <p>a[b</p><p>c]d</p>
1793 // or it's equal to startContainer: <p>a^b</p>
1794 // or different situation happened :P
1795 // then there's no separate container for the end of selection.
1796 pos = endContainer.getPosition( startContainer ),
1797 separateEndContainer = !!endContainer.getCommonAncestor( startContainer ) && // endC is not detached.
1798 pos != CKEDITOR.POSITION_IDENTICAL && !( pos & CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_IS_CONTAINED ); // endC & endS are in separate branches.
1799
1800 nodesData = extractNodesData( that.dataWrapper, that );
1801
1802 removeBrsAdjacentToPastedBlocks( nodesData, range );
1803
1804 for ( ; nodeIndex < nodesData.length; nodeIndex++ ) {
1805 nodeData = nodesData[ nodeIndex ];
1806
1807 // Ignore trailing <brs>
1808 if ( nodeData.isLineBreak && splitOnLineBreak( range, blockLimit, nodeData ) ) {
1809 // Do not move caret towards the text (in cleanupAfterInsertion),
1810 // because caret was placed after a line break.
1811 dontMoveCaret = nodeIndex > 0;
1812 continue;
1813 }
1814
1815 path = range.startPath();
1816
1817 // Auto paragraphing.
1818 if ( !nodeData.isBlock && shouldAutoParagraph( that.editor, path.block, path.blockLimit ) && ( fixBlock = autoParagraphTag( that.editor ) ) ) {
1819 fixBlock = doc.createElement( fixBlock );
1820 fixBlock.appendBogus();
1821 range.insertNode( fixBlock );
1822 if ( CKEDITOR.env.needsBrFiller && ( bogus = fixBlock.getBogus() ) )
1823 bogus.remove();
1824 range.moveToPosition( fixBlock, CKEDITOR.POSITION_BEFORE_END );
1825 }
1826
1827 node = range.startPath().block;
1828
1829 // Remove any bogus element on the current path block for now, and mark
1830 // it for later compensation.
1831 if ( node && !node.equals( pathBlock ) ) {
1832 bogus = node.getBogus();
1833 if ( bogus ) {
1834 bogus.remove();
1835 bogusNeededBlocks.push( node );
1836 }
1837
1838 pathBlock = node;
1839 }
1840
1841 // First not allowed node reached - start splitting original container
1842 if ( nodeData.firstNotAllowed )
1843 splittingContainer = 1;
1844
1845 if ( splittingContainer && nodeData.isElement ) {
1846 insertionContainer = range.startContainer;
1847 toSplit = null;
1848
1849 // Find the first ancestor that can contain current node.
1850 // This one won't be split.
1851 while ( insertionContainer && !DTD[ insertionContainer.getName() ][ nodeData.name ] ) {
1852 if ( insertionContainer.equals( blockLimit ) ) {
1853 insertionContainer = null;
1854 break;
1855 }
1856
1857 toSplit = insertionContainer;
1858 insertionContainer = insertionContainer.getParent();
1859 }
1860
1861 // If split has to be done - do it and mark both ends as a possible zombies.
1862 if ( insertionContainer ) {
1863 if ( toSplit ) {
1864 newContainer = range.splitElement( toSplit );
1865 that.zombies.push( newContainer );
1866 that.zombies.push( toSplit );
1867 }
1868 }
1869 // Unable to make the insertion happen in place, resort to the content filter.
1870 else {
1871 // If everything worked fine insertionContainer == blockLimit here.
1872 filteredNodes = filterElement( nodeData.node, blockLimit.getName(), !nodeIndex, nodeIndex == nodesData.length - 1 );
1873 }
1874 }
1875
1876 if ( filteredNodes ) {
1877 while ( ( node = filteredNodes.pop() ) )
1878 range.insertNode( node );
1879 filteredNodes = 0;
1880 } else {
1881 // Insert current node at the start of range.
1882 range.insertNode( nodeData.node );
1883 }
1884
1885 // Move range to the endContainer for the final allowed elements.
1886 if ( nodeData.lastNotAllowed && nodeIndex < nodesData.length - 1 ) {
1887 // If separateEndContainer exists move range there.
1888 // Otherwise try to move range to container created during splitting.
1889 // If this doesn't work - don't move range.
1890 newContainer = separateEndContainer ? endContainer : newContainer;
1891 newContainer && range.setEndAt( newContainer, CKEDITOR.POSITION_AFTER_START );
1892 splittingContainer = 0;
1893 }
1894
1895 // Collapse range after insertion to end.
1896 range.collapse();
1897 }
1898
1899 // Rule 9. Non-editable content should be selected as a whole.
1900 if ( isSingleNonEditableElement( nodesData ) ) {
1901 dontMoveCaret = true;
1902 node = nodesData[ 0 ].node;
1903 range.setStartAt( node, CKEDITOR.POSITION_BEFORE_START );
1904 range.setEndAt( node, CKEDITOR.POSITION_AFTER_END );
1905 }
1906
1907 that.dontMoveCaret = dontMoveCaret;
1908 that.bogusNeededBlocks = bogusNeededBlocks;
1909 }
1910
1911 function cleanupAfterInsertion( that ) {
1912 var range = that.range,
1913 node, testRange, movedIntoInline,
1914 bogusNeededBlocks = that.bogusNeededBlocks,
1915 // Create a bookmark to defend against the following range deconstructing operations.
1916 bm = range.createBookmark();
1917
1918 // Remove all elements that could be created while splitting nodes
1919 // with ranges at its start|end.
1920 // E.g. remove <div><p></p></div>
1921 // But not <div><p> </p></div>
1922 // And replace <div><p><span data="cke-bookmark"/></p></div> with found bookmark.
1923 while ( ( node = that.zombies.pop() ) ) {
1924 // Detached element.
1925 if ( !node.getParent() )
1926 continue;
1927
1928 testRange = range.clone();
1929 testRange.moveToElementEditStart( node );
1930 testRange.removeEmptyBlocksAtEnd();
1931 }
1932
1933 if ( bogusNeededBlocks ) {
1934 // Bring back all block bogus nodes.
1935 while ( ( node = bogusNeededBlocks.pop() ) ) {
1936 if ( CKEDITOR.env.needsBrFiller )
1937 node.appendBogus();
1938 else
1939 node.append( range.document.createText( '\u00a0' ) );
1940 }
1941 }
1942
1943 // Eventually merge identical inline elements.
1944 while ( ( node = that.mergeCandidates.pop() ) )
1945 node.mergeSiblings();
1946
1947 range.moveToBookmark( bm );
1948
1949 // Rule 3.
1950 // Shrink range to the BEFOREEND of previous innermost editable node in source order.
1951
1952 if ( !that.dontMoveCaret ) {
1953 node = getRangePrevious( range );
1954
1955 while ( node && checkIfElement( node ) && !node.is( DTD.$empty ) ) {
1956 if ( node.isBlockBoundary() )
1957 range.moveToPosition( node, CKEDITOR.POSITION_BEFORE_END );
1958 else {
1959 // Don't move into inline element (which ends with a text node)
1960 // found which contains white-space at its end.
1961 // If not - move range's end to the end of this element.
1962 if ( isInline( node ) && node.getHtml().match( /(\s|&nbsp;)$/g ) ) {
1963 movedIntoInline = null;
1964 break;
1965 }
1966
1967 movedIntoInline = range.clone();
1968 movedIntoInline.moveToPosition( node, CKEDITOR.POSITION_BEFORE_END );
1969 }
1970
1971 node = node.getLast( isNotEmpty );
1972 }
1973
1974 movedIntoInline && range.moveToRange( movedIntoInline );
1975 }
1976
1977 }
1978
1979 //
1980 // HELPERS ------------------------------------------------------------
1981 //
1982
1983 function checkIfElement( node ) {
1984 return node.type == CKEDITOR.NODE_ELEMENT;
1985 }
1986
1987 function extractNodesData( dataWrapper, that ) {
1988 var node, sibling, nodeName, allowed,
1989 nodesData = [],
1990 startContainer = that.range.startContainer,
1991 path = that.range.startPath(),
1992 allowedNames = DTD[ startContainer.getName() ],
1993 nodeIndex = 0,
1994 nodesList = dataWrapper.getChildren(),
1995 nodesCount = nodesList.count(),
1996 firstNotAllowed = -1,
1997 lastNotAllowed = -1,
1998 lineBreak = 0,
1999 blockSibling;
2000
2001 // Selection start within a list.
2002 var insideOfList = path.contains( DTD.$list );
2003
2004 for ( ; nodeIndex < nodesCount; ++nodeIndex ) {
2005 node = nodesList.getItem( nodeIndex );
2006
2007 if ( checkIfElement( node ) ) {
2008 nodeName = node.getName();
2009
2010 // Extract only the list items, when insertion happens
2011 // inside of a list, reads as rearrange list items. (#7957)
2012 if ( insideOfList && nodeName in CKEDITOR.dtd.$list ) {
2013 nodesData = nodesData.concat( extractNodesData( node, that ) );
2014 continue;
2015 }
2016
2017 allowed = !!allowedNames[ nodeName ];
2018
2019 // Mark <brs data-cke-eol="1"> at the beginning and at the end.
2020 if ( nodeName == 'br' && node.data( 'cke-eol' ) && ( !nodeIndex || nodeIndex == nodesCount - 1 ) ) {
2021 sibling = nodeIndex ? nodesData[ nodeIndex - 1 ].node : nodesList.getItem( nodeIndex + 1 );
2022
2023 // Line break has to have sibling which is not an <br>.
2024 lineBreak = sibling && ( !checkIfElement( sibling ) || !sibling.is( 'br' ) );
2025 // Line break has block element as a sibling.
2026 blockSibling = sibling && checkIfElement( sibling ) && DTD.$block[ sibling.getName() ];
2027 }
2028
2029 if ( firstNotAllowed == -1 && !allowed )
2030 firstNotAllowed = nodeIndex;
2031 if ( !allowed )
2032 lastNotAllowed = nodeIndex;
2033
2034 nodesData.push( {
2035 isElement: 1,
2036 isLineBreak: lineBreak,
2037 isBlock: node.isBlockBoundary(),
2038 hasBlockSibling: blockSibling,
2039 node: node,
2040 name: nodeName,
2041 allowed: allowed
2042 } );
2043
2044 lineBreak = 0;
2045 blockSibling = 0;
2046 } else {
2047 nodesData.push( { isElement: 0, node: node, allowed: 1 } );
2048 }
2049 }
2050
2051 // Mark first node that cannot be inserted directly into startContainer
2052 // and last node for which startContainer has to be split.
2053 if ( firstNotAllowed > -1 )
2054 nodesData[ firstNotAllowed ].firstNotAllowed = 1;
2055 if ( lastNotAllowed > -1 )
2056 nodesData[ lastNotAllowed ].lastNotAllowed = 1;
2057
2058 return nodesData;
2059 }
2060
2061 // TODO: Review content transformation rules on filtering element.
2062 function filterElement( element, parentName, isFirst, isLast ) {
2063 var nodes = filterElementInner( element, parentName ),
2064 nodes2 = [],
2065 nodesCount = nodes.length,
2066 nodeIndex = 0,
2067 node,
2068 afterSpace = 0,
2069 lastSpaceIndex = -1;
2070
2071 // Remove duplicated spaces and spaces at the:
2072 // * beginnig if filtered element isFirst (isFirst that's going to be inserted)
2073 // * end if filtered element isLast.
2074 for ( ; nodeIndex < nodesCount; nodeIndex++ ) {
2075 node = nodes[ nodeIndex ];
2076
2077 if ( node == ' ' ) {
2078 // Don't push doubled space and if it's leading space for insertion.
2079 if ( !afterSpace && !( isFirst && !nodeIndex ) ) {
2080 nodes2.push( new CKEDITOR.dom.text( ' ' ) );
2081 lastSpaceIndex = nodes2.length;
2082 }
2083 afterSpace = 1;
2084 } else {
2085 nodes2.push( node );
2086 afterSpace = 0;
2087 }
2088 }
2089
2090 // Remove trailing space.
2091 if ( isLast && lastSpaceIndex == nodes2.length )
2092 nodes2.pop();
2093
2094 return nodes2;
2095 }
2096
2097 function filterElementInner( element, parentName ) {
2098 var nodes = [],
2099 children = element.getChildren(),
2100 childrenCount = children.count(),
2101 child,
2102 childIndex = 0,
2103 allowedNames = DTD[ parentName ],
2104 surroundBySpaces = !element.is( DTD.$inline ) || element.is( 'br' );
2105
2106 if ( surroundBySpaces )
2107 nodes.push( ' ' );
2108
2109 for ( ; childIndex < childrenCount; childIndex++ ) {
2110 child = children.getItem( childIndex );
2111
2112 if ( checkIfElement( child ) && !child.is( allowedNames ) )
2113 nodes = nodes.concat( filterElementInner( child, parentName ) );
2114 else
2115 nodes.push( child );
2116 }
2117
2118 if ( surroundBySpaces )
2119 nodes.push( ' ' );
2120
2121 return nodes;
2122 }
2123
2124 function getRangePrevious( range ) {
2125 return checkIfElement( range.startContainer ) && range.startContainer.getChild( range.startOffset - 1 );
2126 }
2127
2128 function isInline( node ) {
2129 return node && checkIfElement( node ) && ( node.is( DTD.$removeEmpty ) || node.is( 'a' ) && !node.isBlockBoundary() );
2130 }
2131
2132 // Checks if only non-editable element is being inserted.
2133 function isSingleNonEditableElement( nodesData ) {
2134 if ( nodesData.length != 1 )
2135 return false;
2136
2137 var nodeData = nodesData[ 0 ];
2138
2139 return nodeData.isElement && ( nodeData.node.getAttribute( 'contenteditable' ) == 'false' );
2140 }
2141
2142 var blockMergedTags = { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, li: 1, pre: 1, dl: 1, blockquote: 1 };
2143
2144 // See rule 5. in TCs.
2145 // Initial situation:
2146 // <ul><li>AA^</li></ul><ul><li>BB</li></ul>
2147 // We're looking for 2nd <ul>, comparing with 1st <ul> and merging.
2148 // We're not merging if caret is between these elements.
2149 function mergeAncestorElementsOfSelectionEnds( range, blockLimit, startPath, endPath ) {
2150 var walkerRange = range.clone(),
2151 walker, nextNode, previousNode;
2152
2153 walkerRange.setEndAt( blockLimit, CKEDITOR.POSITION_BEFORE_END );
2154 walker = new CKEDITOR.dom.walker( walkerRange );
2155
2156 if ( ( nextNode = walker.next() ) && // Find next source node
2157 checkIfElement( nextNode ) && // which is an element
2158 blockMergedTags[ nextNode.getName() ] && // that can be merged.
2159 ( previousNode = nextNode.getPrevious() ) && // Take previous one
2160 checkIfElement( previousNode ) && // which also has to be an element.
2161 !previousNode.getParent().equals( range.startContainer ) && // Fail if caret is on the same level.
2162 // This means that caret is between these nodes.
2163 startPath.contains( previousNode ) && // Elements path of start of selection has
2164 endPath.contains( nextNode ) && // to contain prevNode and vice versa.
2165 nextNode.isIdentical( previousNode ) // Check if elements are identical.
2166 ) {
2167 // Merge blocks and repeat.
2168 nextNode.moveChildren( previousNode );
2169 nextNode.remove();
2170 mergeAncestorElementsOfSelectionEnds( range, blockLimit, startPath, endPath );
2171 }
2172 }
2173
2174 // If last node that will be inserted is a block (but not a <br>)
2175 // and it will be inserted right before <br> remove this <br>.
2176 // Do the same for the first element that will be inserted and preceding <br>.
2177 function removeBrsAdjacentToPastedBlocks( nodesData, range ) {
2178 var succeedingNode = range.endContainer.getChild( range.endOffset ),
2179 precedingNode = range.endContainer.getChild( range.endOffset - 1 );
2180
2181 if ( succeedingNode )
2182 remove( succeedingNode, nodesData[ nodesData.length - 1 ] );
2183
2184 if ( precedingNode && remove( precedingNode, nodesData[ 0 ] ) ) {
2185 // If preceding <br> was removed - move range left.
2186 range.setEnd( range.endContainer, range.endOffset - 1 );
2187 range.collapse();
2188 }
2189
2190 function remove( maybeBr, maybeBlockData ) {
2191 if ( maybeBlockData.isBlock && maybeBlockData.isElement && !maybeBlockData.node.is( 'br' ) &&
2192 checkIfElement( maybeBr ) && maybeBr.is( 'br' ) ) {
2193 maybeBr.remove();
2194 return 1;
2195 }
2196 }
2197 }
2198
2199 // Return 1 if <br> should be skipped when inserting, 0 otherwise.
2200 function splitOnLineBreak( range, blockLimit, nodeData ) {
2201 var firstBlockAscendant, pos;
2202
2203 if ( nodeData.hasBlockSibling )
2204 return 1;
2205
2206 firstBlockAscendant = range.startContainer.getAscendant( DTD.$block, 1 );
2207 if ( !firstBlockAscendant || !firstBlockAscendant.is( { div: 1, p: 1 } ) )
2208 return 0;
2209
2210 pos = firstBlockAscendant.getPosition( blockLimit );
2211
2212 if ( pos == CKEDITOR.POSITION_IDENTICAL || pos == CKEDITOR.POSITION_CONTAINS )
2213 return 0;
2214
2215 var newContainer = range.splitElement( firstBlockAscendant );
2216 range.moveToPosition( newContainer, CKEDITOR.POSITION_AFTER_START );
2217
2218 return 1;
2219 }
2220
2221 var stripSingleBlockTags = { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1 },
2222 inlineButNotBr = CKEDITOR.tools.extend( {}, DTD.$inline );
2223 delete inlineButNotBr.br;
2224
2225 // Rule 7.
2226 function stripBlockTagIfSingleLine( dataWrapper ) {
2227 var block, children;
2228
2229 if ( dataWrapper.getChildCount() == 1 && // Only one node bein inserted.
2230 checkIfElement( block = dataWrapper.getFirst() ) && // And it's an element.
2231 block.is( stripSingleBlockTags ) && // That's <p> or <div> or header.
2232 !block.hasAttribute( 'contenteditable' ) // It's not a non-editable block or nested editable.
2233 ) {
2234 // Check children not containing block.
2235 children = block.getElementsByTag( '*' );
2236 for ( var i = 0, child, count = children.count(); i < count; i++ ) {
2237 child = children.getItem( i );
2238 if ( !child.is( inlineButNotBr ) )
2239 return;
2240 }
2241
2242 block.moveChildren( block.getParent( 1 ) );
2243 block.remove();
2244 }
2245 }
2246
2247 function wrapDataWithInlineStyles( data, that ) {
2248 var element = that.inlineStylesPeak,
2249 doc = element.getDocument(),
2250 wrapper = doc.createText( '{cke-peak}' ),
2251 limit = that.inlineStylesRoot.getParent();
2252
2253 while ( !element.equals( limit ) ) {
2254 wrapper = wrapper.appendTo( element.clone() );
2255 element = element.getParent();
2256 }
2257
2258 // Don't use String.replace because it fails in IE7 if special replacement
2259 // characters ($$, $&, etc.) are in data (#10367).
2260 return wrapper.getOuterHtml().split( '{cke-peak}' ).join( data );
2261 }
2262
2263 return insert;
2264 } )();
2265
2266 function afterInsert( editable ) {
2267 var editor = editable.editor;
2268
2269 // Scroll using selection, not ranges, to affect native pastes.
2270 editor.getSelection().scrollIntoView();
2271
2272 // Save snaps after the whole execution completed.
2273 // This's a workaround for make DOM modification's happened after
2274 // 'insertElement' to be included either, e.g. Form-based dialogs' 'commitContents'
2275 // call.
2276 setTimeout( function() {
2277 editor.fire( 'saveSnapshot' );
2278 }, 0 );
2279 }
2280
2281 // 1. Fixes a range which is a result of deleteContents() and is placed in an intermediate element (see dtd.$intermediate),
2282 // inside a table. A goal is to find a closest <td> or <th> element and when this fails, recreate the structure of the table.
2283 // 2. Fixes empty cells by appending bogus <br>s or deleting empty text nodes in IE<=8 case.
2284 fixTableAfterContentsDeletion = ( function() {
2285 // Creates an element walker which can only "go deeper". It won't
2286 // move out from any element. Therefore it can be used to find <td>x</td> in cases like:
2287 // <table><tbody><tr><td>x</td></tr></tbody>^<tfoot>...
2288 function getFixTableSelectionWalker( testRange ) {
2289 var walker = new CKEDITOR.dom.walker( testRange );
2290 walker.guard = function( node, isMovingOut ) {
2291 if ( isMovingOut )
2292 return false;
2293 if ( node.type == CKEDITOR.NODE_ELEMENT )
2294 return node.is( CKEDITOR.dtd.$tableContent );
2295 };
2296 walker.evaluator = function( node ) {
2297 return node.type == CKEDITOR.NODE_ELEMENT;
2298 };
2299
2300 return walker;
2301 }
2302
2303 function fixTableStructure( element, newElementName, appendToStart ) {
2304 var temp = element.getDocument().createElement( newElementName );
2305 element.append( temp, appendToStart );
2306 return temp;
2307 }
2308
2309 // Fix empty cells. This means:
2310 // * add bogus <br> if browser needs it
2311 // * remove empty text nodes on IE8, because it will crash (http://dev.ckeditor.com/ticket/11183#comment:8).
2312 function fixEmptyCells( cells ) {
2313 var i = cells.count(),
2314 cell;
2315
2316 for ( i; i-- > 0; ) {
2317 cell = cells.getItem( i );
2318
2319 if ( !CKEDITOR.tools.trim( cell.getHtml() ) ) {
2320 cell.appendBogus();
2321 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && cell.getChildCount() )
2322 cell.getFirst().remove();
2323 }
2324 }
2325 }
2326
2327 return function( range ) {
2328 var container = range.startContainer,
2329 table = container.getAscendant( 'table', 1 ),
2330 testRange,
2331 deeperSibling,
2332 appendToStart = false;
2333
2334 fixEmptyCells( table.getElementsByTag( 'td' ) );
2335 fixEmptyCells( table.getElementsByTag( 'th' ) );
2336
2337 // Look left.
2338 testRange = range.clone();
2339 testRange.setStart( container, 0 );
2340 deeperSibling = getFixTableSelectionWalker( testRange ).lastBackward();
2341
2342 // If left is empty, look right.
2343 if ( !deeperSibling ) {
2344 testRange = range.clone();
2345 testRange.setEndAt( container, CKEDITOR.POSITION_BEFORE_END );
2346 deeperSibling = getFixTableSelectionWalker( testRange ).lastForward();
2347 appendToStart = true;
2348 }
2349
2350 // If there's no deeper nested element in both direction - container is empty - we'll use it then.
2351 if ( !deeperSibling )
2352 deeperSibling = container;
2353
2354 // Fix structure...
2355
2356 // We found a table what means that it's empty - remove it completely.
2357 if ( deeperSibling.is( 'table' ) ) {
2358 range.setStartAt( deeperSibling, CKEDITOR.POSITION_BEFORE_START );
2359 range.collapse( true );
2360 deeperSibling.remove();
2361 return;
2362 }
2363
2364 // Found an empty txxx element - append tr.
2365 if ( deeperSibling.is( { tbody: 1, thead: 1, tfoot: 1 } ) )
2366 deeperSibling = fixTableStructure( deeperSibling, 'tr', appendToStart );
2367
2368 // Found an empty tr element - append td/th.
2369 if ( deeperSibling.is( 'tr' ) )
2370 deeperSibling = fixTableStructure( deeperSibling, deeperSibling.getParent().is( 'thead' ) ? 'th' : 'td', appendToStart );
2371
2372 // To avoid setting selection after bogus, remove it from the current cell.
2373 // We can safely do that, because we'll insert element into that cell.
2374 var bogus = deeperSibling.getBogus();
2375 if ( bogus )
2376 bogus.remove();
2377
2378 range.moveToPosition( deeperSibling, appendToStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
2379 };
2380 } )();
2381
2382 fixListAfterContentsDelete = ( function() {
2383 // Creates an element walker which operates only within lists.
2384 function getFixListSelectionWalker( testRange ) {
2385 var walker = new CKEDITOR.dom.walker( testRange );
2386 walker.guard = function( node, isMovingOut ) {
2387 if ( isMovingOut )
2388 return false;
2389 if ( node.type == CKEDITOR.NODE_ELEMENT )
2390 return node.is( CKEDITOR.dtd.$list ) || node.is( CKEDITOR.dtd.$listItem );
2391 };
2392 walker.evaluator = function( node ) {
2393 return node.type == CKEDITOR.NODE_ELEMENT && node.is( CKEDITOR.dtd.$listItem );
2394 };
2395
2396 return walker;
2397 }
2398
2399 return function( range ) {
2400 var container = range.startContainer,
2401 appendToStart = false,
2402 testRange,
2403 deeperSibling;
2404
2405 // Look left.
2406 testRange = range.clone();
2407 testRange.setStart( container, 0 );
2408 deeperSibling = getFixListSelectionWalker( testRange ).lastBackward();
2409
2410 // If left is empty, look right.
2411 if ( !deeperSibling ) {
2412 testRange = range.clone();
2413 testRange.setEndAt( container, CKEDITOR.POSITION_BEFORE_END );
2414 deeperSibling = getFixListSelectionWalker( testRange ).lastForward();
2415 appendToStart = true;
2416 }
2417
2418 // If there's no deeper nested element in both direction - container is empty - we'll use it then.
2419 if ( !deeperSibling )
2420 deeperSibling = container;
2421
2422 // We found a list what means that it's empty - remove it completely.
2423 if ( deeperSibling.is( CKEDITOR.dtd.$list ) ) {
2424 range.setStartAt( deeperSibling, CKEDITOR.POSITION_BEFORE_START );
2425 range.collapse( true );
2426 deeperSibling.remove();
2427 return;
2428 }
2429
2430 // To avoid setting selection after bogus, remove it from the target list item.
2431 // We can safely do that, because we'll insert element into that cell.
2432 var bogus = deeperSibling.getBogus();
2433 if ( bogus )
2434 bogus.remove();
2435
2436 range.moveToPosition( deeperSibling, appendToStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
2437 range.select();
2438 };
2439 } )();
2440
2441 function mergeBlocksCollapsedSelection( editor, range, backspace, startPath ) {
2442 var startBlock = startPath.block;
2443
2444 // Selection must be collapsed and to be anchored in a block.
2445 if ( !startBlock )
2446 return false;
2447
2448 // Exclude cases where, i.e. if pressed arrow key, selection
2449 // would move within the same block (merge inside a block).
2450 if ( !range[ backspace ? 'checkStartOfBlock' : 'checkEndOfBlock' ]() )
2451 return false;
2452
2453 // Make sure, there's an editable position to put selection,
2454 // which i.e. would be used if pressed arrow key, but abort
2455 // if such position exists but means a selected non-editable element.
2456 if ( !range.moveToClosestEditablePosition( startBlock, !backspace ) || !range.collapsed )
2457 return false;
2458
2459 // Handle special case, when block's sibling is a <hr>. Delete it and keep selection
2460 // in the same place (http://dev.ckeditor.com/ticket/11861#comment:9).
2461 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT ) {
2462 var touched = range.startContainer.getChild( range.startOffset - ( backspace ? 1 : 0 ) );
2463 if ( touched && touched.type == CKEDITOR.NODE_ELEMENT && touched.is( 'hr' ) ) {
2464 editor.fire( 'saveSnapshot' );
2465 touched.remove();
2466 return true;
2467 }
2468 }
2469
2470 var siblingBlock = range.startPath().block;
2471
2472 // Abort if an editable position exists, but either it's not
2473 // in a block or that block is the parent of the start block
2474 // (merging child into parent).
2475 if ( !siblingBlock || ( siblingBlock && siblingBlock.contains( startBlock ) ) )
2476 return;
2477
2478 editor.fire( 'saveSnapshot' );
2479
2480 // Remove bogus to avoid duplicated boguses.
2481 var bogus;
2482 if ( ( bogus = ( backspace ? siblingBlock : startBlock ).getBogus() ) )
2483 bogus.remove();
2484
2485 // Save selection. It will be restored.
2486 var selection = editor.getSelection(),
2487 bookmarks = selection.createBookmarks();
2488
2489 // Merge blocks.
2490 ( backspace ? startBlock : siblingBlock ).moveChildren( backspace ? siblingBlock : startBlock, false );
2491
2492 // Also merge children along with parents.
2493 startPath.lastElement.mergeSiblings();
2494
2495 // Cut off removable branch of the DOM tree.
2496 pruneEmptyDisjointAncestors( startBlock, siblingBlock, !backspace );
2497
2498 // Restore selection.
2499 selection.selectBookmarks( bookmarks );
2500
2501 return true;
2502 }
2503
2504 function mergeBlocksNonCollapsedSelection( editor, range, startPath ) {
2505 var startBlock = startPath.block,
2506 endPath = range.endPath(),
2507 endBlock = endPath.block;
2508
2509 // Selection must be anchored in two different blocks.
2510 if ( !startBlock || !endBlock || startBlock.equals( endBlock ) )
2511 return false;
2512
2513 editor.fire( 'saveSnapshot' );
2514
2515 // Remove bogus to avoid duplicated boguses.
2516 var bogus;
2517 if ( ( bogus = startBlock.getBogus() ) )
2518 bogus.remove();
2519
2520 // Changing end container to element from text node (#12503).
2521 range.enlarge( CKEDITOR.ENLARGE_INLINE );
2522
2523 // Delete range contents. Do NOT merge. Merging is weird.
2524 range.deleteContents();
2525
2526 // If something has left of the block to be merged, clean it up.
2527 // It may happen when merging with list items.
2528 if ( endBlock.getParent() ) {
2529 // Move children to the first block.
2530 endBlock.moveChildren( startBlock, false );
2531
2532 // ...and merge them if that's possible.
2533 startPath.lastElement.mergeSiblings();
2534
2535 // If expanded selection, things are always merged like with BACKSPACE.
2536 pruneEmptyDisjointAncestors( startBlock, endBlock, true );
2537 }
2538
2539 // Make sure the result selection is collapsed.
2540 range = editor.getSelection().getRanges()[ 0 ];
2541 range.collapse( 1 );
2542
2543 // Optimizing range containers from text nodes to elements (#12503).
2544 range.optimize();
2545 if ( range.startContainer.getHtml() === '' ) {
2546 range.startContainer.appendBogus();
2547 }
2548
2549 range.select();
2550
2551 return true;
2552 }
2553
2554 // Finds the innermost child of common parent, which,
2555 // if removed, removes nothing but the contents of the element.
2556 //
2557 // before: <div><p><strong>first</strong></p><p>second</p></div>
2558 // after: <div><p>second</p></div>
2559 //
2560 // before: <div><p>x<strong>first</strong></p><p>second</p></div>
2561 // after: <div><p>x</p><p>second</p></div>
2562 //
2563 // isPruneToEnd=true
2564 // before: <div><p><strong>first</strong></p><p>second</p></div>
2565 // after: <div><p><strong>first</strong></p></div>
2566 //
2567 // @param {CKEDITOR.dom.element} first
2568 // @param {CKEDITOR.dom.element} second
2569 // @param {Boolean} isPruneToEnd
2570 function pruneEmptyDisjointAncestors( first, second, isPruneToEnd ) {
2571 var commonParent = first.getCommonAncestor( second ),
2572 node = isPruneToEnd ? second : first,
2573 removableParent = node;
2574
2575 while ( ( node = node.getParent() ) && !commonParent.equals( node ) && node.getChildCount() == 1 )
2576 removableParent = node;
2577
2578 removableParent.remove();
2579 }
2580
2581 //
2582 // Helpers for editable.getHtmlFromRange.
2583 //
2584 getHtmlFromRangeHelpers = {
2585 eol: {
2586 detect: function( that, editable ) {
2587 var range = that.range,
2588 rangeStart = range.clone(),
2589 rangeEnd = range.clone(),
2590
2591 startPath = new CKEDITOR.dom.elementPath( range.startContainer, editable ),
2592 endPath = new CKEDITOR.dom.elementPath( range.endContainer, editable );
2593
2594 // Note: checkBoundaryOfElement will not work on original range as CKEDITOR.START|END
2595 // means that range start|end must be literally anchored at block start|end, e.g.
2596 //
2597 // <p>a{</p><p>}b</p>
2598 //
2599 // will return false for both paragraphs but two similar ranges
2600 //
2601 // <p>a{}</p><p>{}b</p>
2602 //
2603 // will return true if checked separately.
2604 rangeStart.collapse( 1 );
2605 rangeEnd.collapse();
2606
2607 if ( startPath.block && rangeStart.checkBoundaryOfElement( startPath.block, CKEDITOR.END ) ) {
2608 range.setStartAfter( startPath.block );
2609 that.prependEolBr = 1;
2610 }
2611
2612 if ( endPath.block && rangeEnd.checkBoundaryOfElement( endPath.block, CKEDITOR.START ) ) {
2613 range.setEndBefore( endPath.block );
2614 that.appendEolBr = 1;
2615 }
2616 },
2617
2618 fix: function( that, editable ) {
2619 var doc = editable.getDocument(),
2620 appended;
2621
2622 // Append <br data-cke-eol="1"> to the fragment.
2623 if ( that.appendEolBr ) {
2624 appended = this.createEolBr( doc );
2625 that.fragment.append( appended );
2626 }
2627
2628 // Prepend <br data-cke-eol="1"> to the fragment but avoid duplicates. Such
2629 // elements should never follow each other in DOM.
2630 if ( that.prependEolBr && ( !appended || appended.getPrevious() ) ) {
2631 that.fragment.append( this.createEolBr( doc ), 1 );
2632 }
2633 },
2634
2635 createEolBr: function( doc ) {
2636 return doc.createElement( 'br', {
2637 attributes: {
2638 'data-cke-eol': 1
2639 }
2640 } );
2641 }
2642 },
2643
2644 bogus: {
2645 exclude: function( that ) {
2646 var boundaryNodes = that.range.getBoundaryNodes(),
2647 startNode = boundaryNodes.startNode,
2648 endNode = boundaryNodes.endNode;
2649
2650 // If bogus is the last node in range but not the only node, exclude it.
2651 if ( endNode && isBogus( endNode ) && ( !startNode || !startNode.equals( endNode ) ) )
2652 that.range.setEndBefore( endNode );
2653 }
2654 },
2655
2656 tree: {
2657 rebuild: function( that, editable ) {
2658 var range = that.range,
2659 node = range.getCommonAncestor(),
2660
2661 // A path relative to the common ancestor.
2662 commonPath = new CKEDITOR.dom.elementPath( node, editable ),
2663 startPath = new CKEDITOR.dom.elementPath( range.startContainer, editable ),
2664 endPath = new CKEDITOR.dom.elementPath( range.endContainer, editable ),
2665 limit;
2666
2667 if ( node.type == CKEDITOR.NODE_TEXT )
2668 node = node.getParent();
2669
2670 // Fix DOM of partially enclosed tables
2671 // <table><tbody><tr><td>a{b</td><td>c}d</td></tr></tbody></table>
2672 // Full table is returned
2673 // <table><tbody><tr><td>b</td><td>c</td></tr></tbody></table>
2674 // instead of
2675 // <td>b</td><td>c</td>
2676 if ( commonPath.blockLimit.is( { tr: 1, table: 1 } ) ) {
2677 var tableParent = commonPath.contains( 'table' ).getParent();
2678
2679 limit = function( node ) {
2680 return !node.equals( tableParent );
2681 };
2682 }
2683
2684 // Fix DOM in the following case
2685 // <ol><li>a{b<ul><li>c}d</li></ul></li></ol>
2686 // Full list is returned
2687 // <ol><li>b<ul><li>c</li></ul></li></ol>
2688 // instead of
2689 // b<ul><li>c</li></ul>
2690 else if ( commonPath.block && commonPath.block.is( CKEDITOR.dtd.$listItem ) ) {
2691 var startList = startPath.contains( CKEDITOR.dtd.$list ),
2692 endList = endPath.contains( CKEDITOR.dtd.$list );
2693
2694 if ( !startList.equals( endList ) ) {
2695 var listParent = commonPath.contains( CKEDITOR.dtd.$list ).getParent();
2696
2697 limit = function( node ) {
2698 return !node.equals( listParent );
2699 };
2700 }
2701 }
2702
2703 // If not defined, use generic limit function.
2704 if ( !limit ) {
2705 limit = function( node ) {
2706 return !node.equals( commonPath.block ) && !node.equals( commonPath.blockLimit );
2707 };
2708 }
2709
2710 this.rebuildFragment( that, editable, node, limit );
2711 },
2712
2713 rebuildFragment: function( that, editable, node, checkLimit ) {
2714 var clone;
2715
2716 while ( node && !node.equals( editable ) && checkLimit( node ) ) {
2717 // Don't clone children. Preserve element ids.
2718 clone = node.clone( 0, 1 );
2719 that.fragment.appendTo( clone );
2720 that.fragment = clone;
2721
2722 node = node.getParent();
2723 }
2724 }
2725 },
2726
2727 cell: {
2728 // Handle range anchored in table row with a single cell enclosed:
2729 // <table><tbody><tr>[<td>a</td>]</tr></tbody></table>
2730 // becomes
2731 // <table><tbody><tr><td>{a}</td></tr></tbody></table>
2732 shrink: function( that ) {
2733 var range = that.range,
2734 startContainer = range.startContainer,
2735 endContainer = range.endContainer,
2736 startOffset = range.startOffset,
2737 endOffset = range.endOffset;
2738
2739 if ( startContainer.type == CKEDITOR.NODE_ELEMENT && startContainer.equals( endContainer ) && startContainer.is( 'tr' ) && ++startOffset == endOffset ) {
2740 range.shrink( CKEDITOR.SHRINK_TEXT );
2741 }
2742 }
2743 }
2744 };
2745
2746 //
2747 // Helpers for editable.extractHtmlFromRange.
2748 //
2749 extractHtmlFromRangeHelpers = ( function() {
2750 function optimizeBookmarkNode( node, toStart ) {
2751 var parent = node.getParent();
2752
2753 if ( parent.is( CKEDITOR.dtd.$inline ) )
2754 node[ toStart ? 'insertBefore' : 'insertAfter' ]( parent );
2755 }
2756
2757 function mergeElements( merged, startBookmark, endBookmark ) {
2758 optimizeBookmarkNode( startBookmark );
2759 optimizeBookmarkNode( endBookmark, 1 );
2760
2761 var next;
2762 while ( ( next = endBookmark.getNext() ) ) {
2763 next.insertAfter( startBookmark );
2764
2765 // Update startBookmark after insertion to avoid the reversal of nodes (#13449).
2766 startBookmark = next;
2767 }
2768
2769 if ( isEmpty( merged ) )
2770 merged.remove();
2771 }
2772
2773 function getPath( startElement, root ) {
2774 return new CKEDITOR.dom.elementPath( startElement, root );
2775 }
2776
2777 // Creates a range from a bookmark without removing the bookmark.
2778 function createRangeFromBookmark( root, bookmark ) {
2779 var range = new CKEDITOR.dom.range( root );
2780 range.setStartAfter( bookmark.startNode );
2781 range.setEndBefore( bookmark.endNode );
2782 return range;
2783 }
2784
2785 var list = {
2786 detectMerge: function( that, editable ) {
2787 var range = createRangeFromBookmark( editable, that.bookmark ),
2788 startPath = range.startPath(),
2789 endPath = range.endPath(),
2790
2791 startList = startPath.contains( CKEDITOR.dtd.$list ),
2792 endList = endPath.contains( CKEDITOR.dtd.$list );
2793
2794 that.mergeList =
2795 // Both lists must exist
2796 startList && endList &&
2797 // ...and be of the same type
2798 // startList.getName() == endList.getName() &&
2799 // ...and share the same parent (same level in the tree)
2800 startList.getParent().equals( endList.getParent() ) &&
2801 // ...and must be different.
2802 !startList.equals( endList );
2803
2804 that.mergeListItems =
2805 startPath.block && endPath.block &&
2806 // Both containers must be list items
2807 startPath.block.is( CKEDITOR.dtd.$listItem ) && endPath.block.is( CKEDITOR.dtd.$listItem );
2808
2809 // Create merge bookmark.
2810 if ( that.mergeList || that.mergeListItems ) {
2811 var rangeClone = range.clone();
2812
2813 rangeClone.setStartBefore( that.bookmark.startNode );
2814 rangeClone.setEndAfter( that.bookmark.endNode );
2815
2816 that.mergeListBookmark = rangeClone.createBookmark();
2817 }
2818 },
2819
2820 merge: function( that, editable ) {
2821 if ( !that.mergeListBookmark )
2822 return;
2823
2824 var startNode = that.mergeListBookmark.startNode,
2825 endNode = that.mergeListBookmark.endNode,
2826
2827 startPath = getPath( startNode, editable ),
2828 endPath = getPath( endNode, editable );
2829
2830 if ( that.mergeList ) {
2831 var firstList = startPath.contains( CKEDITOR.dtd.$list ),
2832 secondList = endPath.contains( CKEDITOR.dtd.$list );
2833
2834 if ( !firstList.equals( secondList ) ) {
2835 secondList.moveChildren( firstList );
2836 secondList.remove();
2837 }
2838 }
2839
2840 if ( that.mergeListItems ) {
2841 var firstListItem = startPath.contains( CKEDITOR.dtd.$listItem ),
2842 secondListItem = endPath.contains( CKEDITOR.dtd.$listItem );
2843
2844 if ( !firstListItem.equals( secondListItem ) ) {
2845 mergeElements( secondListItem, startNode, endNode );
2846 }
2847 }
2848
2849 // Remove bookmark nodes.
2850 startNode.remove();
2851 endNode.remove();
2852 }
2853 };
2854
2855 var block = {
2856 // Detects whether blocks should be merged once contents are extracted.
2857 detectMerge: function( that, editable ) {
2858 // Don't merge blocks if lists or tables are already involved.
2859 if ( that.tableContentsRanges || that.mergeListBookmark )
2860 return;
2861
2862 var rangeClone = new CKEDITOR.dom.range( editable );
2863
2864 rangeClone.setStartBefore( that.bookmark.startNode );
2865 rangeClone.setEndAfter( that.bookmark.endNode );
2866
2867 that.mergeBlockBookmark = rangeClone.createBookmark();
2868 },
2869
2870 merge: function( that, editable ) {
2871 if ( !that.mergeBlockBookmark || that.purgeTableBookmark )
2872 return;
2873
2874 var startNode = that.mergeBlockBookmark.startNode,
2875 endNode = that.mergeBlockBookmark.endNode,
2876
2877 startPath = getPath( startNode, editable ),
2878 endPath = getPath( endNode, editable ),
2879
2880 firstBlock = startPath.block,
2881 secondBlock = endPath.block;
2882
2883 if ( firstBlock && secondBlock && !firstBlock.equals( secondBlock ) ) {
2884 mergeElements( secondBlock, startNode, endNode );
2885 }
2886
2887 // Remove bookmark nodes.
2888 startNode.remove();
2889 endNode.remove();
2890 }
2891 };
2892
2893 var table = ( function() {
2894 var tableEditable = { td: 1, th: 1, caption: 1 };
2895
2896 // Returns an array of ranges which should be entirely extracted.
2897 //
2898 // <table><tr>[<td>xx</td><td>y}y</td></tr></table>
2899 // will find:
2900 // <table><tr><td>[xx]</td><td>[y}y</td></tr></table>
2901 function findTableContentsRanges( range ) {
2902 // Leaving the below for debugging purposes.
2903 //
2904 // console.log( 'findTableContentsRanges' );
2905 // console.log( bender.tools.range.getWithHtml( range.root, range ) );
2906
2907 var contentsRanges = [],
2908 editableRange,
2909 walker = new CKEDITOR.dom.walker( range ),
2910 startCell = range.startPath().contains( tableEditable ),
2911 endCell = range.endPath().contains( tableEditable ),
2912 database = {};
2913
2914 walker.guard = function( node, leaving ) {
2915 // Guard may be executed on some node boundaries multiple times,
2916 // what results in creating more than one range for each selected cell. (#12964)
2917 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
2918 var key = 'visited_' + ( leaving ? 'out' : 'in' );
2919 if ( node.getCustomData( key ) ) {
2920 return;
2921 }
2922
2923 CKEDITOR.dom.element.setMarker( database, node, key, 1 );
2924 }
2925
2926 // Handle partial selection in a cell in which the range starts:
2927 // <td><p>x{xx</p></td>...
2928 // will store:
2929 // <td><p>x{xx</p>]</td>
2930 if ( leaving && startCell && node.equals( startCell ) ) {
2931 editableRange = range.clone();
2932 editableRange.setEndAt( startCell, CKEDITOR.POSITION_BEFORE_END );
2933 contentsRanges.push( editableRange );
2934 return;
2935 }
2936
2937 // Handle partial selection in a cell in which the range ends.
2938 if ( !leaving && endCell && node.equals( endCell ) ) {
2939 editableRange = range.clone();
2940 editableRange.setStartAt( endCell, CKEDITOR.POSITION_AFTER_START );
2941 contentsRanges.push( editableRange );
2942 return;
2943 }
2944
2945 // Handle all other cells visited by the walker.
2946 // We need to check whether the cell is disjoint with
2947 // the start and end cells to correctly handle case like:
2948 // <td>x{x</td><td><table>..<td>y}y</td>..</table></td>
2949 // without the check the second cell's content would be entirely removed.
2950 if ( !leaving && checkRemoveCellContents( node ) ) {
2951 editableRange = range.clone();
2952 editableRange.selectNodeContents( node );
2953 contentsRanges.push( editableRange );
2954 }
2955 };
2956
2957 walker.lastForward();
2958
2959 // Clear all markers so next extraction will not be affected by this one.
2960 CKEDITOR.dom.element.clearAllMarkers( database );
2961
2962 return contentsRanges;
2963
2964 function checkRemoveCellContents( node ) {
2965 return (
2966 // Must be a cell.
2967 node.type == CKEDITOR.NODE_ELEMENT && node.is( tableEditable ) &&
2968 // Must be disjoint with the range's startCell if exists.
2969 ( !startCell || checkDisjointNodes( node, startCell ) ) &&
2970 // Must be disjoint with the range's endCell if exists.
2971 ( !endCell || checkDisjointNodes( node, endCell ) )
2972 );
2973 }
2974 }
2975
2976 // Returns a normalized common ancestor of a range.
2977 // If the real common ancestor is located somewhere in between a table and a td/th/caption,
2978 // then the table will be returned.
2979 function getNormalizedAncestor( range ) {
2980 var common = range.getCommonAncestor();
2981
2982 if ( common.is( CKEDITOR.dtd.$tableContent ) && !common.is( tableEditable ) ) {
2983 common = common.getAscendant( 'table', true );
2984 }
2985
2986 return common;
2987 }
2988
2989 // Check whether node1 and node2 are disjoint, so are:
2990 // * not identical,
2991 // * not contained in each other.
2992 function checkDisjointNodes( node1, node2 ) {
2993 var disallowedPositions = CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_IS_CONTAINED,
2994 pos = node1.getPosition( node2 );
2995
2996 // Baaah... IDENTICAL is 0, so we can't simplify this ;/.
2997 return pos === CKEDITOR.POSITION_IDENTICAL ?
2998 false :
2999 ( ( pos & disallowedPositions ) === 0 );
3000 }
3001
3002 return {
3003 // Detects whether to purge entire list.
3004 detectPurge: function( that ) {
3005 var range = that.range,
3006 walkerRange = range.clone();
3007
3008 walkerRange.enlarge( CKEDITOR.ENLARGE_ELEMENT );
3009
3010 var walker = new CKEDITOR.dom.walker( walkerRange ),
3011 editablesCount = 0;
3012
3013 // Count the number of table editables in the range. If there's more than one,
3014 // table MAY be removed completely (it's a cross-cell range). Otherwise, only
3015 // the contents of the cell are usually removed.
3016 walker.evaluator = function( node ) {
3017 if ( node.type == CKEDITOR.NODE_ELEMENT && node.is( tableEditable ) ) {
3018 ++editablesCount;
3019 }
3020 };
3021
3022 walker.checkForward();
3023
3024 if ( editablesCount > 1 ) {
3025 var startTable = range.startPath().contains( 'table' ),
3026 endTable = range.endPath().contains( 'table' );
3027
3028 if ( startTable && endTable && range.checkBoundaryOfElement( startTable, CKEDITOR.START ) && range.checkBoundaryOfElement( endTable, CKEDITOR.END ) ) {
3029 var rangeClone = that.range.clone();
3030
3031 rangeClone.setStartBefore( startTable );
3032 rangeClone.setEndAfter( endTable );
3033
3034 that.purgeTableBookmark = rangeClone.createBookmark();
3035 }
3036 }
3037 },
3038
3039 // The magic.
3040 //
3041 // This method tries to discover whether the range starts or ends somewhere in a table
3042 // (it is not interested whether the range contains a table, because in such case
3043 // the extractContents() methods does the job correctly).
3044 // If the range meets these criteria, then the method tries to discover and store the following:
3045 //
3046 // * that.tableSurroundingRange - a part of the range which is located outside of any table which
3047 // will be touched (note: when range is located in a single cell it does not touch the table).
3048 // This range can be placed at:
3049 // * at the beginning: <p>he{re</p><table>..]..</table>
3050 // * in the middle: <table>..[..</table><p>here</p><table>..]..</table>
3051 // * at the end: <table>..[..</table><p>he}re</p>
3052 // * that.tableContentsRanges - an array of ranges with contents of td/th/caption that should be removed.
3053 // This assures that calling extractContents() does not change the structure of the table(s).
3054 detectRanges: function( that, editable ) {
3055 var range = createRangeFromBookmark( editable, that.bookmark ),
3056 surroundingRange = range.clone(),
3057 leftRange,
3058 rightRange,
3059
3060 // Find a common ancestor and normalize it (so the following paths contain tables).
3061 commonAncestor = getNormalizedAncestor( range ),
3062
3063 // Create paths using the normalized ancestor, so tables beyond the context
3064 // of the input range are not found.
3065 startPath = new CKEDITOR.dom.elementPath( range.startContainer, commonAncestor ),
3066 endPath = new CKEDITOR.dom.elementPath( range.endContainer, commonAncestor ),
3067
3068 startTable = startPath.contains( 'table' ),
3069 endTable = endPath.contains( 'table' ),
3070
3071 tableContentsRanges;
3072
3073 // Nothing to do here - the range doesn't touch any table or
3074 // it contains a table, but that table is fully selected so it will be simply fully removed
3075 // by the normal algorithm.
3076 if ( !startTable && !endTable ) {
3077 return;
3078 }
3079
3080 // Handle two disjoint tables case:
3081 // <table>..[..</table><p>ab</p><table>..]..</table>
3082 // is handled as (respectively: findTableContents( left ), surroundingRange, findTableContents( right )):
3083 // <table>..[..</table>][<p>ab</p>][<table>..]..</table>
3084 // Check that tables are disjoint to exclude a case when start equals end or one is contained
3085 // in the other.
3086 if ( startTable && endTable && checkDisjointNodes( startTable, endTable ) ) {
3087 that.tableSurroundingRange = surroundingRange;
3088 surroundingRange.setStartAt( startTable, CKEDITOR.POSITION_AFTER_END );
3089 surroundingRange.setEndAt( endTable, CKEDITOR.POSITION_BEFORE_START );
3090
3091 leftRange = range.clone();
3092 leftRange.setEndAt( startTable, CKEDITOR.POSITION_AFTER_END );
3093
3094 rightRange = range.clone();
3095 rightRange.setStartAt( endTable, CKEDITOR.POSITION_BEFORE_START );
3096
3097 tableContentsRanges = findTableContentsRanges( leftRange ).concat( findTableContentsRanges( rightRange ) );
3098 }
3099 // Divide the initial range into two parts:
3100 // * range which contains the part containing the table,
3101 // * surroundingRange which contains the part outside the table.
3102 //
3103 // The surroundingRange exists only if one of the range ends is
3104 // located outside the table.
3105 //
3106 // <p>a{b</p><table>..]..</table><p>cd</p>
3107 // becomes (respectively: surroundingRange, range):
3108 // <p>a{b</p>][<table>..]..</table><p>cd</p>
3109 else if ( !startTable ) {
3110 that.tableSurroundingRange = surroundingRange;
3111 surroundingRange.setEndAt( endTable, CKEDITOR.POSITION_BEFORE_START );
3112
3113 range.setStartAt( endTable, CKEDITOR.POSITION_AFTER_START );
3114 }
3115 // <p>ab</p><table>..[..</table><p>c}d</p>
3116 // becomes (respectively range, surroundingRange):
3117 // <p>ab</p><table>..[..</table>][<p>c}d</p>
3118 else if ( !endTable ) {
3119 that.tableSurroundingRange = surroundingRange;
3120 surroundingRange.setStartAt( startTable, CKEDITOR.POSITION_AFTER_END );
3121
3122 range.setEndAt( startTable, CKEDITOR.POSITION_AFTER_END );
3123 }
3124
3125 // Use already calculated or calculate for the remaining range.
3126 that.tableContentsRanges = tableContentsRanges ? tableContentsRanges : findTableContentsRanges( range );
3127
3128 // Leaving the below for debugging purposes.
3129 //
3130 // if ( that.tableSurroundingRange ) {
3131 // console.log( 'tableSurroundingRange' );
3132 // console.log( bender.tools.range.getWithHtml( that.tableSurroundingRange.root, that.tableSurroundingRange ) );
3133 // }
3134 //
3135 // console.log( 'tableContentsRanges' );
3136 // that.tableContentsRanges.forEach( function( range ) {
3137 // console.log( bender.tools.range.getWithHtml( range.root, range ) );
3138 // } );
3139 },
3140
3141 deleteRanges: function( that ) {
3142 var range;
3143
3144 // Delete table cell contents.
3145 while ( ( range = that.tableContentsRanges.pop() ) ) {
3146 range.extractContents();
3147
3148 if ( isEmpty( range.startContainer ) )
3149 range.startContainer.appendBogus();
3150 }
3151
3152 // Finally delete surroundings of the table.
3153 if ( that.tableSurroundingRange ) {
3154 that.tableSurroundingRange.extractContents();
3155 }
3156 },
3157
3158 purge: function( that ) {
3159 if ( !that.purgeTableBookmark )
3160 return;
3161
3162 var doc = that.doc,
3163 range = that.range,
3164 rangeClone = range.clone(),
3165 // How about different enter modes?
3166 block = doc.createElement( 'p' );
3167
3168 block.insertBefore( that.purgeTableBookmark.startNode );
3169
3170 rangeClone.moveToBookmark( that.purgeTableBookmark );
3171 rangeClone.deleteContents();
3172
3173 that.range.moveToPosition( block, CKEDITOR.POSITION_AFTER_START );
3174 }
3175 };
3176 } )();
3177
3178 return {
3179 list: list,
3180 block: block,
3181 table: table,
3182
3183 // Detects whether use "mergeThen" argument in range.extractContents().
3184 detectExtractMerge: function( that ) {
3185 // Don't merge if playing with lists.
3186 return !(
3187 that.range.startPath().contains( CKEDITOR.dtd.$listItem ) &&
3188 that.range.endPath().contains( CKEDITOR.dtd.$listItem )
3189 );
3190 },
3191
3192 fixUneditableRangePosition: function( range ) {
3193 if ( !range.startContainer.getDtd()[ '#' ] ) {
3194 range.moveToClosestEditablePosition( null, true );
3195 }
3196 },
3197
3198 // Perform auto paragraphing if needed.
3199 autoParagraph: function( editor, range ) {
3200 var path = range.startPath(),
3201 fixBlock;
3202
3203 if ( shouldAutoParagraph( editor, path.block, path.blockLimit ) && ( fixBlock = autoParagraphTag( editor ) ) ) {
3204 fixBlock = range.document.createElement( fixBlock );
3205 fixBlock.appendBogus();
3206 range.insertNode( fixBlock );
3207 range.moveToPosition( fixBlock, CKEDITOR.POSITION_AFTER_START );
3208 }
3209 }
3210 };
3211 } )();
3212
3213 } )();
3214
3215 /**
3216 * Whether the editor must output an empty value (`''`) if its content only consists
3217 * of an empty paragraph.
3218 *
3219 * config.ignoreEmptyParagraph = false;
3220 *
3221 * @cfg {Boolean} [ignoreEmptyParagraph=true]
3222 * @member CKEDITOR.config
3223 */
3224
3225 /**
3226 * Event fired by the editor in order to get accessibility help label.
3227 * The event is responded to by a component which provides accessibility
3228 * help (i.e. the `a11yhelp` plugin) hence the editor is notified whether
3229 * accessibility help is available.
3230 *
3231 * Providing info:
3232 *
3233 * editor.on( 'ariaEditorHelpLabel', function( evt ) {
3234 * evt.data.label = editor.lang.common.editorHelp;
3235 * } );
3236 *
3237 * Getting label:
3238 *
3239 * var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
3240 *
3241 * @since 4.4.3
3242 * @event ariaEditorHelpLabel
3243 * @param {String} label The label to be used.
3244 * @member CKEDITOR.editor
3245 */
3246
3247 /**
3248 * Event fired when the user double-clicks in the editable area.
3249 * The event allows to open a dialog window for a clicked element in a convenient way:
3250 *
3251 * editor.on( 'doubleclick', function( evt ) {
3252 * var element = evt.data.element;
3253 *
3254 * if ( element.is( 'table' ) )
3255 * evt.data.dialog = 'tableProperties';
3256 * } );
3257 *
3258 * **Note:** To handle double-click on a widget use {@link CKEDITOR.plugins.widget#doubleclick}.
3259 *
3260 * @event doubleclick
3261 * @param data
3262 * @param {CKEDITOR.dom.element} data.element The double-clicked element.
3263 * @param {String} data.dialog The dialog window to be opened. If set by the listener,
3264 * the specified dialog window will be opened.
3265 * @member CKEDITOR.editor
3266 */