2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
8 * Editable class which provides all editing related activities by
9 * the `contenteditable` element, dynamically get attached to editor instance.
11 * @class CKEDITOR.editable
12 * @extends CKEDITOR.dom.element
14 CKEDITOR
.editable
= CKEDITOR
.tools
.createClass( {
15 base: CKEDITOR
.dom
.element
,
17 * The constructor only stores generic editable creation logic that is commonly shared among
18 * all different editable elements.
20 * @constructor Creates an editable class instance.
21 * @param {CKEDITOR.editor} editor The editor instance on which the editable operates.
22 * @param {HTMLElement/CKEDITOR.dom.element} element Any DOM element that was as the editor's
23 * editing container, e.g. it could be either an HTML element with the `contenteditable` attribute
24 * set to the `true` that handles WYSIWYG editing or a `<textarea>` element that handles source editing.
26 $: function( editor
, element
) {
27 // Transform the element into a CKEDITOR.dom.element instance.
28 this.base( element
.$ || element
);
33 * Indicates the initialization status of the editable element. The following statuses are available:
35 * * **unloaded** – the initial state. The editable's instance was created but
36 * is not fully loaded (in particular it has no data).
37 * * **ready** – the editable is fully initialized. The `ready` status is set after
38 * the first {@link CKEDITOR.editor#method-setData} is called.
39 * * **detached** – the editable was detached.
45 this.status
= 'unloaded';
48 * Indicates whether the editable element gained focus.
50 * @property {Boolean} hasFocus
52 this.hasFocus
= false;
54 // The bootstrapping logic.
63 // [Webkit] When DOM focus is inside of nested contenteditable elements,
64 // apply focus on the main editable will compromise it's text selection.
65 if ( CKEDITOR
.env
.webkit
&& !this.hasFocus
) {
66 // Restore focus on element which we cached (on selectionCheck) as previously active.
67 active
= this.editor
._
.previousActive
|| this.getDocument().getActive();
68 if ( this.contains( active
) ) {
74 // [IE] Use instead "setActive" method to focus the editable if it belongs to
75 // the host page document, to avoid bringing an unexpected scroll.
77 this.$[ CKEDITOR
.env
.ie
&& this.getDocument().equals( CKEDITOR
.document
) ? 'setActive' : 'focus' ]();
79 // IE throws unspecified error when focusing editable after closing dialog opened on nested editable.
80 if ( !CKEDITOR
.env
.ie
)
84 // Remedy if Safari doens't applies focus properly. (#279)
85 if ( CKEDITOR
.env
.safari
&& !this.isInline() ) {
86 active
= CKEDITOR
.document
.getActive();
87 if ( !active
.equals( this.getWindow().getFrame() ) )
88 this.getWindow().focus();
94 * Overrides {@link CKEDITOR.dom.element#on} to have special `focus/blur` handling.
95 * The `focusin/focusout` events are used in IE to replace regular `focus/blur` events
96 * because we want to avoid the asynchronous nature of later ones.
98 on: function( name
, fn
) {
99 var args
= Array
.prototype.slice
.call( arguments
, 0 );
101 if ( CKEDITOR
.env
.ie
&& ( /^focus|blur$/ ).exec( name
) ) {
102 name
= name
== 'focus' ? 'focusin' : 'focusout';
104 // The "focusin/focusout" events bubbled, e.g. If there are elements with layout
105 // they fire this event when clicking in to edit them but it must be ignored
106 // to allow edit their contents. (#4682)
107 fn
= isNotBubbling( fn
, this );
112 return CKEDITOR
.dom
.element
.prototype.on
.apply( this, args
);
116 * Registers an event listener that needs to be removed when detaching this editable.
117 * This means that it will be automatically removed when {@link #detach} is executed,
118 * for example on {@link CKEDITOR.editor#setMode changing editor mode} or destroying editor.
120 * Except for `obj` all other arguments have the same meaning as in {@link CKEDITOR.event#on}.
122 * This method is strongly related to the {@link CKEDITOR.editor#contentDom} and
123 * {@link CKEDITOR.editor#contentDomUnload} events, because they are fired
124 * when an editable is being attached and detached. Therefore, this method is usually used
125 * in the following way:
127 * editor.on( 'contentDom', function() {
128 * var editable = editor.editable();
129 * editable.attachListener( editable, 'mousedown', function() {
134 * This code will attach the `mousedown` listener every time a new editable is attached
135 * to the editor, which in classic (`iframe`-based) editor happens every time the
136 * data or the mode is set. This listener will also be removed when that editable is detached.
138 * It is also possible to attach a listener to another object (e.g. to a document).
140 * editor.on( 'contentDom', function() {
141 * editor.editable().attachListener( editor.document, 'mousedown', function() {
146 * @param {CKEDITOR.event} obj The element/object to which the listener will be attached. Every object
147 * which inherits from {@link CKEDITOR.event} may be used including {@link CKEDITOR.dom.element},
148 * {@link CKEDITOR.dom.document}, and {@link CKEDITOR.editable}.
149 * @param {String} eventName The name of the event that will be listened to.
150 * @param {Function} listenerFunction The function listening to the
151 * event. A single {@link CKEDITOR.eventInfo} object instance
152 * containing all the event data is passed to this function.
153 * @param {Object} [scopeObj] The object used to scope the listener
154 * call (the `this` object). If omitted, the current object is used.
155 * @param {Object} [listenerData] Data to be sent as the
156 * {@link CKEDITOR.eventInfo#listenerData} when calling the listener.
157 * @param {Number} [priority=10] The listener priority. Lower priority
158 * listeners are called first. Listeners with the same priority
159 * value are called in the registration order.
160 * @returns {Object} An object containing the `removeListener`
161 * function that can be used to remove the listener at any time.
163 attachListener: function( obj
/*, event, fn, scope, listenerData, priority*/ ) {
164 !this._
.listeners
&& ( this._
.listeners
= [] );
165 // Register the listener.
166 var args
= Array
.prototype.slice
.call( arguments
, 1 ),
167 listener
= obj
.on
.apply( obj
, args
);
169 this._
.listeners
.push( listener
);
175 * Remove all event listeners registered from {@link #attachListener}.
177 clearListeners: function() {
178 var listeners
= this._
.listeners
;
179 // Don't get broken by this.
181 while ( listeners
.length
)
182 listeners
.pop().removeListener();
187 * Restore all attribution changes made by {@link #changeAttr }.
189 restoreAttrs: function() {
190 var changes
= this._
.attrChanges
, orgVal
;
191 for ( var attr
in changes
) {
192 if ( changes
.hasOwnProperty( attr
) ) {
193 orgVal
= changes
[ attr
];
194 // Restore original attribute.
195 orgVal
!== null ? this.setAttribute( attr
, orgVal
) : this.removeAttribute( attr
);
201 * Adds a CSS class name to this editable that needs to be removed on detaching.
203 * @param {String} className The class name to be added.
204 * @see CKEDITOR.dom.element#addClass
206 attachClass: function( cls
) {
207 var classes
= this.getCustomData( 'classes' );
208 if ( !this.hasClass( cls
) ) {
209 !classes
&& ( classes
= [] ), classes
.push( cls
);
210 this.setCustomData( 'classes', classes
);
211 this.addClass( cls
);
216 * Make an attribution change that would be reverted on editable detaching.
217 * @param {String} attr The attribute name to be changed.
218 * @param {String} val The value of specified attribute.
220 changeAttr: function( attr
, val
) {
221 var orgVal
= this.getAttribute( attr
);
222 if ( val
!== orgVal
) {
223 !this._
.attrChanges
&& ( this._
.attrChanges
= {} );
225 // Saved the original attribute val.
226 if ( !( attr
in this._
.attrChanges
) )
227 this._
.attrChanges
[ attr
] = orgVal
;
229 this.setAttribute( attr
, val
);
234 * Low-level method for inserting text into the editable.
235 * See the {@link CKEDITOR.editor#method-insertText} method which is the editor-level API
238 * @param {String} text
240 insertText: function( text
) {
241 // Focus the editor before calling transformPlainTextToHtml. (#12726)
243 this.insertHtml( this.transformPlainTextToHtml( text
), 'text' );
247 * Transforms plain text to HTML based on current selection and {@link CKEDITOR.editor#activeEnterMode}.
250 * @param {String} text Text to transform.
251 * @returns {String} HTML generated from the text.
253 transformPlainTextToHtml: function( text
) {
254 var enterMode
= this.editor
.getSelection().getStartElement().hasAscendant( 'pre', true ) ?
256 this.editor
.activeEnterMode
;
258 return CKEDITOR
.tools
.transformPlainTextToHtml( text
, enterMode
);
262 * Low-level method for inserting HTML into the editable.
263 * See the {@link CKEDITOR.editor#method-insertHtml} method which is the editor-level API
266 * This method will insert HTML into the current selection or a given range. It also creates an undo snapshot,
267 * scrolls the viewport to the insertion and selects the range next to the inserted content.
268 * If you want to insert HTML without additional operations use {@link #method-insertHtmlIntoRange}.
270 * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
272 * @param {String} data The HTML to be inserted.
273 * @param {String} [mode='html'] See {@link CKEDITOR.editor#method-insertHtml}'s param.
274 * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range
275 * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case).
276 * Introduced in CKEditor 4.5.
278 insertHtml: function( data
, mode
, range
) {
279 var editor
= this.editor
;
282 editor
.fire( 'saveSnapshot' );
285 // HTML insertion only considers the first range.
286 // Note: getRanges will be overwritten for tests since we want to test
287 // custom ranges and bypass native selections.
288 range
= editor
.getSelection().getRanges()[ 0 ];
291 // Default mode is 'html'.
292 insert( this, mode
|| 'html', data
, range
);
294 // Make the final range selection.
299 this.editor
.fire( 'afterInsertHtml', {} );
303 * Inserts HTML into the position in the editor determined by the range.
305 * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects inserted
306 * HTML. If you want to do it, use {@link #method-insertHtml}.
308 * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
311 * @param {String} data HTML code to be inserted into the editor.
312 * @param {CKEDITOR.dom.range} range The range as a place of insertion.
313 * @param {String} [mode='html'] Mode in which HTML will be inserted.
314 * See {@link CKEDITOR.editor#method-insertHtml}.
316 insertHtmlIntoRange: function( data
, range
, mode
) {
317 // Default mode is 'html'
318 insert( this, mode
|| 'html', data
, range
);
320 this.editor
.fire( 'afterInsertHtml', { intoRange: range
} );
324 * Low-level method for inserting an element into the editable.
325 * See the {@link CKEDITOR.editor#method-insertElement} method which is the editor-level API
328 * This method will insert the element into the current selection or a given range. It also creates an undo
329 * snapshot, scrolls the viewport to the insertion and selects the range next to the inserted content.
330 * If you want to insert an element without additional operations use {@link #method-insertElementIntoRange}.
332 * @param {CKEDITOR.dom.element} element The element to insert.
333 * @param {CKEDITOR.dom.range} [range] If specified, the element will be inserted into the range
334 * instead of into the selection.
336 insertElement: function( element
, range
) {
337 var editor
= this.editor
;
339 // Prepare for the insertion. For example - focus editor (#11848).
341 editor
.fire( 'saveSnapshot' );
343 var enterMode
= editor
.activeEnterMode
,
344 selection
= editor
.getSelection(),
345 elementName
= element
.getName(),
346 isBlock
= CKEDITOR
.dtd
.$block
[ elementName
];
349 range
= selection
.getRanges()[ 0 ];
352 // Insert element into first range only and ignore the rest (#11183).
353 if ( this.insertElementIntoRange( element
, range
) ) {
354 range
.moveToPosition( element
, CKEDITOR
.POSITION_AFTER_END
);
356 // If we're inserting a block element, the new cursor position must be
357 // optimized. (#3100,#5436,#8950)
359 // Find next, meaningful element.
360 var next
= element
.getNext( function( node
) {
361 return isNotEmpty( node
) && !isBogus( node
);
364 if ( next
&& next
.type
== CKEDITOR
.NODE_ELEMENT
&& next
.is( CKEDITOR
.dtd
.$block
) ) {
365 // If the next one is a text block, move cursor to the start of it's content.
366 if ( next
.getDtd()[ '#' ] )
367 range
.moveToElementEditStart( next
);
368 // Otherwise move cursor to the before end of the last element.
370 range
.moveToElementEditEnd( element
);
372 // Open a new line if the block is inserted at the end of parent.
373 else if ( !next
&& enterMode
!= CKEDITOR
.ENTER_BR
) {
374 next
= range
.fixBlock( true, enterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p' );
375 range
.moveToElementEditStart( next
);
380 // Set up the correct selection.
381 selection
.selectRanges( [ range
] );
387 * Alias for {@link #insertElement}.
390 * @param {CKEDITOR.dom.element} element The element to be inserted.
392 insertElementIntoSelection: function( element
) {
393 this.insertElement( element
);
397 * Inserts an element into the position in the editor determined by the range.
399 * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects the inserted
400 * element. If you want to do it, use the {@link #method-insertElement} method.
402 * @param {CKEDITOR.dom.element} element The element to be inserted.
403 * @param {CKEDITOR.dom.range} range The range as a place of insertion.
404 * @returns {Boolean} Informs whether the insertion was successful.
406 insertElementIntoRange: function( element
, range
) {
407 var editor
= this.editor
,
408 enterMode
= editor
.config
.enterMode
,
409 elementName
= element
.getName(),
410 isBlock
= CKEDITOR
.dtd
.$block
[ elementName
];
412 if ( range
.checkReadOnly() )
415 // Remove the original contents, merge split nodes.
416 range
.deleteContents( 1 );
418 // If range is placed in inermediate element (not td or th), we need to do three things:
419 // * fill emptied <td/th>s with if browser needs them,
420 // * remove empty text nodes so IE8 won't crash (http://dev.ckeditor.com/ticket/11183#comment:8),
421 // * fix structure and move range into the <td/th> element.
422 if ( range
.startContainer
.type
== CKEDITOR
.NODE_ELEMENT
&& range
.startContainer
.is( { tr: 1, table: 1, tbody: 1, thead: 1, tfoot: 1 } ) )
423 fixTableAfterContentsDeletion( range
);
425 // If we're inserting a block at dtd-violated position, split
426 // the parent blocks until we reach blockLimit.
430 while ( ( current
= range
.getCommonAncestor( 0, 1 ) ) &&
431 ( dtd
= CKEDITOR
.dtd
[ current
.getName() ] ) &&
432 !( dtd
&& dtd
[ elementName
] ) ) {
433 // Split up inline elements.
434 if ( current
.getName() in CKEDITOR
.dtd
.span
)
435 range
.splitElement( current
);
437 // If we're in an empty block which indicate a new paragraph,
438 // simply replace it with the inserting block.(#3664)
439 else if ( range
.checkStartOfBlock() && range
.checkEndOfBlock() ) {
440 range
.setStartBefore( current
);
441 range
.collapse( true );
444 range
.splitBlock( enterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p', editor
.editable() );
449 // Insert the new node.
450 range
.insertNode( element
);
452 // Return true if insertion was successful.
457 * @see CKEDITOR.editor#setData
459 setData: function( data
, isSnapshot
) {
461 data
= this.editor
.dataProcessor
.toHtml( data
);
463 this.setHtml( data
);
464 this.fixInitialSelection();
466 // Editable is ready after first setData.
467 if ( this.status
== 'unloaded' )
468 this.status
= 'ready';
470 this.editor
.fire( 'dataReady' );
474 * @see CKEDITOR.editor#getData
476 getData: function( isSnapshot
) {
477 var data
= this.getHtml();
480 data
= this.editor
.dataProcessor
.toDataFormat( data
);
486 * Changes the read-only state of this editable.
488 * @param {Boolean} isReadOnly
490 setReadOnly: function( isReadOnly
) {
491 this.setAttribute( 'contenteditable', !isReadOnly
);
495 * Detaches this editable object from the DOM (removes classes, listeners, etc.)
498 // Cleanup the element.
499 this.removeClass( 'cke_editable' );
501 this.status
= 'detached';
503 // Save the editor reference which will be lost after
504 // calling detach from super class.
505 var editor
= this.editor
;
509 delete editor
.document
;
510 delete editor
.window
;
514 * Checks if the editable is one of the host page elements, indicates
515 * an inline editing environment.
519 isInline: function() {
520 return this.getDocument().equals( CKEDITOR
.document
);
524 * Fixes the selection and focus which may be in incorrect state after
525 * editable's inner HTML was overwritten.
527 * If the editable did not have focus, then the selection will be fixed when the editable
528 * is focused for the first time. If the editable already had focus, then the selection will
529 * be fixed immediately.
531 * To understand the problem see:
533 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
534 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusafterundoing
535 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/selectionafterfocusing
536 * * http://tests.ckeditor.dev:1030/tests/plugins/newpage/manual/selectionafternewpage
541 fixInitialSelection: function() {
544 // Deal with IE8- IEQM (the old MS selection) first.
545 if ( CKEDITOR
.env
.ie
&& ( CKEDITOR
.env
.version
< 9 || CKEDITOR
.env
.quirks
) ) {
546 if ( this.hasFocus
) {
554 // If editable did not have focus, fix the selection when it is first focused.
555 if ( !this.hasFocus
) {
556 this.once( 'focus', function() {
558 }, null, null, -999 );
559 // If editable had focus, fix the selection immediately.
565 function fixSelection() {
566 var $doc
= that
.getDocument().$,
567 $sel
= $doc
.getSelection();
569 if ( requiresFix( $sel
) ) {
570 var range
= new CKEDITOR
.dom
.range( that
);
571 range
.moveToElementEditStart( that
);
573 var $range
= $doc
.createRange();
574 $range
.setStart( range
.startContainer
.$, range
.startOffset
);
575 $range
.collapse( true );
577 $sel
.removeAllRanges();
578 $sel
.addRange( $range
);
582 function requiresFix( $sel
) {
583 // This condition covers most broken cases after setting data.
584 if ( $sel
.anchorNode
&& $sel
.anchorNode
== that
.$ ) {
589 // http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
590 // (the inline editor TC)
591 if ( CKEDITOR
.env
.webkit
) {
592 var active
= that
.getDocument().getActive();
593 if ( active
&& active
.equals( that
) && !$sel
.anchorNode
) {
599 function fixMSSelection() {
600 var $doc
= that
.getDocument().$,
601 $sel
= $doc
.selection
,
602 active
= that
.getDocument().getActive();
604 if ( $sel
.type
== 'None' && active
.equals( that
) ) {
605 var range
= new CKEDITOR
.dom
.range( that
),
607 $range
= $doc
.body
.createTextRange();
609 range
.moveToElementEditStart( that
);
611 parentElement
= range
.startContainer
;
612 if ( parentElement
.type
!= CKEDITOR
.NODE_ELEMENT
) {
613 parentElement
= parentElement
.getParent();
616 $range
.moveToElementText( parentElement
.$ );
617 $range
.collapse( true );
624 * The base of the {@link CKEDITOR.editor#getSelectedHtml} method.
627 * @method getHtmlFromRange
628 * @param {CKEDITOR.dom.range} range
629 * @returns {CKEDITOR.dom.documentFragment}
631 getHtmlFromRange: function( range
) {
632 // There's nothing to return if range is collapsed.
633 if ( range
.collapsed
)
634 return new CKEDITOR
.dom
.documentFragment( range
.document
);
636 // Info object passed between methods.
638 doc: this.getDocument(),
639 // Leave original range object untouched.
643 getHtmlFromRangeHelpers
.eol
.detect( that
, this );
644 getHtmlFromRangeHelpers
.bogus
.exclude( that
);
645 getHtmlFromRangeHelpers
.cell
.shrink( that
);
647 that
.fragment
= that
.range
.cloneContents();
649 getHtmlFromRangeHelpers
.tree
.rebuild( that
, this );
650 getHtmlFromRangeHelpers
.eol
.fix( that
, this );
652 return new CKEDITOR
.dom
.documentFragment( that
.fragment
.$ );
656 * The base of the {@link CKEDITOR.editor#extractSelectedHtml} method.
658 * **Note:** The range is modified so it matches the desired selection after extraction
659 * even though the selection is not made.
662 * @param {CKEDITOR.dom.range} range
663 * @param {Boolean} [removeEmptyBlock=false] See {@link CKEDITOR.editor#extractSelectedHtml}'s parameter.
664 * Note that the range will not be modified if this parameter is set to `true`.
665 * @returns {CKEDITOR.dom.documentFragment} The extracted fragment of the editable content.
667 extractHtmlFromRange: function( range
, removeEmptyBlock
) {
668 var helpers
= extractHtmlFromRangeHelpers
,
673 // Since it is quite hard to build a valid documentFragment
674 // out of extracted contents because DOM changes, let's mimic
675 // extracted HTML with #getHtmlFromRange. Yep. It's a hack.
676 extractedFragment
= this.getHtmlFromRange( range
);
678 // Collapsed range means that there's nothing to extract.
679 if ( range
.collapsed
) {
681 return extractedFragment
;
684 // Include inline element if possible.
685 range
.enlarge( CKEDITOR
.ENLARGE_INLINE
, 1 );
687 // This got to be done before bookmarks are created because purging
688 // depends on the position of the range at the boundaries of the table,
689 // usually distorted by bookmark spans.
690 helpers
.table
.detectPurge( that
);
692 // We'll play with DOM, let's hold the position of the range.
693 that
.bookmark
= range
.createBookmark();
694 // While bookmarked, make unaccessible, to make sure that none of the methods
695 // will try to use it (they should use that.bookmark).
696 // This is done because ranges get desynchronized with the DOM when more bookmarks
697 // is created (as for instance that.targetBookmark).
700 // The range to be restored after extraction should be kept
701 // outside of the range, so it's not removed by range.extractContents.
702 var targetRange
= this.editor
.createRange();
703 targetRange
.moveToPosition( that
.bookmark
.startNode
, CKEDITOR
.POSITION_BEFORE_START
);
704 that
.targetBookmark
= targetRange
.createBookmark();
706 // Execute content-specific detections.
707 helpers
.list
.detectMerge( that
, this );
708 helpers
.table
.detectRanges( that
, this );
709 helpers
.block
.detectMerge( that
, this );
711 // Simply, do the job.
712 if ( that
.tableContentsRanges
) {
713 helpers
.table
.deleteRanges( that
);
715 // Done here only to remove bookmark's spans.
716 range
.moveToBookmark( that
.bookmark
);
719 // To use the range we need to restore the bookmark and make
720 // the range accessible again.
721 range
.moveToBookmark( that
.bookmark
);
723 range
.extractContents( helpers
.detectExtractMerge( that
) );
726 // Move working range to desired, pre-computed position.
727 range
.moveToBookmark( that
.targetBookmark
);
729 // Make sure range is always anchored in an element. For consistency.
732 // It my happen that the uncollapsed range which referred to a valid selection,
733 // will be placed in an uneditable location after being collapsed:
734 // <tr>[<td>x</td>]</tr> -> <tr>[]<td>x</td></tr> -> <tr><td>[]x</td></tr>
735 helpers
.fixUneditableRangePosition( range
);
737 // Execute content-specific post-extract routines.
738 helpers
.list
.merge( that
, this );
739 helpers
.table
.purge( that
, this );
740 helpers
.block
.merge( that
, this );
742 // Remove empty block, duh!
743 if ( removeEmptyBlock
) {
744 var path
= range
.startPath();
746 // <p><b>^</b></p> is empty block.
748 range
.checkStartOfBlock() &&
749 range
.checkEndOfBlock() &&
751 !range
.root
.equals( path
.block
) &&
752 // Do not remove a block with bookmarks. (#13465)
753 !hasBookmarks( path
.block
) ) {
754 range
.moveToPosition( path
.block
, CKEDITOR
.POSITION_BEFORE_START
);
758 // Auto paragraph, if needed.
759 helpers
.autoParagraph( this.editor
, range
);
761 // Let's have a bogus next to the caret, if needed.
762 if ( isEmpty( range
.startContainer
) )
763 range
.startContainer
.appendBogus();
766 // Merge inline siblings if any around the caret.
767 range
.startContainer
.mergeSiblings();
769 return extractedFragment
;
773 * Editable element bootstrapping.
778 var editor
= this.editor
;
780 // Handle the load/read of editor data/snapshot.
781 this.attachListener( editor
, 'beforeGetData', function() {
782 var data
= this.getData();
784 // Post processing html output of wysiwyg editable.
785 if ( !this.is( 'textarea' ) ) {
786 // Reset empty if the document contains only one empty paragraph.
787 if ( editor
.config
.ignoreEmptyParagraph
!== false )
788 data
= data
.replace( emptyParagraphRegexp
, function( match
, lookback
) {
793 editor
.setData( data
, null, 1 );
796 this.attachListener( editor
, 'getSnapshot', function( evt
) {
797 evt
.data
= this.getData( 1 );
800 this.attachListener( editor
, 'afterSetData', function() {
801 this.setData( editor
.getData( 1 ) );
803 this.attachListener( editor
, 'loadSnapshot', function( evt
) {
804 this.setData( evt
.data
, 1 );
807 // Delegate editor focus/blur to editable.
808 this.attachListener( editor
, 'beforeFocus', function() {
809 var sel
= editor
.getSelection(),
810 ieSel
= sel
&& sel
.getNative();
812 // IE considers control-type element as separate
813 // focus host when selected, avoid destroying the
814 // selection in such case. (#5812) (#8949)
815 if ( ieSel
&& ieSel
.type
== 'Control' )
821 this.attachListener( editor
, 'insertHtml', function( evt
) {
822 this.insertHtml( evt
.data
.dataValue
, evt
.data
.mode
, evt
.data
.range
);
824 this.attachListener( editor
, 'insertElement', function( evt
) {
825 this.insertElement( evt
.data
);
827 this.attachListener( editor
, 'insertText', function( evt
) {
828 this.insertText( evt
.data
);
831 // Update editable state.
832 this.setReadOnly( editor
.readOnly
);
834 // The editable class.
835 this.attachClass( 'cke_editable' );
837 // The element mode css class.
838 if ( editor
.elementMode
== CKEDITOR
.ELEMENT_MODE_INLINE
) {
839 this.attachClass( 'cke_editable_inline' );
840 } else if ( editor
.elementMode
== CKEDITOR
.ELEMENT_MODE_REPLACE
||
841 editor
.elementMode
== CKEDITOR
.ELEMENT_MODE_APPENDTO
) {
842 this.attachClass( 'cke_editable_themed' );
845 this.attachClass( 'cke_contents_' + editor
.config
.contentsLangDirection
);
847 // Setup editor keystroke handlers on this element.
848 var keystrokeHandler
= editor
.keystrokeHandler
;
850 // If editor is read-only, then make sure that BACKSPACE key
851 // is blocked to prevent browser history navigation.
852 keystrokeHandler
.blockedKeystrokes
[ 8 ] = +editor
.readOnly
;
854 editor
.keystrokeHandler
.attach( this );
856 // Update focus states.
857 this.on( 'blur', function() {
858 this.hasFocus
= false;
861 this.on( 'focus', function() {
862 this.hasFocus
= true;
865 // Register to focus manager.
866 editor
.focusManager
.add( this );
868 // Inherit the initial focus on editable element.
869 if ( this.equals( CKEDITOR
.document
.getActive() ) ) {
870 this.hasFocus
= true;
871 // Pending until this editable has attached.
872 editor
.once( 'contentDom', function() {
873 editor
.focusManager
.focus( this );
877 // Apply tab index on demand, with original direction saved.
878 if ( this.isInline() ) {
880 // tabIndex of the editable is different than editor's one.
881 // Update the attribute of the editable.
882 this.changeAttr( 'tabindex', editor
.tabIndex
);
885 // The above is all we'll be doing for a <textarea> editable.
886 if ( this.is( 'textarea' ) )
889 // The DOM document which the editing acts upon.
890 editor
.document
= this.getDocument();
891 editor
.window
= this.getWindow();
893 var doc
= editor
.document
;
895 this.changeAttr( 'spellcheck', !editor
.config
.disableNativeSpellChecker
);
897 // Apply contents direction on demand, with original direction saved.
898 var dir
= editor
.config
.contentsLangDirection
;
899 if ( this.getDirection( 1 ) != dir
)
900 this.changeAttr( 'dir', dir
);
902 // Create the content stylesheet for this document.
903 var styles
= CKEDITOR
.getCss();
905 var head
= doc
.getHead();
906 if ( !head
.getCustomData( 'stylesheet' ) ) {
907 var sheet
= doc
.appendStyleText( styles
);
908 sheet
= new CKEDITOR
.dom
.element( sheet
.ownerNode
|| sheet
.owningElement
);
909 head
.setCustomData( 'stylesheet', sheet
);
910 sheet
.data( 'cke-temp', 1 );
914 // Update the stylesheet sharing count.
915 var ref
= doc
.getCustomData( 'stylesheet_ref' ) || 0;
916 doc
.setCustomData( 'stylesheet_ref', ref
+ 1 );
918 // Pass this configuration to styles system.
919 this.setCustomData( 'cke_includeReadonly', !editor
.config
.disableReadonlyStyling
);
921 // Prevent the browser opening read-only links. (#6032 & #10912)
922 this.attachListener( this, 'click', function( evt
) {
925 var link
= new CKEDITOR
.dom
.elementPath( evt
.getTarget(), this ).contains( 'a' );
927 if ( link
&& evt
.$.button
!= 2 && link
.isReadOnly() )
928 evt
.preventDefault();
931 var backspaceOrDelete
= { 8: 1, 46: 1 };
933 // Override keystrokes which should have deletion behavior
934 // on fully selected element . (#4047) (#7645)
935 this.attachListener( editor
, 'key', function( evt
) {
936 if ( editor
.readOnly
)
939 // Use getKey directly in order to ignore modifiers.
940 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
941 var keyCode
= evt
.data
.domEvent
.getKey(),
944 // Backspace OR Delete.
945 if ( keyCode
in backspaceOrDelete
) {
946 var sel
= editor
.getSelection(),
948 range
= sel
.getRanges()[ 0 ],
949 path
= range
.startPath(),
956 // [IE<11] Remove selected image/anchor/etc here to avoid going back in history. (#10055)
957 ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 11 && ( selected
= sel
.getSelectedElement() ) ) ||
958 // Remove the entire list/table on fully selected content. (#7645)
959 ( selected
= getSelectedTableList( sel
) ) ) {
960 // Make undo snapshot.
961 editor
.fire( 'saveSnapshot' );
963 // Delete any element that 'hasLayout' (e.g. hr,table) in IE8 will
964 // break up the selection, safely manage it here. (#4795)
965 range
.moveToPosition( selected
, CKEDITOR
.POSITION_BEFORE_START
);
966 // Remove the control manually.
970 editor
.fire( 'saveSnapshot' );
973 } else if ( range
.collapsed
) {
974 // Handle the following special cases: (#6217)
975 // 1. Del/Backspace key before/after table;
976 // 2. Backspace Key after start of table.
977 if ( ( block
= path
.block
) &&
978 ( next
= block
[ rtl
? 'getPrevious' : 'getNext' ]( isNotWhitespace
) ) &&
979 ( next
.type
== CKEDITOR
.NODE_ELEMENT
) &&
980 next
.is( 'table' ) &&
981 range
[ rtl
? 'checkStartOfBlock' : 'checkEndOfBlock' ]() ) {
982 editor
.fire( 'saveSnapshot' );
984 // Remove the current empty block.
985 if ( range
[ rtl
? 'checkEndOfBlock' : 'checkStartOfBlock' ]() )
988 // Move cursor to the beginning/end of table cell.
989 range
[ 'moveToElementEdit' + ( rtl
? 'End' : 'Start' ) ]( next
);
992 editor
.fire( 'saveSnapshot' );
996 else if ( path
.blockLimit
&& path
.blockLimit
.is( 'td' ) &&
997 ( parent
= path
.blockLimit
.getAscendant( 'table' ) ) &&
998 range
.checkBoundaryOfElement( parent
, rtl
? CKEDITOR
.START : CKEDITOR
.END
) &&
999 ( next
= parent
[ rtl
? 'getPrevious' : 'getNext' ]( isNotWhitespace
) ) ) {
1000 editor
.fire( 'saveSnapshot' );
1002 // Move cursor to the end of previous block.
1003 range
[ 'moveToElementEdit' + ( rtl
? 'End' : 'Start' ) ]( next
);
1005 // Remove any previous empty block.
1006 if ( range
.checkStartOfBlock() && range
.checkEndOfBlock() )
1011 editor
.fire( 'saveSnapshot' );
1015 // BACKSPACE/DEL pressed at the start/end of table cell.
1016 else if ( ( parent
= path
.contains( [ 'td', 'th', 'caption' ] ) ) &&
1017 range
.checkBoundaryOfElement( parent
, rtl
? CKEDITOR
.START : CKEDITOR
.END
) ) {
1027 // On IE>=11 we need to fill blockless editable with <br> if it was deleted.
1028 if ( editor
.blockless
&& CKEDITOR
.env
.ie
&& CKEDITOR
.env
.needsBrFiller
) {
1029 this.attachListener( this, 'keyup', function( evt
) {
1030 if ( evt
.data
.getKeystroke() in backspaceOrDelete
&& !this.getFirst( isNotEmpty
) ) {
1033 // Set the selection before bogus, because IE tends to put it after.
1034 var range
= editor
.createRange();
1035 range
.moveToPosition( this, CKEDITOR
.POSITION_AFTER_START
);
1041 this.attachListener( this, 'dblclick', function( evt
) {
1042 if ( editor
.readOnly
)
1045 var data
= { element: evt
.data
.getTarget() };
1046 editor
.fire( 'doubleclick', data
);
1049 // Prevent automatic submission in IE #6336
1050 CKEDITOR
.env
.ie
&& this.attachListener( this, 'click', blockInputClick
);
1052 // Gecko/Webkit need some help when selecting control type elements. (#3448)
1053 // We apply same behavior for IE Edge. (#13386)
1054 if ( !CKEDITOR
.env
.ie
|| CKEDITOR
.env
.edge
) {
1055 this.attachListener( this, 'mousedown', function( ev
) {
1056 var control
= ev
.data
.getTarget();
1057 // #11727. Note: htmlDP assures that input/textarea/select have contenteditable=false
1058 // attributes. However, they also have data-cke-editable attribute, so isReadOnly() returns false,
1059 // and therefore those elements are correctly selected by this code.
1060 if ( control
.is( 'img', 'hr', 'input', 'textarea', 'select' ) && !control
.isReadOnly() ) {
1061 editor
.getSelection().selectElement( control
);
1063 // Prevent focus from stealing from the editable. (#9515)
1064 if ( control
.is( 'input', 'textarea', 'select' ) )
1065 ev
.data
.preventDefault();
1070 // For some reason, after click event is done, IE Edge loses focus on the selected element. (#13386)
1071 if ( CKEDITOR
.env
.edge
) {
1072 this.attachListener( this, 'mouseup', function( ev
) {
1073 var selectedElement
= ev
.data
.getTarget();
1074 if ( selectedElement
&& selectedElement
.is( 'img' ) ) {
1075 editor
.getSelection().selectElement( selectedElement
);
1080 // Prevent right click from selecting an empty block even
1081 // when selection is anchored inside it. (#5845)
1082 if ( CKEDITOR
.env
.gecko
) {
1083 this.attachListener( this, 'mouseup', function( ev
) {
1084 if ( ev
.data
.$.button
== 2 ) {
1085 var target
= ev
.data
.getTarget();
1087 if ( !target
.getOuterHtml().replace( emptyParagraphRegexp
, '' ) ) {
1088 var range
= editor
.createRange();
1089 range
.moveToElementEditStart( target
);
1090 range
.select( true );
1096 // Webkit: avoid from editing form control elements content.
1097 if ( CKEDITOR
.env
.webkit
) {
1098 // Prevent from tick checkbox/radiobox/select
1099 this.attachListener( this, 'click', function( ev
) {
1100 if ( ev
.data
.getTarget().is( 'input', 'select' ) )
1101 ev
.data
.preventDefault();
1104 // Prevent from editig textfield/textarea value.
1105 this.attachListener( this, 'mouseup', function( ev
) {
1106 if ( ev
.data
.getTarget().is( 'input', 'textarea' ) )
1107 ev
.data
.preventDefault();
1111 // Prevent Webkit/Blink from going rogue when joining
1112 // blocks on BACKSPACE/DEL (#11861,#9998).
1113 if ( CKEDITOR
.env
.webkit
) {
1114 this.attachListener( editor
, 'key', function( evt
) {
1115 if ( editor
.readOnly
) {
1119 // Use getKey directly in order to ignore modifiers.
1120 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
1121 var key
= evt
.data
.domEvent
.getKey();
1123 if ( !( key
in backspaceOrDelete
) )
1126 var backspace
= key
== 8,
1127 range
= editor
.getSelection().getRanges()[ 0 ],
1128 startPath
= range
.startPath();
1130 if ( range
.collapsed
) {
1131 if ( !mergeBlocksCollapsedSelection( editor
, range
, backspace
, startPath
) )
1134 if ( !mergeBlocksNonCollapsedSelection( editor
, range
, startPath
) )
1138 // Scroll to the new position of the caret (#11960).
1139 editor
.getSelection().scrollIntoView();
1140 editor
.fire( 'saveSnapshot' );
1143 }, this, null, 100 ); // Later is better – do not override existing listeners.
1149 detach: function() {
1150 // Update the editor cached data with current data.
1151 this.editor
.setData( this.editor
.getData(), 0, 1 );
1153 this.clearListeners();
1154 this.restoreAttrs();
1156 // Cleanup our custom classes.
1158 if ( ( classes
= this.removeCustomData( 'classes' ) ) ) {
1159 while ( classes
.length
)
1160 this.removeClass( classes
.pop() );
1163 // Remove contents stylesheet from document if it's the last usage.
1164 if ( !this.is( 'textarea' ) ) {
1165 var doc
= this.getDocument(),
1166 head
= doc
.getHead();
1167 if ( head
.getCustomData( 'stylesheet' ) ) {
1168 var refs
= doc
.getCustomData( 'stylesheet_ref' );
1169 if ( !( --refs
) ) {
1170 doc
.removeCustomData( 'stylesheet_ref' );
1171 var sheet
= head
.removeCustomData( 'stylesheet' );
1174 doc
.setCustomData( 'stylesheet_ref', refs
);
1179 this.editor
.fire( 'contentDomUnload' );
1181 // Free up the editor reference.
1188 * Creates, retrieves or detaches an editable element of the editor.
1189 * This method should always be used instead of calling {@link CKEDITOR.editable} directly.
1192 * @member CKEDITOR.editor
1193 * @param {CKEDITOR.dom.element/CKEDITOR.editable} elementOrEditable The
1194 * DOM element to become the editable or a {@link CKEDITOR.editable} object.
1196 CKEDITOR
.editor
.prototype.editable = function( element
) {
1197 var editable
= this._
.editable
;
1199 // This editor has already associated with
1200 // an editable element, silently fails.
1201 if ( editable
&& element
)
1204 if ( arguments
.length
) {
1205 editable
= this._
.editable
= element
? ( element
instanceof CKEDITOR
.editable
? element : new CKEDITOR
.editable( this, element
) ) :
1206 // Detach the editable from editor.
1207 ( editable
&& editable
.detach(), null );
1210 // Just retrieve the editable.
1214 CKEDITOR
.on( 'instanceLoaded', function( evt
) {
1215 var editor
= evt
.editor
;
1217 // and flag that the element was locked by our code so it'll be editable by the editor functions (#6046).
1218 editor
.on( 'insertElement', function( evt
) {
1219 var element
= evt
.data
;
1220 if ( element
.type
== CKEDITOR
.NODE_ELEMENT
&& ( element
.is( 'input' ) || element
.is( 'textarea' ) ) ) {
1221 // // The element is still not inserted yet, force attribute-based check.
1222 if ( element
.getAttribute( 'contentEditable' ) != 'false' )
1223 element
.data( 'cke-editable', element
.hasAttribute( 'contenteditable' ) ? 'true' : '1' );
1224 element
.setAttribute( 'contentEditable', false );
1228 editor
.on( 'selectionChange', function( evt
) {
1229 if ( editor
.readOnly
)
1232 // Auto fixing on some document structure weakness to enhance usabilities. (#3190 and #3189)
1233 var sel
= editor
.getSelection();
1234 // Do it only when selection is not locked. (#8222)
1235 if ( sel
&& !sel
.isLocked
) {
1236 var isDirty
= editor
.checkDirty();
1238 // Lock undoM before touching DOM to prevent
1239 // recording these changes as separate snapshot.
1240 editor
.fire( 'lockSnapshot' );
1242 editor
.fire( 'unlockSnapshot' );
1244 !isDirty
&& editor
.resetDirty();
1249 CKEDITOR
.on( 'instanceCreated', function( evt
) {
1250 var editor
= evt
.editor
;
1252 editor
.on( 'mode', function() {
1254 var editable
= editor
.editable();
1256 // Setup proper ARIA roles and properties for inline editable, classic
1257 // (iframe-based) editable is instead handled by plugin.
1258 if ( editable
&& editable
.isInline() ) {
1260 var ariaLabel
= editor
.title
;
1262 editable
.changeAttr( 'role', 'textbox' );
1263 editable
.changeAttr( 'aria-label', ariaLabel
);
1266 editable
.changeAttr( 'title', ariaLabel
);
1268 var helpLabel
= editor
.fire( 'ariaEditorHelpLabel', {} ).label
;
1270 // Put the voice label in different spaces, depending on element mode, so
1271 // the DOM element get auto detached on mode reload or editor destroy.
1272 var ct
= this.ui
.space( this.elementMode
== CKEDITOR
.ELEMENT_MODE_INLINE
? 'top' : 'contents' );
1274 var ariaDescId
= CKEDITOR
.tools
.getNextId(),
1275 desc
= CKEDITOR
.dom
.element
.createFromHtml( '<span id="' + ariaDescId
+ '" class="cke_voice_label">' + helpLabel
+ '</span>' );
1277 editable
.changeAttr( 'aria-describedby', ariaDescId
);
1284 // #9222: Show text cursor in Gecko.
1285 // Show default cursor over control elements on all non-IEs.
1286 CKEDITOR
.addCss( '.cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}' );
1290 // Bazillion helpers for the editable class and above listeners.
1294 var isNotWhitespace
= CKEDITOR
.dom
.walker
.whitespaces( true ),
1295 isNotBookmark
= CKEDITOR
.dom
.walker
.bookmark( false, true ),
1296 isEmpty
= CKEDITOR
.dom
.walker
.empty(),
1297 isBogus
= CKEDITOR
.dom
.walker
.bogus(),
1298 // Matching an empty paragraph at the end of document.
1299 emptyParagraphRegexp
= /(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>| |\u00A0| )?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi;
1301 // Auto-fixing block-less content by wrapping paragraph (#3190), prevent
1302 // non-exitable-block by padding extra br.(#3189)
1303 // Returns truly value when dom was changed, falsy otherwise.
1304 function fixDom( evt
) {
1305 var editor
= evt
.editor
,
1306 path
= evt
.data
.path
,
1307 blockLimit
= path
.blockLimit
,
1308 selection
= evt
.data
.selection
,
1309 range
= selection
.getRanges()[ 0 ],
1310 selectionUpdateNeeded
;
1312 if ( CKEDITOR
.env
.gecko
|| ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.needsBrFiller
) ) {
1313 var blockNeedsFiller
= needsBrFiller( selection
, path
);
1314 if ( blockNeedsFiller
) {
1315 blockNeedsFiller
.appendBogus();
1316 // IE tends to place selection after appended bogus, so we need to
1317 // select the original range (placed before bogus).
1318 selectionUpdateNeeded
= CKEDITOR
.env
.ie
;
1322 // When we're in block enter mode, a new paragraph will be established
1323 // to encapsulate inline contents inside editable. (#3657)
1324 // Don't autoparagraph if browser (namely - IE) incorrectly anchored selection
1325 // inside non-editable content. This happens e.g. if non-editable block is the only
1326 // content of editable.
1327 if ( shouldAutoParagraph( editor
, path
.block
, blockLimit
) && range
.collapsed
&& !range
.getCommonAncestor().isReadOnly() ) {
1328 var testRng
= range
.clone();
1329 testRng
.enlarge( CKEDITOR
.ENLARGE_BLOCK_CONTENTS
);
1330 var walker
= new CKEDITOR
.dom
.walker( testRng
);
1331 walker
.guard = function( node
) {
1332 return !isNotEmpty( node
) ||
1333 node
.type
== CKEDITOR
.NODE_COMMENT
||
1337 // 1. Inline content discovered under cursor;
1338 // 2. Empty editable.
1339 if ( !walker
.checkForward() || testRng
.checkStartOfBlock() && testRng
.checkEndOfBlock() ) {
1340 var fixedBlock
= range
.fixBlock( true, editor
.activeEnterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p' );
1342 // For IE<11, we should remove any filler node which was introduced before.
1343 if ( !CKEDITOR
.env
.needsBrFiller
) {
1344 var first
= fixedBlock
.getFirst( isNotEmpty
);
1345 if ( first
&& isNbsp( first
) )
1349 selectionUpdateNeeded
= 1;
1351 // Cancel this selection change in favor of the next (correct). (#6811)
1356 if ( selectionUpdateNeeded
)
1360 // Checks whether current selection requires br filler to be appended.
1361 // @returns Block which needs filler or falsy value.
1362 function needsBrFiller( selection
, path
) {
1363 // Fake selection does not need filler, because it is fake.
1364 if ( selection
.isFake
)
1367 // Ensure bogus br could help to move cursor (out of styles) to the end of block. (#7041)
1368 var pathBlock
= path
.block
|| path
.blockLimit
,
1369 lastNode
= pathBlock
&& pathBlock
.getLast( isNotEmpty
);
1371 // Check some specialities of the current path block:
1372 // 1. It is really displayed as block; (#7221)
1373 // 2. It doesn't end with one inner block; (#7467)
1374 // 3. It doesn't have bogus br yet.
1376 pathBlock
&& pathBlock
.isBlockBoundary() &&
1377 !( lastNode
&& lastNode
.type
== CKEDITOR
.NODE_ELEMENT
&& lastNode
.isBlockBoundary() ) &&
1378 !pathBlock
.is( 'pre' ) && !pathBlock
.getBogus()
1383 function blockInputClick( evt
) {
1384 var element
= evt
.data
.getTarget();
1385 if ( element
.is( 'input' ) ) {
1386 var type
= element
.getAttribute( 'type' );
1387 if ( type
== 'submit' || type
== 'reset' )
1388 evt
.data
.preventDefault();
1392 function isNotEmpty( node
) {
1393 return isNotWhitespace( node
) && isNotBookmark( node
);
1396 function isNbsp( node
) {
1397 return node
.type
== CKEDITOR
.NODE_TEXT
&& CKEDITOR
.tools
.trim( node
.getText() ).match( /^(?: |\xa0)$/ );
1400 function isNotBubbling( fn
, src
) {
1401 return function( evt
) {
1402 var other
= evt
.data
.$.toElement
|| evt
.data
.$.fromElement
|| evt
.data
.$.relatedTarget
;
1404 // First of all, other may simply be null/undefined.
1405 // Second of all, at least early versions of Spartan returned empty objects from evt.relatedTarget,
1406 // so let's also check the node type.
1407 other
= ( other
&& other
.nodeType
== CKEDITOR
.NODE_ELEMENT
) ? new CKEDITOR
.dom
.element( other
) : null;
1409 if ( !( other
&& ( src
.equals( other
) || src
.contains( other
) ) ) )
1410 fn
.call( this, evt
);
1414 function hasBookmarks( element
) {
1415 // We use getElementsByTag() instead of find() to retain compatibility with IE quirks mode.
1416 var potentialBookmarks
= element
.getElementsByTag( 'span' ),
1420 if ( potentialBookmarks
) {
1421 while ( ( child
= potentialBookmarks
.getItem( i
++ ) ) ) {
1422 if ( !isNotBookmark( child
) ) {
1431 // Check if the entire table/list contents is selected.
1432 function getSelectedTableList( sel
) {
1434 range
= sel
.getRanges()[ 0 ],
1435 editable
= sel
.root
,
1436 path
= range
.startPath(),
1437 structural
= { table: 1, ul: 1, ol: 1, dl: 1 };
1439 if ( path
.contains( structural
) ) {
1440 // Clone the original range.
1441 var walkerRng
= range
.clone();
1443 // Enlarge the range: X<ul><li>[Y]</li></ul>X => [X<ul><li>]Y</li></ul>X
1444 walkerRng
.collapse( 1 );
1445 walkerRng
.setStartAt( editable
, CKEDITOR
.POSITION_AFTER_START
);
1447 // Create a new walker.
1448 var walker
= new CKEDITOR
.dom
.walker( walkerRng
);
1450 // Assign a new guard to the walker.
1451 walker
.guard
= guard();
1453 // Go backwards checking for selected structural node.
1454 walker
.checkBackward();
1456 // If there's a selected structured element when checking backwards,
1457 // then check the same forwards.
1459 // Clone the original range.
1460 walkerRng
= range
.clone();
1462 // Enlarge the range (assuming <ul> is selected element from guard):
1464 // X<ul><li>[Y]</li></ul>X => X<ul><li>Y[</li></ul>]X
1466 // If the walker went deeper down DOM than a while ago when traversing
1467 // backwards, then it doesn't make sense: an element must be selected
1468 // symmetrically. By placing range end **after previously selected node**,
1469 // we make sure we don't go no deeper in DOM when going forwards.
1470 walkerRng
.collapse();
1471 walkerRng
.setEndAt( selected
, CKEDITOR
.POSITION_AFTER_END
);
1473 // Create a new walker.
1474 walker
= new CKEDITOR
.dom
.walker( walkerRng
);
1476 // Assign a new guard to the walker.
1477 walker
.guard
= guard( true );
1479 // Reset selected node.
1482 // Go forwards checking for selected structural node.
1483 walker
.checkForward();
1491 function guard( forwardGuard
) {
1492 return function( node
, isWalkOut
) {
1493 // Save the encountered node as selected if going down the DOM structure
1494 // and the node is structured element.
1495 if ( isWalkOut
&& node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( structural
) )
1498 // Stop the walker when either traversing another non-empty node at the same
1499 // DOM level as in previous step.
1500 // NOTE: When going forwards, stop if encountered a bogus.
1501 if ( !isWalkOut
&& isNotEmpty( node
) && !( forwardGuard
&& isBogus( node
) ) )
1507 // Whether in given context (pathBlock, pathBlockLimit and editor settings)
1508 // editor should automatically wrap inline contents with blocks.
1509 function shouldAutoParagraph( editor
, pathBlock
, pathBlockLimit
) {
1510 // Check whether pathBlock equals pathBlockLimit to support nested editable (#12162).
1511 return editor
.config
.autoParagraph
!== false &&
1512 editor
.activeEnterMode
!= CKEDITOR
.ENTER_BR
&&
1514 ( editor
.editable().equals( pathBlockLimit
) && !pathBlock
) ||
1515 ( pathBlock
&& pathBlock
.getAttribute( 'contenteditable' ) == 'true' )
1519 function autoParagraphTag( editor
) {
1520 return ( editor
.activeEnterMode
!= CKEDITOR
.ENTER_BR
&& editor
.config
.autoParagraph
!== false ) ? editor
.activeEnterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p' : false;
1524 // Functions related to insertXXX methods
1526 var insert
= ( function() {
1529 var DTD
= CKEDITOR
.dtd
;
1531 // Inserts the given (valid) HTML into the range position (with range content deleted),
1532 // guarantee it's result to be a valid DOM tree.
1533 function insert( editable
, type
, data
, range
) {
1534 var editor
= editable
.editor
,
1537 if ( type
== 'unfiltered_html' ) {
1542 // Check range spans in non-editable.
1543 if ( range
.checkReadOnly() )
1546 // RANGE PREPARATIONS
1548 var path
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, range
.root
),
1549 // Let root be the nearest block that's impossible to be split
1550 // during html processing.
1551 blockLimit
= path
.blockLimit
|| range
.root
,
1552 // The "state" value.
1555 dontFilter: dontFilter
,
1559 blockLimit: blockLimit
,
1560 // During pre-processing / preparations startContainer of affectedRange should be placed
1561 // in this element in which inserted or moved (in case when we merge blocks) content
1562 // could create situation that will need merging inline elements.
1564 // <div><b>A</b>^B</div> + <b>C</b> => <div><b>A</b><b>C</b>B</div> - affected container is <div>.
1565 // <p><b>A[B</b></p><p><b>C]D</b></p> + E => <p><b>AE</b></p><p><b>D</b></p> =>
1566 // <p><b>AE</b><b>D</b></p> - affected container is <p> (in text mode).
1567 mergeCandidates: [],
1571 prepareRangeToDataInsertion( that
);
1575 // Select range and stop execution.
1576 // If data has been totally emptied after the filtering,
1577 // any insertion is pointless (#10339).
1578 if ( data
&& processDataForInsertion( that
, data
) ) {
1580 insertDataIntoRange( that
);
1584 // Set final range position and clean up.
1586 cleanupAfterInsertion( that
);
1589 // Prepare range to its data deletion.
1590 // Delete its contents.
1591 // Prepare it to insertion.
1592 function prepareRangeToDataInsertion( that
) {
1593 var range
= that
.range
,
1594 mergeCandidates
= that
.mergeCandidates
,
1595 node
, marker
, path
, startPath
, endPath
, previous
, bm
;
1597 // If range starts in inline element then insert a marker, so empty
1598 // inline elements won't be removed while range.deleteContents
1599 // and we will be able to move range back into this element.
1600 // E.g. 'aa<b>[bb</b>]cc' -> (after deleting) 'aa<b><span/></b>cc'
1601 if ( that
.type
== 'text' && range
.shrink( CKEDITOR
.SHRINK_ELEMENT
, true, false ) ) {
1602 marker
= CKEDITOR
.dom
.element
.createFromHtml( '<span> </span>', range
.document
);
1603 range
.insertNode( marker
);
1604 range
.setStartAfter( marker
);
1607 // By using path we can recover in which element was startContainer
1608 // before deleting contents.
1609 // Start and endPathElements will be used to squash selected blocks, after removing
1610 // selection contents. See rule 5.
1611 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
);
1612 that
.endPath
= endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
);
1614 if ( !range
.collapsed
) {
1615 // Anticipate the possibly empty block at the end of range after deletion.
1616 node
= endPath
.block
|| endPath
.blockLimit
;
1617 var ancestor
= range
.getCommonAncestor();
1618 if ( node
&& !( node
.equals( ancestor
) || node
.contains( ancestor
) ) && range
.checkEndOfBlock() ) {
1619 that
.zombies
.push( node
);
1622 range
.deleteContents();
1626 // Move range into the previous block.
1628 ( previous
= getRangePrevious( range
) ) && checkIfElement( previous
) && previous
.isBlockBoundary() &&
1629 // Check if previousNode was parent of range's startContainer before deleteContents.
1630 startPath
.contains( previous
)
1632 range
.moveToPosition( previous
, CKEDITOR
.POSITION_BEFORE_END
);
1635 mergeAncestorElementsOfSelectionEnds( range
, that
.blockLimit
, startPath
, endPath
);
1639 // If marker was created then move collapsed range into its place.
1640 range
.setEndBefore( marker
);
1645 // Split inline elements so HTML will be inserted with its own styles.
1646 path
= range
.startPath();
1647 if ( ( node
= path
.contains( isInline
, false, 1 ) ) ) {
1648 range
.splitElement( node
);
1649 that
.inlineStylesRoot
= node
;
1650 that
.inlineStylesPeak
= path
.lastElement
;
1653 // Record inline merging candidates for later cleanup in place.
1654 bm
= range
.createBookmark();
1656 // 1. Inline siblings.
1657 node
= bm
.startNode
.getPrevious( isNotEmpty
);
1658 node
&& checkIfElement( node
) && isInline( node
) && mergeCandidates
.push( node
);
1659 node
= bm
.startNode
.getNext( isNotEmpty
);
1660 node
&& checkIfElement( node
) && isInline( node
) && mergeCandidates
.push( node
);
1662 // 2. Inline parents.
1663 node
= bm
.startNode
;
1664 while ( ( node
= node
.getParent() ) && isInline( node
) )
1665 mergeCandidates
.push( node
);
1667 range
.moveToBookmark( bm
);
1670 function processDataForInsertion( that
, data
) {
1671 var range
= that
.range
;
1673 // Rule 8. - wrap entire data in inline styles.
1674 // (e.g. <p><b>x^z</b></p> + <p>a</p><p>b</p> -> <b><p>a</p><p>b</p></b>)
1675 // Incorrect tags order will be fixed by htmlDataProcessor.
1676 if ( that
.type
== 'text' && that
.inlineStylesRoot
)
1677 data
= wrapDataWithInlineStyles( data
, that
);
1680 var context
= that
.blockLimit
.getName();
1682 // Wrap data to be inserted, to avoid losing leading whitespaces
1683 // when going through the below procedure.
1684 if ( /^\s+|\s+$/.test( data
) && 'span' in CKEDITOR
.dtd
[ context
] ) {
1685 var protect
= '<span data-cke-marker="1"> </span>';
1686 data
= protect
+ data
+ protect
;
1689 // Process the inserted html, in context of the insertion root.
1690 // Don't use the "fix for body" feature as auto paragraphing must
1691 // be handled during insertion.
1692 data
= that
.editor
.dataProcessor
.toHtml( data
, {
1695 protectedWhitespaces: !!protect
,
1696 dontFilter: that
.dontFilter
,
1697 // Use the current, contextual settings.
1698 filter: that
.editor
.activeFilter
,
1699 enterMode: that
.editor
.activeEnterMode
1703 // Build the node list for insertion.
1704 var doc
= range
.document
,
1705 wrapper
= doc
.createElement( 'body' );
1707 wrapper
.setHtml( data
);
1709 // Eventually remove the temporaries.
1711 wrapper
.getFirst().remove();
1712 wrapper
.getLast().remove();
1716 var block
= range
.startPath().block
;
1717 if ( block
&& // Apply when there exists path block after deleting selection's content...
1718 !( block
.getChildCount() == 1 && block
.getBogus() ) ) { // ... and the only content of this block isn't a bogus.
1719 stripBlockTagIfSingleLine( wrapper
);
1722 that
.dataWrapper
= wrapper
;
1727 function insertDataIntoRange( that
) {
1728 var range
= that
.range
,
1729 doc
= range
.document
,
1731 blockLimit
= that
.blockLimit
,
1732 nodesData
, nodeData
, node
,
1735 bogusNeededBlocks
= [],
1736 pathBlock
, fixBlock
,
1737 splittingContainer
= 0,
1739 insertionContainer
, toSplit
, newContainer
,
1740 startContainer
= range
.startContainer
,
1741 endContainer
= that
.endPath
.elements
[ 0 ],
1743 // If endContainer was merged into startContainer: <p>a[b</p><p>c]d</p>
1744 // or it's equal to startContainer: <p>a^b</p>
1745 // or different situation happened :P
1746 // then there's no separate container for the end of selection.
1747 pos
= endContainer
.getPosition( startContainer
),
1748 separateEndContainer
= !!endContainer
.getCommonAncestor( startContainer
) && // endC is not detached.
1749 pos
!= CKEDITOR
.POSITION_IDENTICAL
&& !( pos
& CKEDITOR
.POSITION_CONTAINS
+ CKEDITOR
.POSITION_IS_CONTAINED
); // endC & endS are in separate branches.
1751 nodesData
= extractNodesData( that
.dataWrapper
, that
);
1753 removeBrsAdjacentToPastedBlocks( nodesData
, range
);
1755 for ( ; nodeIndex
< nodesData
.length
; nodeIndex
++ ) {
1756 nodeData
= nodesData
[ nodeIndex
];
1758 // Ignore trailing <brs>
1759 if ( nodeData
.isLineBreak
&& splitOnLineBreak( range
, blockLimit
, nodeData
) ) {
1760 // Do not move caret towards the text (in cleanupAfterInsertion),
1761 // because caret was placed after a line break.
1762 dontMoveCaret
= nodeIndex
> 0;
1766 path
= range
.startPath();
1768 // Auto paragraphing.
1769 if ( !nodeData
.isBlock
&& shouldAutoParagraph( that
.editor
, path
.block
, path
.blockLimit
) && ( fixBlock
= autoParagraphTag( that
.editor
) ) ) {
1770 fixBlock
= doc
.createElement( fixBlock
);
1771 fixBlock
.appendBogus();
1772 range
.insertNode( fixBlock
);
1773 if ( CKEDITOR
.env
.needsBrFiller
&& ( bogus
= fixBlock
.getBogus() ) )
1775 range
.moveToPosition( fixBlock
, CKEDITOR
.POSITION_BEFORE_END
);
1778 node
= range
.startPath().block
;
1780 // Remove any bogus element on the current path block for now, and mark
1781 // it for later compensation.
1782 if ( node
&& !node
.equals( pathBlock
) ) {
1783 bogus
= node
.getBogus();
1786 bogusNeededBlocks
.push( node
);
1792 // First not allowed node reached - start splitting original container
1793 if ( nodeData
.firstNotAllowed
)
1794 splittingContainer
= 1;
1796 if ( splittingContainer
&& nodeData
.isElement
) {
1797 insertionContainer
= range
.startContainer
;
1800 // Find the first ancestor that can contain current node.
1801 // This one won't be split.
1802 while ( insertionContainer
&& !DTD
[ insertionContainer
.getName() ][ nodeData
.name
] ) {
1803 if ( insertionContainer
.equals( blockLimit
) ) {
1804 insertionContainer
= null;
1808 toSplit
= insertionContainer
;
1809 insertionContainer
= insertionContainer
.getParent();
1812 // If split has to be done - do it and mark both ends as a possible zombies.
1813 if ( insertionContainer
) {
1815 newContainer
= range
.splitElement( toSplit
);
1816 that
.zombies
.push( newContainer
);
1817 that
.zombies
.push( toSplit
);
1820 // Unable to make the insertion happen in place, resort to the content filter.
1822 // If everything worked fine insertionContainer == blockLimit here.
1823 filteredNodes
= filterElement( nodeData
.node
, blockLimit
.getName(), !nodeIndex
, nodeIndex
== nodesData
.length
- 1 );
1827 if ( filteredNodes
) {
1828 while ( ( node
= filteredNodes
.pop() ) )
1829 range
.insertNode( node
);
1832 // Insert current node at the start of range.
1833 range
.insertNode( nodeData
.node
);
1836 // Move range to the endContainer for the final allowed elements.
1837 if ( nodeData
.lastNotAllowed
&& nodeIndex
< nodesData
.length
- 1 ) {
1838 // If separateEndContainer exists move range there.
1839 // Otherwise try to move range to container created during splitting.
1840 // If this doesn't work - don't move range.
1841 newContainer
= separateEndContainer
? endContainer : newContainer
;
1842 newContainer
&& range
.setEndAt( newContainer
, CKEDITOR
.POSITION_AFTER_START
);
1843 splittingContainer
= 0;
1846 // Collapse range after insertion to end.
1850 // Rule 9. Non-editable content should be selected as a whole.
1851 if ( isSingleNonEditableElement( nodesData
) ) {
1852 dontMoveCaret
= true;
1853 node
= nodesData
[ 0 ].node
;
1854 range
.setStartAt( node
, CKEDITOR
.POSITION_BEFORE_START
);
1855 range
.setEndAt( node
, CKEDITOR
.POSITION_AFTER_END
);
1858 that
.dontMoveCaret
= dontMoveCaret
;
1859 that
.bogusNeededBlocks
= bogusNeededBlocks
;
1862 function cleanupAfterInsertion( that
) {
1863 var range
= that
.range
,
1864 node
, testRange
, movedIntoInline
,
1865 bogusNeededBlocks
= that
.bogusNeededBlocks
,
1866 // Create a bookmark to defend against the following range deconstructing operations.
1867 bm
= range
.createBookmark();
1869 // Remove all elements that could be created while splitting nodes
1870 // with ranges at its start|end.
1871 // E.g. remove <div><p></p></div>
1872 // But not <div><p> </p></div>
1873 // And replace <div><p><span data="cke-bookmark"/></p></div> with found bookmark.
1874 while ( ( node
= that
.zombies
.pop() ) ) {
1875 // Detached element.
1876 if ( !node
.getParent() )
1879 testRange
= range
.clone();
1880 testRange
.moveToElementEditStart( node
);
1881 testRange
.removeEmptyBlocksAtEnd();
1884 if ( bogusNeededBlocks
) {
1885 // Bring back all block bogus nodes.
1886 while ( ( node
= bogusNeededBlocks
.pop() ) ) {
1887 if ( CKEDITOR
.env
.needsBrFiller
)
1890 node
.append( range
.document
.createText( '\u00a0' ) );
1894 // Eventually merge identical inline elements.
1895 while ( ( node
= that
.mergeCandidates
.pop() ) )
1896 node
.mergeSiblings();
1898 range
.moveToBookmark( bm
);
1901 // Shrink range to the BEFOREEND of previous innermost editable node in source order.
1903 if ( !that
.dontMoveCaret
) {
1904 node
= getRangePrevious( range
);
1906 while ( node
&& checkIfElement( node
) && !node
.is( DTD
.$empty
) ) {
1907 if ( node
.isBlockBoundary() )
1908 range
.moveToPosition( node
, CKEDITOR
.POSITION_BEFORE_END
);
1910 // Don't move into inline element (which ends with a text node)
1911 // found which contains white-space at its end.
1912 // If not - move range's end to the end of this element.
1913 if ( isInline( node
) && node
.getHtml().match( /(\s| )$/g ) ) {
1914 movedIntoInline
= null;
1918 movedIntoInline
= range
.clone();
1919 movedIntoInline
.moveToPosition( node
, CKEDITOR
.POSITION_BEFORE_END
);
1922 node
= node
.getLast( isNotEmpty
);
1925 movedIntoInline
&& range
.moveToRange( movedIntoInline
);
1931 // HELPERS ------------------------------------------------------------
1934 function checkIfElement( node
) {
1935 return node
.type
== CKEDITOR
.NODE_ELEMENT
;
1938 function extractNodesData( dataWrapper
, that
) {
1939 var node
, sibling
, nodeName
, allowed
,
1941 startContainer
= that
.range
.startContainer
,
1942 path
= that
.range
.startPath(),
1943 allowedNames
= DTD
[ startContainer
.getName() ],
1945 nodesList
= dataWrapper
.getChildren(),
1946 nodesCount
= nodesList
.count(),
1947 firstNotAllowed
= -1,
1948 lastNotAllowed
= -1,
1952 // Selection start within a list.
1953 var insideOfList
= path
.contains( DTD
.$list
);
1955 for ( ; nodeIndex
< nodesCount
; ++nodeIndex
) {
1956 node
= nodesList
.getItem( nodeIndex
);
1958 if ( checkIfElement( node
) ) {
1959 nodeName
= node
.getName();
1961 // Extract only the list items, when insertion happens
1962 // inside of a list, reads as rearrange list items. (#7957)
1963 if ( insideOfList
&& nodeName
in CKEDITOR
.dtd
.$list
) {
1964 nodesData
= nodesData
.concat( extractNodesData( node
, that
) );
1968 allowed
= !!allowedNames
[ nodeName
];
1970 // Mark <brs data-cke-eol="1"> at the beginning and at the end.
1971 if ( nodeName
== 'br' && node
.data( 'cke-eol' ) && ( !nodeIndex
|| nodeIndex
== nodesCount
- 1 ) ) {
1972 sibling
= nodeIndex
? nodesData
[ nodeIndex
- 1 ].node : nodesList
.getItem( nodeIndex
+ 1 );
1974 // Line break has to have sibling which is not an <br>.
1975 lineBreak
= sibling
&& ( !checkIfElement( sibling
) || !sibling
.is( 'br' ) );
1976 // Line break has block element as a sibling.
1977 blockSibling
= sibling
&& checkIfElement( sibling
) && DTD
.$block
[ sibling
.getName() ];
1980 if ( firstNotAllowed
== -1 && !allowed
)
1981 firstNotAllowed
= nodeIndex
;
1983 lastNotAllowed
= nodeIndex
;
1987 isLineBreak: lineBreak
,
1988 isBlock: node
.isBlockBoundary(),
1989 hasBlockSibling: blockSibling
,
1998 nodesData
.push( { isElement: 0, node: node
, allowed: 1 } );
2002 // Mark first node that cannot be inserted directly into startContainer
2003 // and last node for which startContainer has to be split.
2004 if ( firstNotAllowed
> -1 )
2005 nodesData
[ firstNotAllowed
].firstNotAllowed
= 1;
2006 if ( lastNotAllowed
> -1 )
2007 nodesData
[ lastNotAllowed
].lastNotAllowed
= 1;
2012 // TODO: Review content transformation rules on filtering element.
2013 function filterElement( element
, parentName
, isFirst
, isLast
) {
2014 var nodes
= filterElementInner( element
, parentName
),
2016 nodesCount
= nodes
.length
,
2020 lastSpaceIndex
= -1;
2022 // Remove duplicated spaces and spaces at the:
2023 // * beginnig if filtered element isFirst (isFirst that's going to be inserted)
2024 // * end if filtered element isLast.
2025 for ( ; nodeIndex
< nodesCount
; nodeIndex
++ ) {
2026 node
= nodes
[ nodeIndex
];
2028 if ( node
== ' ' ) {
2029 // Don't push doubled space and if it's leading space for insertion.
2030 if ( !afterSpace
&& !( isFirst
&& !nodeIndex
) ) {
2031 nodes2
.push( new CKEDITOR
.dom
.text( ' ' ) );
2032 lastSpaceIndex
= nodes2
.length
;
2036 nodes2
.push( node
);
2041 // Remove trailing space.
2042 if ( isLast
&& lastSpaceIndex
== nodes2
.length
)
2048 function filterElementInner( element
, parentName
) {
2050 children
= element
.getChildren(),
2051 childrenCount
= children
.count(),
2054 allowedNames
= DTD
[ parentName
],
2055 surroundBySpaces
= !element
.is( DTD
.$inline
) || element
.is( 'br' );
2057 if ( surroundBySpaces
)
2060 for ( ; childIndex
< childrenCount
; childIndex
++ ) {
2061 child
= children
.getItem( childIndex
);
2063 if ( checkIfElement( child
) && !child
.is( allowedNames
) )
2064 nodes
= nodes
.concat( filterElementInner( child
, parentName
) );
2066 nodes
.push( child
);
2069 if ( surroundBySpaces
)
2075 function getRangePrevious( range
) {
2076 return checkIfElement( range
.startContainer
) && range
.startContainer
.getChild( range
.startOffset
- 1 );
2079 function isInline( node
) {
2080 return node
&& checkIfElement( node
) && ( node
.is( DTD
.$removeEmpty
) || node
.is( 'a' ) && !node
.isBlockBoundary() );
2083 // Checks if only non-editable element is being inserted.
2084 function isSingleNonEditableElement( nodesData
) {
2085 if ( nodesData
.length
!= 1 )
2088 var nodeData
= nodesData
[ 0 ];
2090 return nodeData
.isElement
&& ( nodeData
.node
.getAttribute( 'contenteditable' ) == 'false' );
2093 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 };
2095 // See rule 5. in TCs.
2096 // Initial situation:
2097 // <ul><li>AA^</li></ul><ul><li>BB</li></ul>
2098 // We're looking for 2nd <ul>, comparing with 1st <ul> and merging.
2099 // We're not merging if caret is between these elements.
2100 function mergeAncestorElementsOfSelectionEnds( range
, blockLimit
, startPath
, endPath
) {
2101 var walkerRange
= range
.clone(),
2102 walker
, nextNode
, previousNode
;
2104 walkerRange
.setEndAt( blockLimit
, CKEDITOR
.POSITION_BEFORE_END
);
2105 walker
= new CKEDITOR
.dom
.walker( walkerRange
);
2107 if ( ( nextNode
= walker
.next() ) && // Find next source node
2108 checkIfElement( nextNode
) && // which is an element
2109 blockMergedTags
[ nextNode
.getName() ] && // that can be merged.
2110 ( previousNode
= nextNode
.getPrevious() ) && // Take previous one
2111 checkIfElement( previousNode
) && // which also has to be an element.
2112 !previousNode
.getParent().equals( range
.startContainer
) && // Fail if caret is on the same level.
2113 // This means that caret is between these nodes.
2114 startPath
.contains( previousNode
) && // Elements path of start of selection has
2115 endPath
.contains( nextNode
) && // to contain prevNode and vice versa.
2116 nextNode
.isIdentical( previousNode
) // Check if elements are identical.
2118 // Merge blocks and repeat.
2119 nextNode
.moveChildren( previousNode
);
2121 mergeAncestorElementsOfSelectionEnds( range
, blockLimit
, startPath
, endPath
);
2125 // If last node that will be inserted is a block (but not a <br>)
2126 // and it will be inserted right before <br> remove this <br>.
2127 // Do the same for the first element that will be inserted and preceding <br>.
2128 function removeBrsAdjacentToPastedBlocks( nodesData
, range
) {
2129 var succeedingNode
= range
.endContainer
.getChild( range
.endOffset
),
2130 precedingNode
= range
.endContainer
.getChild( range
.endOffset
- 1 );
2132 if ( succeedingNode
)
2133 remove( succeedingNode
, nodesData
[ nodesData
.length
- 1 ] );
2135 if ( precedingNode
&& remove( precedingNode
, nodesData
[ 0 ] ) ) {
2136 // If preceding <br> was removed - move range left.
2137 range
.setEnd( range
.endContainer
, range
.endOffset
- 1 );
2141 function remove( maybeBr
, maybeBlockData
) {
2142 if ( maybeBlockData
.isBlock
&& maybeBlockData
.isElement
&& !maybeBlockData
.node
.is( 'br' ) &&
2143 checkIfElement( maybeBr
) && maybeBr
.is( 'br' ) ) {
2150 // Return 1 if <br> should be skipped when inserting, 0 otherwise.
2151 function splitOnLineBreak( range
, blockLimit
, nodeData
) {
2152 var firstBlockAscendant
, pos
;
2154 if ( nodeData
.hasBlockSibling
)
2157 firstBlockAscendant
= range
.startContainer
.getAscendant( DTD
.$block
, 1 );
2158 if ( !firstBlockAscendant
|| !firstBlockAscendant
.is( { div: 1, p: 1 } ) )
2161 pos
= firstBlockAscendant
.getPosition( blockLimit
);
2163 if ( pos
== CKEDITOR
.POSITION_IDENTICAL
|| pos
== CKEDITOR
.POSITION_CONTAINS
)
2166 var newContainer
= range
.splitElement( firstBlockAscendant
);
2167 range
.moveToPosition( newContainer
, CKEDITOR
.POSITION_AFTER_START
);
2172 var stripSingleBlockTags
= { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1 },
2173 inlineButNotBr
= CKEDITOR
.tools
.extend( {}, DTD
.$inline
);
2174 delete inlineButNotBr
.br
;
2177 function stripBlockTagIfSingleLine( dataWrapper
) {
2178 var block
, children
;
2180 if ( dataWrapper
.getChildCount() == 1 && // Only one node bein inserted.
2181 checkIfElement( block
= dataWrapper
.getFirst() ) && // And it's an element.
2182 block
.is( stripSingleBlockTags
) && // That's <p> or <div> or header.
2183 !block
.hasAttribute( 'contenteditable' ) // It's not a non-editable block or nested editable.
2185 // Check children not containing block.
2186 children
= block
.getElementsByTag( '*' );
2187 for ( var i
= 0, child
, count
= children
.count(); i
< count
; i
++ ) {
2188 child
= children
.getItem( i
);
2189 if ( !child
.is( inlineButNotBr
) )
2193 block
.moveChildren( block
.getParent( 1 ) );
2198 function wrapDataWithInlineStyles( data
, that
) {
2199 var element
= that
.inlineStylesPeak
,
2200 doc
= element
.getDocument(),
2201 wrapper
= doc
.createText( '{cke-peak}' ),
2202 limit
= that
.inlineStylesRoot
.getParent();
2204 while ( !element
.equals( limit
) ) {
2205 wrapper
= wrapper
.appendTo( element
.clone() );
2206 element
= element
.getParent();
2209 // Don't use String.replace because it fails in IE7 if special replacement
2210 // characters ($$, $&, etc.) are in data (#10367).
2211 return wrapper
.getOuterHtml().split( '{cke-peak}' ).join( data
);
2217 function afterInsert( editable
) {
2218 var editor
= editable
.editor
;
2220 // Scroll using selection, not ranges, to affect native pastes.
2221 editor
.getSelection().scrollIntoView();
2223 // Save snaps after the whole execution completed.
2224 // This's a workaround for make DOM modification's happened after
2225 // 'insertElement' to be included either, e.g. Form-based dialogs' 'commitContents'
2227 setTimeout( function() {
2228 editor
.fire( 'saveSnapshot' );
2232 // 1. Fixes a range which is a result of deleteContents() and is placed in an intermediate element (see dtd.$intermediate),
2233 // inside a table. A goal is to find a closest <td> or <th> element and when this fails, recreate the structure of the table.
2234 // 2. Fixes empty cells by appending bogus <br>s or deleting empty text nodes in IE<=8 case.
2235 var fixTableAfterContentsDeletion
= ( function() {
2236 // Creates an element walker which can only "go deeper". It won't
2237 // move out from any element. Therefore it can be used to find <td>x</td> in cases like:
2238 // <table><tbody><tr><td>x</td></tr></tbody>^<tfoot>...
2239 function getFixTableSelectionWalker( testRange
) {
2240 var walker
= new CKEDITOR
.dom
.walker( testRange
);
2241 walker
.guard = function( node
, isMovingOut
) {
2244 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
)
2245 return node
.is( CKEDITOR
.dtd
.$tableContent
);
2247 walker
.evaluator = function( node
) {
2248 return node
.type
== CKEDITOR
.NODE_ELEMENT
;
2254 function fixTableStructure( element
, newElementName
, appendToStart
) {
2255 var temp
= element
.getDocument().createElement( newElementName
);
2256 element
.append( temp
, appendToStart
);
2260 // Fix empty cells. This means:
2261 // * add bogus <br> if browser needs it
2262 // * remove empty text nodes on IE8, because it will crash (http://dev.ckeditor.com/ticket/11183#comment:8).
2263 function fixEmptyCells( cells
) {
2264 var i
= cells
.count(),
2267 for ( i
; i
-- > 0; ) {
2268 cell
= cells
.getItem( i
);
2270 if ( !CKEDITOR
.tools
.trim( cell
.getHtml() ) ) {
2272 if ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 9 && cell
.getChildCount() )
2273 cell
.getFirst().remove();
2278 return function( range
) {
2279 var container
= range
.startContainer
,
2280 table
= container
.getAscendant( 'table', 1 ),
2283 appendToStart
= false;
2285 fixEmptyCells( table
.getElementsByTag( 'td' ) );
2286 fixEmptyCells( table
.getElementsByTag( 'th' ) );
2289 testRange
= range
.clone();
2290 testRange
.setStart( container
, 0 );
2291 deeperSibling
= getFixTableSelectionWalker( testRange
).lastBackward();
2293 // If left is empty, look right.
2294 if ( !deeperSibling
) {
2295 testRange
= range
.clone();
2296 testRange
.setEndAt( container
, CKEDITOR
.POSITION_BEFORE_END
);
2297 deeperSibling
= getFixTableSelectionWalker( testRange
).lastForward();
2298 appendToStart
= true;
2301 // If there's no deeper nested element in both direction - container is empty - we'll use it then.
2302 if ( !deeperSibling
)
2303 deeperSibling
= container
;
2307 // We found a table what means that it's empty - remove it completely.
2308 if ( deeperSibling
.is( 'table' ) ) {
2309 range
.setStartAt( deeperSibling
, CKEDITOR
.POSITION_BEFORE_START
);
2310 range
.collapse( true );
2311 deeperSibling
.remove();
2315 // Found an empty txxx element - append tr.
2316 if ( deeperSibling
.is( { tbody: 1, thead: 1, tfoot: 1 } ) )
2317 deeperSibling
= fixTableStructure( deeperSibling
, 'tr', appendToStart
);
2319 // Found an empty tr element - append td/th.
2320 if ( deeperSibling
.is( 'tr' ) )
2321 deeperSibling
= fixTableStructure( deeperSibling
, deeperSibling
.getParent().is( 'thead' ) ? 'th' : 'td', appendToStart
);
2323 // To avoid setting selection after bogus, remove it from the current cell.
2324 // We can safely do that, because we'll insert element into that cell.
2325 var bogus
= deeperSibling
.getBogus();
2329 range
.moveToPosition( deeperSibling
, appendToStart
? CKEDITOR
.POSITION_AFTER_START : CKEDITOR
.POSITION_BEFORE_END
);
2333 function mergeBlocksCollapsedSelection( editor
, range
, backspace
, startPath
) {
2334 var startBlock
= startPath
.block
;
2336 // Selection must be collapsed and to be anchored in a block.
2340 // Exclude cases where, i.e. if pressed arrow key, selection
2341 // would move within the same block (merge inside a block).
2342 if ( !range
[ backspace
? 'checkStartOfBlock' : 'checkEndOfBlock' ]() )
2345 // Make sure, there's an editable position to put selection,
2346 // which i.e. would be used if pressed arrow key, but abort
2347 // if such position exists but means a selected non-editable element.
2348 if ( !range
.moveToClosestEditablePosition( startBlock
, !backspace
) || !range
.collapsed
)
2351 // Handle special case, when block's sibling is a <hr>. Delete it and keep selection
2352 // in the same place (http://dev.ckeditor.com/ticket/11861#comment:9).
2353 if ( range
.startContainer
.type
== CKEDITOR
.NODE_ELEMENT
) {
2354 var touched
= range
.startContainer
.getChild( range
.startOffset
- ( backspace
? 1 : 0 ) );
2355 if ( touched
&& touched
.type
== CKEDITOR
.NODE_ELEMENT
&& touched
.is( 'hr' ) ) {
2356 editor
.fire( 'saveSnapshot' );
2362 var siblingBlock
= range
.startPath().block
;
2364 // Abort if an editable position exists, but either it's not
2365 // in a block or that block is the parent of the start block
2366 // (merging child into parent).
2367 if ( !siblingBlock
|| ( siblingBlock
&& siblingBlock
.contains( startBlock
) ) )
2370 editor
.fire( 'saveSnapshot' );
2372 // Remove bogus to avoid duplicated boguses.
2374 if ( ( bogus
= ( backspace
? siblingBlock : startBlock
).getBogus() ) )
2377 // Save selection. It will be restored.
2378 var selection
= editor
.getSelection(),
2379 bookmarks
= selection
.createBookmarks();
2382 ( backspace
? startBlock : siblingBlock
).moveChildren( backspace
? siblingBlock : startBlock
, false );
2384 // Also merge children along with parents.
2385 startPath
.lastElement
.mergeSiblings();
2387 // Cut off removable branch of the DOM tree.
2388 pruneEmptyDisjointAncestors( startBlock
, siblingBlock
, !backspace
);
2390 // Restore selection.
2391 selection
.selectBookmarks( bookmarks
);
2396 function mergeBlocksNonCollapsedSelection( editor
, range
, startPath
) {
2397 var startBlock
= startPath
.block
,
2398 endPath
= range
.endPath(),
2399 endBlock
= endPath
.block
;
2401 // Selection must be anchored in two different blocks.
2402 if ( !startBlock
|| !endBlock
|| startBlock
.equals( endBlock
) )
2405 editor
.fire( 'saveSnapshot' );
2407 // Remove bogus to avoid duplicated boguses.
2409 if ( ( bogus
= startBlock
.getBogus() ) )
2412 // Changing end container to element from text node (#12503).
2413 range
.enlarge( CKEDITOR
.ENLARGE_INLINE
);
2415 // Delete range contents. Do NOT merge. Merging is weird.
2416 range
.deleteContents();
2418 // If something has left of the block to be merged, clean it up.
2419 // It may happen when merging with list items.
2420 if ( endBlock
.getParent() ) {
2421 // Move children to the first block.
2422 endBlock
.moveChildren( startBlock
, false );
2424 // ...and merge them if that's possible.
2425 startPath
.lastElement
.mergeSiblings();
2427 // If expanded selection, things are always merged like with BACKSPACE.
2428 pruneEmptyDisjointAncestors( startBlock
, endBlock
, true );
2431 // Make sure the result selection is collapsed.
2432 range
= editor
.getSelection().getRanges()[ 0 ];
2433 range
.collapse( 1 );
2435 // Optimizing range containers from text nodes to elements (#12503).
2437 if ( range
.startContainer
.getHtml() === '' ) {
2438 range
.startContainer
.appendBogus();
2446 // Finds the innermost child of common parent, which,
2447 // if removed, removes nothing but the contents of the element.
2449 // before: <div><p><strong>first</strong></p><p>second</p></div>
2450 // after: <div><p>second</p></div>
2452 // before: <div><p>x<strong>first</strong></p><p>second</p></div>
2453 // after: <div><p>x</p><p>second</p></div>
2455 // isPruneToEnd=true
2456 // before: <div><p><strong>first</strong></p><p>second</p></div>
2457 // after: <div><p><strong>first</strong></p></div>
2459 // @param {CKEDITOR.dom.element} first
2460 // @param {CKEDITOR.dom.element} second
2461 // @param {Boolean} isPruneToEnd
2462 function pruneEmptyDisjointAncestors( first
, second
, isPruneToEnd
) {
2463 var commonParent
= first
.getCommonAncestor( second
),
2464 node
= isPruneToEnd
? second : first
,
2465 removableParent
= node
;
2467 while ( ( node
= node
.getParent() ) && !commonParent
.equals( node
) && node
.getChildCount() == 1 )
2468 removableParent
= node
;
2470 removableParent
.remove();
2474 // Helpers for editable.getHtmlFromRange.
2476 var getHtmlFromRangeHelpers
= {
2478 detect: function( that
, editable
) {
2479 var range
= that
.range
,
2480 rangeStart
= range
.clone(),
2481 rangeEnd
= range
.clone(),
2483 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, editable
),
2484 endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
, editable
);
2486 // Note: checkBoundaryOfElement will not work on original range as CKEDITOR.START|END
2487 // means that range start|end must be literally anchored at block start|end, e.g.
2489 // <p>a{</p><p>}b</p>
2491 // will return false for both paragraphs but two similar ranges
2493 // <p>a{}</p><p>{}b</p>
2495 // will return true if checked separately.
2496 rangeStart
.collapse( 1 );
2497 rangeEnd
.collapse();
2499 if ( startPath
.block
&& rangeStart
.checkBoundaryOfElement( startPath
.block
, CKEDITOR
.END
) ) {
2500 range
.setStartAfter( startPath
.block
);
2501 that
.prependEolBr
= 1;
2504 if ( endPath
.block
&& rangeEnd
.checkBoundaryOfElement( endPath
.block
, CKEDITOR
.START
) ) {
2505 range
.setEndBefore( endPath
.block
);
2506 that
.appendEolBr
= 1;
2510 fix: function( that
, editable
) {
2511 var doc
= editable
.getDocument(),
2514 // Append <br data-cke-eol="1"> to the fragment.
2515 if ( that
.appendEolBr
) {
2516 appended
= this.createEolBr( doc
);
2517 that
.fragment
.append( appended
);
2520 // Prepend <br data-cke-eol="1"> to the fragment but avoid duplicates. Such
2521 // elements should never follow each other in DOM.
2522 if ( that
.prependEolBr
&& ( !appended
|| appended
.getPrevious() ) ) {
2523 that
.fragment
.append( this.createEolBr( doc
), 1 );
2527 createEolBr: function( doc
) {
2528 return doc
.createElement( 'br', {
2537 exclude: function( that
) {
2538 var boundaryNodes
= that
.range
.getBoundaryNodes(),
2539 startNode
= boundaryNodes
.startNode
,
2540 endNode
= boundaryNodes
.endNode
;
2542 // If bogus is the last node in range but not the only node, exclude it.
2543 if ( endNode
&& isBogus( endNode
) && ( !startNode
|| !startNode
.equals( endNode
) ) )
2544 that
.range
.setEndBefore( endNode
);
2549 rebuild: function( that
, editable
) {
2550 var range
= that
.range
,
2551 node
= range
.getCommonAncestor(),
2553 // A path relative to the common ancestor.
2554 commonPath
= new CKEDITOR
.dom
.elementPath( node
, editable
),
2555 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, editable
),
2556 endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
, editable
),
2559 if ( node
.type
== CKEDITOR
.NODE_TEXT
)
2560 node
= node
.getParent();
2562 // Fix DOM of partially enclosed tables
2563 // <table><tbody><tr><td>a{b</td><td>c}d</td></tr></tbody></table>
2564 // Full table is returned
2565 // <table><tbody><tr><td>b</td><td>c</td></tr></tbody></table>
2567 // <td>b</td><td>c</td>
2568 if ( commonPath
.blockLimit
.is( { tr: 1, table: 1 } ) ) {
2569 var tableParent
= commonPath
.contains( 'table' ).getParent();
2571 limit = function( node
) {
2572 return !node
.equals( tableParent
);
2576 // Fix DOM in the following case
2577 // <ol><li>a{b<ul><li>c}d</li></ul></li></ol>
2578 // Full list is returned
2579 // <ol><li>b<ul><li>c</li></ul></li></ol>
2581 // b<ul><li>c</li></ul>
2582 else if ( commonPath
.block
&& commonPath
.block
.is( CKEDITOR
.dtd
.$listItem
) ) {
2583 var startList
= startPath
.contains( CKEDITOR
.dtd
.$list
),
2584 endList
= endPath
.contains( CKEDITOR
.dtd
.$list
);
2586 if ( !startList
.equals( endList
) ) {
2587 var listParent
= commonPath
.contains( CKEDITOR
.dtd
.$list
).getParent();
2589 limit = function( node
) {
2590 return !node
.equals( listParent
);
2595 // If not defined, use generic limit function.
2597 limit = function( node
) {
2598 return !node
.equals( commonPath
.block
) && !node
.equals( commonPath
.blockLimit
);
2602 this.rebuildFragment( that
, editable
, node
, limit
);
2605 rebuildFragment: function( that
, editable
, node
, checkLimit
) {
2608 while ( node
&& !node
.equals( editable
) && checkLimit( node
) ) {
2609 // Don't clone children. Preserve element ids.
2610 clone
= node
.clone( 0, 1 );
2611 that
.fragment
.appendTo( clone
);
2612 that
.fragment
= clone
;
2614 node
= node
.getParent();
2620 // Handle range anchored in table row with a single cell enclosed:
2621 // <table><tbody><tr>[<td>a</td>]</tr></tbody></table>
2623 // <table><tbody><tr><td>{a}</td></tr></tbody></table>
2624 shrink: function( that
) {
2625 var range
= that
.range
,
2626 startContainer
= range
.startContainer
,
2627 endContainer
= range
.endContainer
,
2628 startOffset
= range
.startOffset
,
2629 endOffset
= range
.endOffset
;
2631 if ( startContainer
.type
== CKEDITOR
.NODE_ELEMENT
&& startContainer
.equals( endContainer
) && startContainer
.is( 'tr' ) && ++startOffset
== endOffset
) {
2632 range
.shrink( CKEDITOR
.SHRINK_TEXT
);
2639 // Helpers for editable.extractHtmlFromRange.
2641 var extractHtmlFromRangeHelpers
= ( function() {
2642 function optimizeBookmarkNode( node
, toStart
) {
2643 var parent
= node
.getParent();
2645 if ( parent
.is( CKEDITOR
.dtd
.$inline
) )
2646 node
[ toStart
? 'insertBefore' : 'insertAfter' ]( parent
);
2649 function mergeElements( merged
, startBookmark
, endBookmark
) {
2650 optimizeBookmarkNode( startBookmark
);
2651 optimizeBookmarkNode( endBookmark
, 1 );
2654 while ( ( next
= endBookmark
.getNext() ) ) {
2655 next
.insertAfter( startBookmark
);
2657 // Update startBookmark after insertion to avoid the reversal of nodes (#13449).
2658 startBookmark
= next
;
2661 if ( isEmpty( merged
) )
2665 function getPath( startElement
, root
) {
2666 return new CKEDITOR
.dom
.elementPath( startElement
, root
);
2669 // Creates a range from a bookmark without removing the bookmark.
2670 function createRangeFromBookmark( root
, bookmark
) {
2671 var range
= new CKEDITOR
.dom
.range( root
);
2672 range
.setStartAfter( bookmark
.startNode
);
2673 range
.setEndBefore( bookmark
.endNode
);
2678 detectMerge: function( that
, editable
) {
2679 var range
= createRangeFromBookmark( editable
, that
.bookmark
),
2680 startPath
= range
.startPath(),
2681 endPath
= range
.endPath(),
2683 startList
= startPath
.contains( CKEDITOR
.dtd
.$list
),
2684 endList
= endPath
.contains( CKEDITOR
.dtd
.$list
);
2687 // Both lists must exist
2688 startList
&& endList
&&
2689 // ...and be of the same type
2690 // startList.getName() == endList.getName() &&
2691 // ...and share the same parent (same level in the tree)
2692 startList
.getParent().equals( endList
.getParent() ) &&
2693 // ...and must be different.
2694 !startList
.equals( endList
);
2696 that
.mergeListItems
=
2697 startPath
.block
&& endPath
.block
&&
2698 // Both containers must be list items
2699 startPath
.block
.is( CKEDITOR
.dtd
.$listItem
) && endPath
.block
.is( CKEDITOR
.dtd
.$listItem
);
2701 // Create merge bookmark.
2702 if ( that
.mergeList
|| that
.mergeListItems
) {
2703 var rangeClone
= range
.clone();
2705 rangeClone
.setStartBefore( that
.bookmark
.startNode
);
2706 rangeClone
.setEndAfter( that
.bookmark
.endNode
);
2708 that
.mergeListBookmark
= rangeClone
.createBookmark();
2712 merge: function( that
, editable
) {
2713 if ( !that
.mergeListBookmark
)
2716 var startNode
= that
.mergeListBookmark
.startNode
,
2717 endNode
= that
.mergeListBookmark
.endNode
,
2719 startPath
= getPath( startNode
, editable
),
2720 endPath
= getPath( endNode
, editable
);
2722 if ( that
.mergeList
) {
2723 var firstList
= startPath
.contains( CKEDITOR
.dtd
.$list
),
2724 secondList
= endPath
.contains( CKEDITOR
.dtd
.$list
);
2726 if ( !firstList
.equals( secondList
) ) {
2727 secondList
.moveChildren( firstList
);
2728 secondList
.remove();
2732 if ( that
.mergeListItems
) {
2733 var firstListItem
= startPath
.contains( CKEDITOR
.dtd
.$listItem
),
2734 secondListItem
= endPath
.contains( CKEDITOR
.dtd
.$listItem
);
2736 if ( !firstListItem
.equals( secondListItem
) ) {
2737 mergeElements( secondListItem
, startNode
, endNode
);
2741 // Remove bookmark nodes.
2748 // Detects whether blocks should be merged once contents are extracted.
2749 detectMerge: function( that
, editable
) {
2750 // Don't merge blocks if lists or tables are already involved.
2751 if ( that
.tableContentsRanges
|| that
.mergeListBookmark
)
2754 var rangeClone
= new CKEDITOR
.dom
.range( editable
);
2756 rangeClone
.setStartBefore( that
.bookmark
.startNode
);
2757 rangeClone
.setEndAfter( that
.bookmark
.endNode
);
2759 that
.mergeBlockBookmark
= rangeClone
.createBookmark();
2762 merge: function( that
, editable
) {
2763 if ( !that
.mergeBlockBookmark
|| that
.purgeTableBookmark
)
2766 var startNode
= that
.mergeBlockBookmark
.startNode
,
2767 endNode
= that
.mergeBlockBookmark
.endNode
,
2769 startPath
= getPath( startNode
, editable
),
2770 endPath
= getPath( endNode
, editable
),
2772 firstBlock
= startPath
.block
,
2773 secondBlock
= endPath
.block
;
2775 if ( firstBlock
&& secondBlock
&& !firstBlock
.equals( secondBlock
) ) {
2776 mergeElements( secondBlock
, startNode
, endNode
);
2779 // Remove bookmark nodes.
2785 var table
= ( function() {
2786 var tableEditable
= { td: 1, th: 1, caption: 1 };
2788 // Returns an array of ranges which should be entirely extracted.
2790 // <table><tr>[<td>xx</td><td>y}y</td></tr></table>
2792 // <table><tr><td>[xx]</td><td>[y}y</td></tr></table>
2793 function findTableContentsRanges( range
) {
2794 // Leaving the below for debugging purposes.
2796 // console.log( 'findTableContentsRanges' );
2797 // console.log( bender.tools.range.getWithHtml( range.root, range ) );
2799 var contentsRanges
= [],
2801 walker
= new CKEDITOR
.dom
.walker( range
),
2802 startCell
= range
.startPath().contains( tableEditable
),
2803 endCell
= range
.endPath().contains( tableEditable
),
2806 walker
.guard = function( node
, leaving
) {
2807 // Guard may be executed on some node boundaries multiple times,
2808 // what results in creating more than one range for each selected cell. (#12964)
2809 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
) {
2810 var key
= 'visited_' + ( leaving
? 'out' : 'in' );
2811 if ( node
.getCustomData( key
) ) {
2815 CKEDITOR
.dom
.element
.setMarker( database
, node
, key
, 1 );
2818 // Handle partial selection in a cell in which the range starts:
2819 // <td><p>x{xx</p></td>...
2821 // <td><p>x{xx</p>]</td>
2822 if ( leaving
&& startCell
&& node
.equals( startCell
) ) {
2823 editableRange
= range
.clone();
2824 editableRange
.setEndAt( startCell
, CKEDITOR
.POSITION_BEFORE_END
);
2825 contentsRanges
.push( editableRange
);
2829 // Handle partial selection in a cell in which the range ends.
2830 if ( !leaving
&& endCell
&& node
.equals( endCell
) ) {
2831 editableRange
= range
.clone();
2832 editableRange
.setStartAt( endCell
, CKEDITOR
.POSITION_AFTER_START
);
2833 contentsRanges
.push( editableRange
);
2837 // Handle all other cells visited by the walker.
2838 // We need to check whether the cell is disjoint with
2839 // the start and end cells to correctly handle case like:
2840 // <td>x{x</td><td><table>..<td>y}y</td>..</table></td>
2841 // without the check the second cell's content would be entirely removed.
2842 if ( !leaving
&& checkRemoveCellContents( node
) ) {
2843 editableRange
= range
.clone();
2844 editableRange
.selectNodeContents( node
);
2845 contentsRanges
.push( editableRange
);
2849 walker
.lastForward();
2851 // Clear all markers so next extraction will not be affected by this one.
2852 CKEDITOR
.dom
.element
.clearAllMarkers( database
);
2854 return contentsRanges
;
2856 function checkRemoveCellContents( node
) {
2859 node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( tableEditable
) &&
2860 // Must be disjoint with the range's startCell if exists.
2861 ( !startCell
|| checkDisjointNodes( node
, startCell
) ) &&
2862 // Must be disjoint with the range's endCell if exists.
2863 ( !endCell
|| checkDisjointNodes( node
, endCell
) )
2868 // Returns a normalized common ancestor of a range.
2869 // If the real common ancestor is located somewhere in between a table and a td/th/caption,
2870 // then the table will be returned.
2871 function getNormalizedAncestor( range
) {
2872 var common
= range
.getCommonAncestor();
2874 if ( common
.is( CKEDITOR
.dtd
.$tableContent
) && !common
.is( tableEditable
) ) {
2875 common
= common
.getAscendant( 'table', true );
2881 // Check whether node1 and node2 are disjoint, so are:
2883 // * not contained in each other.
2884 function checkDisjointNodes( node1
, node2
) {
2885 var disallowedPositions
= CKEDITOR
.POSITION_CONTAINS
+ CKEDITOR
.POSITION_IS_CONTAINED
,
2886 pos
= node1
.getPosition( node2
);
2888 // Baaah... IDENTICAL is 0, so we can't simplify this ;/.
2889 return pos
=== CKEDITOR
.POSITION_IDENTICAL
?
2891 ( ( pos
& disallowedPositions
) === 0 );
2895 // Detects whether to purge entire list.
2896 detectPurge: function( that
) {
2897 var range
= that
.range
,
2898 walkerRange
= range
.clone();
2900 walkerRange
.enlarge( CKEDITOR
.ENLARGE_ELEMENT
);
2902 var walker
= new CKEDITOR
.dom
.walker( walkerRange
),
2905 // Count the number of table editables in the range. If there's more than one,
2906 // table MAY be removed completely (it's a cross-cell range). Otherwise, only
2907 // the contents of the cell are usually removed.
2908 walker
.evaluator = function( node
) {
2909 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( tableEditable
) ) {
2914 walker
.checkForward();
2916 if ( editablesCount
> 1 ) {
2917 var startTable
= range
.startPath().contains( 'table' ),
2918 endTable
= range
.endPath().contains( 'table' );
2920 if ( startTable
&& endTable
&& range
.checkBoundaryOfElement( startTable
, CKEDITOR
.START
) && range
.checkBoundaryOfElement( endTable
, CKEDITOR
.END
) ) {
2921 var rangeClone
= that
.range
.clone();
2923 rangeClone
.setStartBefore( startTable
);
2924 rangeClone
.setEndAfter( endTable
);
2926 that
.purgeTableBookmark
= rangeClone
.createBookmark();
2933 // This method tries to discover whether the range starts or ends somewhere in a table
2934 // (it is not interested whether the range contains a table, because in such case
2935 // the extractContents() methods does the job correctly).
2936 // If the range meets these criteria, then the method tries to discover and store the following:
2938 // * that.tableSurroundingRange - a part of the range which is located outside of any table which
2939 // will be touched (note: when range is located in a single cell it does not touch the table).
2940 // This range can be placed at:
2941 // * at the beginning: <p>he{re</p><table>..]..</table>
2942 // * in the middle: <table>..[..</table><p>here</p><table>..]..</table>
2943 // * at the end: <table>..[..</table><p>he}re</p>
2944 // * that.tableContentsRanges - an array of ranges with contents of td/th/caption that should be removed.
2945 // This assures that calling extractContents() does not change the structure of the table(s).
2946 detectRanges: function( that
, editable
) {
2947 var range
= createRangeFromBookmark( editable
, that
.bookmark
),
2948 surroundingRange
= range
.clone(),
2952 // Find a common ancestor and normalize it (so the following paths contain tables).
2953 commonAncestor
= getNormalizedAncestor( range
),
2955 // Create paths using the normalized ancestor, so tables beyond the context
2956 // of the input range are not found.
2957 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, commonAncestor
),
2958 endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
, commonAncestor
),
2960 startTable
= startPath
.contains( 'table' ),
2961 endTable
= endPath
.contains( 'table' ),
2963 tableContentsRanges
;
2965 // Nothing to do here - the range doesn't touch any table or
2966 // it contains a table, but that table is fully selected so it will be simply fully removed
2967 // by the normal algorithm.
2968 if ( !startTable
&& !endTable
) {
2972 // Handle two disjoint tables case:
2973 // <table>..[..</table><p>ab</p><table>..]..</table>
2974 // is handled as (respectively: findTableContents( left ), surroundingRange, findTableContents( right )):
2975 // <table>..[..</table>][<p>ab</p>][<table>..]..</table>
2976 // Check that tables are disjoint to exclude a case when start equals end or one is contained
2978 if ( startTable
&& endTable
&& checkDisjointNodes( startTable
, endTable
) ) {
2979 that
.tableSurroundingRange
= surroundingRange
;
2980 surroundingRange
.setStartAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
2981 surroundingRange
.setEndAt( endTable
, CKEDITOR
.POSITION_BEFORE_START
);
2983 leftRange
= range
.clone();
2984 leftRange
.setEndAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
2986 rightRange
= range
.clone();
2987 rightRange
.setStartAt( endTable
, CKEDITOR
.POSITION_BEFORE_START
);
2989 tableContentsRanges
= findTableContentsRanges( leftRange
).concat( findTableContentsRanges( rightRange
) );
2991 // Divide the initial range into two parts:
2992 // * range which contains the part containing the table,
2993 // * surroundingRange which contains the part outside the table.
2995 // The surroundingRange exists only if one of the range ends is
2996 // located outside the table.
2998 // <p>a{b</p><table>..]..</table><p>cd</p>
2999 // becomes (respectively: surroundingRange, range):
3000 // <p>a{b</p>][<table>..]..</table><p>cd</p>
3001 else if ( !startTable
) {
3002 that
.tableSurroundingRange
= surroundingRange
;
3003 surroundingRange
.setEndAt( endTable
, CKEDITOR
.POSITION_BEFORE_START
);
3005 range
.setStartAt( endTable
, CKEDITOR
.POSITION_AFTER_START
);
3007 // <p>ab</p><table>..[..</table><p>c}d</p>
3008 // becomes (respectively range, surroundingRange):
3009 // <p>ab</p><table>..[..</table>][<p>c}d</p>
3010 else if ( !endTable
) {
3011 that
.tableSurroundingRange
= surroundingRange
;
3012 surroundingRange
.setStartAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
3014 range
.setEndAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
3017 // Use already calculated or calculate for the remaining range.
3018 that
.tableContentsRanges
= tableContentsRanges
? tableContentsRanges : findTableContentsRanges( range
);
3020 // Leaving the below for debugging purposes.
3022 // if ( that.tableSurroundingRange ) {
3023 // console.log( 'tableSurroundingRange' );
3024 // console.log( bender.tools.range.getWithHtml( that.tableSurroundingRange.root, that.tableSurroundingRange ) );
3027 // console.log( 'tableContentsRanges' );
3028 // that.tableContentsRanges.forEach( function( range ) {
3029 // console.log( bender.tools.range.getWithHtml( range.root, range ) );
3033 deleteRanges: function( that
) {
3036 // Delete table cell contents.
3037 while ( ( range
= that
.tableContentsRanges
.pop() ) ) {
3038 range
.extractContents();
3040 if ( isEmpty( range
.startContainer
) )
3041 range
.startContainer
.appendBogus();
3044 // Finally delete surroundings of the table.
3045 if ( that
.tableSurroundingRange
) {
3046 that
.tableSurroundingRange
.extractContents();
3050 purge: function( that
) {
3051 if ( !that
.purgeTableBookmark
)
3056 rangeClone
= range
.clone(),
3057 // How about different enter modes?
3058 block
= doc
.createElement( 'p' );
3060 block
.insertBefore( that
.purgeTableBookmark
.startNode
);
3062 rangeClone
.moveToBookmark( that
.purgeTableBookmark
);
3063 rangeClone
.deleteContents();
3065 that
.range
.moveToPosition( block
, CKEDITOR
.POSITION_AFTER_START
);
3075 // Detects whether use "mergeThen" argument in range.extractContents().
3076 detectExtractMerge: function( that
) {
3077 // Don't merge if playing with lists.
3079 that
.range
.startPath().contains( CKEDITOR
.dtd
.$listItem
) &&
3080 that
.range
.endPath().contains( CKEDITOR
.dtd
.$listItem
)
3084 fixUneditableRangePosition: function( range
) {
3085 if ( !range
.startContainer
.getDtd()[ '#' ] ) {
3086 range
.moveToClosestEditablePosition( null, true );
3090 // Perform auto paragraphing if needed.
3091 autoParagraph: function( editor
, range
) {
3092 var path
= range
.startPath(),
3095 if ( shouldAutoParagraph( editor
, path
.block
, path
.blockLimit
) && ( fixBlock
= autoParagraphTag( editor
) ) ) {
3096 fixBlock
= range
.document
.createElement( fixBlock
);
3097 fixBlock
.appendBogus();
3098 range
.insertNode( fixBlock
);
3099 range
.moveToPosition( fixBlock
, CKEDITOR
.POSITION_AFTER_START
);
3108 * Whether the editor must output an empty value (`''`) if its content only consists
3109 * of an empty paragraph.
3111 * config.ignoreEmptyParagraph = false;
3113 * @cfg {Boolean} [ignoreEmptyParagraph=true]
3114 * @member CKEDITOR.config
3118 * Event fired by the editor in order to get accessibility help label.
3119 * The event is responded to by a component which provides accessibility
3120 * help (i.e. the `a11yhelp` plugin) hence the editor is notified whether
3121 * accessibility help is available.
3125 * editor.on( 'ariaEditorHelpLabel', function( evt ) {
3126 * evt.data.label = editor.lang.common.editorHelp;
3131 * var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
3134 * @event ariaEditorHelpLabel
3135 * @param {String} label The label to be used.
3136 * @member CKEDITOR.editor
3140 * Event fired when the user double-clicks in the editable area.
3141 * The event allows to open a dialog window for a clicked element in a convenient way:
3143 * editor.on( 'doubleclick', function( evt ) {
3144 * var element = evt.data.element;
3146 * if ( element.is( 'table' ) )
3147 * evt.data.dialog = 'tableProperties';
3150 * **Note:** To handle double-click on a widget use {@link CKEDITOR.plugins.widget#doubleclick}.
3152 * @event doubleclick
3154 * @param {CKEDITOR.dom.element} data.element The double-clicked element.
3155 * @param {String} data.dialog The dialog window to be opened. If set by the listener,
3156 * the specified dialog window will be opened.
3157 * @member CKEDITOR.editor