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