2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 var isNotWhitespace
, isNotBookmark
, isEmpty
, isBogus
, emptyParagraphRegexp
,
8 insert
, fixTableAfterContentsDeletion
, fixListAfterContentsDelete
, getHtmlFromRangeHelpers
, extractHtmlFromRangeHelpers
;
11 * Editable class which provides all editing related activities by
12 * the `contenteditable` element, dynamically get attached to editor instance.
14 * @class CKEDITOR.editable
15 * @extends CKEDITOR.dom.element
17 CKEDITOR
.editable
= CKEDITOR
.tools
.createClass( {
18 base: CKEDITOR
.dom
.element
,
20 * The constructor only stores generic editable creation logic that is commonly shared among
21 * all different editable elements.
23 * @constructor Creates an editable class instance.
24 * @param {CKEDITOR.editor} editor The editor instance on which the editable operates.
25 * @param {HTMLElement/CKEDITOR.dom.element} element Any DOM element that was as the editor's
26 * editing container, e.g. it could be either an HTML element with the `contenteditable` attribute
27 * set to the `true` that handles WYSIWYG editing or a `<textarea>` element that handles source editing.
29 $: function( editor
, element
) {
30 // Transform the element into a CKEDITOR.dom.element instance.
31 this.base( element
.$ || element
);
36 * Indicates the initialization status of the editable element. The following statuses are available:
38 * * **unloaded** – the initial state. The editable's instance was created but
39 * is not fully loaded (in particular it has no data).
40 * * **ready** – the editable is fully initialized. The `ready` status is set after
41 * the first {@link CKEDITOR.editor#method-setData} is called.
42 * * **detached** – the editable was detached.
48 this.status
= 'unloaded';
51 * Indicates whether the editable element gained focus.
53 * @property {Boolean} hasFocus
55 this.hasFocus
= false;
57 // The bootstrapping logic.
66 // [Webkit] When DOM focus is inside of nested contenteditable elements,
67 // apply focus on the main editable will compromise it's text selection.
68 if ( CKEDITOR
.env
.webkit
&& !this.hasFocus
) {
69 // Restore focus on element which we cached (on selectionCheck) as previously active.
70 active
= this.editor
._
.previousActive
|| this.getDocument().getActive();
71 if ( this.contains( active
) ) {
77 // [Edge] Starting from EdgeHTML 14.14393, it does not support `setActive`. We need to use focus which
78 // causes unexpected scroll. Store scrollTop value so it can be restored after focusing editor.
79 // Scroll only happens if the editor is focused for the first time. (#14825)
80 if ( CKEDITOR
.env
.edge
&& CKEDITOR
.env
.version
> 14 && !this.hasFocus
&& this.getDocument().equals( CKEDITOR
.document
) ) {
81 this.editor
._
.previousScrollTop
= this.$.scrollTop
;
84 // [IE] Use instead "setActive" method to focus the editable if it belongs to the host page document,
85 // to avoid bringing an unexpected scroll.
87 if ( CKEDITOR
.env
.ie
&& !( CKEDITOR
.env
.edge
&& CKEDITOR
.env
.version
> 14 ) && this.getDocument().equals( CKEDITOR
.document
) ) {
93 // IE throws unspecified error when focusing editable after closing dialog opened on nested editable.
94 if ( !CKEDITOR
.env
.ie
)
98 // Remedy if Safari doens't applies focus properly. (#279)
99 if ( CKEDITOR
.env
.safari
&& !this.isInline() ) {
100 active
= CKEDITOR
.document
.getActive();
101 if ( !active
.equals( this.getWindow().getFrame() ) )
102 this.getWindow().focus();
108 * Overrides {@link CKEDITOR.dom.element#on} to have special `focus/blur` handling.
109 * The `focusin/focusout` events are used in IE to replace regular `focus/blur` events
110 * because we want to avoid the asynchronous nature of later ones.
112 on: function( name
, fn
) {
113 var args
= Array
.prototype.slice
.call( arguments
, 0 );
115 if ( CKEDITOR
.env
.ie
&& ( /^focus|blur$/ ).exec( name
) ) {
116 name
= name
== 'focus' ? 'focusin' : 'focusout';
118 // The "focusin/focusout" events bubbled, e.g. If there are elements with layout
119 // they fire this event when clicking in to edit them but it must be ignored
120 // to allow edit their contents. (#4682)
121 fn
= isNotBubbling( fn
, this );
126 return CKEDITOR
.dom
.element
.prototype.on
.apply( this, args
);
130 * Registers an event listener that needs to be removed when detaching this editable.
131 * This means that it will be automatically removed when {@link #detach} is executed,
132 * for example on {@link CKEDITOR.editor#setMode changing editor mode} or destroying editor.
134 * Except for `obj` all other arguments have the same meaning as in {@link CKEDITOR.event#on}.
136 * This method is strongly related to the {@link CKEDITOR.editor#contentDom} and
137 * {@link CKEDITOR.editor#contentDomUnload} events, because they are fired
138 * when an editable is being attached and detached. Therefore, this method is usually used
139 * in the following way:
141 * editor.on( 'contentDom', function() {
142 * var editable = editor.editable();
143 * editable.attachListener( editable, 'mousedown', function() {
148 * This code will attach the `mousedown` listener every time a new editable is attached
149 * to the editor, which in classic (`iframe`-based) editor happens every time the
150 * data or the mode is set. This listener will also be removed when that editable is detached.
152 * It is also possible to attach a listener to another object (e.g. to a document).
154 * editor.on( 'contentDom', function() {
155 * editor.editable().attachListener( editor.document, 'mousedown', function() {
160 * @param {CKEDITOR.event} obj The element/object to which the listener will be attached. Every object
161 * which inherits from {@link CKEDITOR.event} may be used including {@link CKEDITOR.dom.element},
162 * {@link CKEDITOR.dom.document}, and {@link CKEDITOR.editable}.
163 * @param {String} eventName The name of the event that will be listened to.
164 * @param {Function} listenerFunction The function listening to the
165 * event. A single {@link CKEDITOR.eventInfo} object instance
166 * containing all the event data is passed to this function.
167 * @param {Object} [scopeObj] The object used to scope the listener
168 * call (the `this` object). If omitted, the current object is used.
169 * @param {Object} [listenerData] Data to be sent as the
170 * {@link CKEDITOR.eventInfo#listenerData} when calling the listener.
171 * @param {Number} [priority=10] The listener priority. Lower priority
172 * listeners are called first. Listeners with the same priority
173 * value are called in the registration order.
174 * @returns {Object} An object containing the `removeListener`
175 * function that can be used to remove the listener at any time.
177 attachListener: function( obj
/*, event, fn, scope, listenerData, priority*/ ) {
178 !this._
.listeners
&& ( this._
.listeners
= [] );
179 // Register the listener.
180 var args
= Array
.prototype.slice
.call( arguments
, 1 ),
181 listener
= obj
.on
.apply( obj
, args
);
183 this._
.listeners
.push( listener
);
189 * Remove all event listeners registered from {@link #attachListener}.
191 clearListeners: function() {
192 var listeners
= this._
.listeners
;
193 // Don't get broken by this.
195 while ( listeners
.length
)
196 listeners
.pop().removeListener();
201 * Restore all attribution changes made by {@link #changeAttr }.
203 restoreAttrs: function() {
204 var changes
= this._
.attrChanges
, orgVal
;
205 for ( var attr
in changes
) {
206 if ( changes
.hasOwnProperty( attr
) ) {
207 orgVal
= changes
[ attr
];
208 // Restore original attribute.
209 orgVal
!== null ? this.setAttribute( attr
, orgVal
) : this.removeAttribute( attr
);
215 * Adds a CSS class name to this editable that needs to be removed on detaching.
217 * @param {String} className The class name to be added.
218 * @see CKEDITOR.dom.element#addClass
220 attachClass: function( cls
) {
221 var classes
= this.getCustomData( 'classes' );
222 if ( !this.hasClass( cls
) ) {
223 !classes
&& ( classes
= [] ), classes
.push( cls
);
224 this.setCustomData( 'classes', classes
);
225 this.addClass( cls
);
230 * Make an attribution change that would be reverted on editable detaching.
231 * @param {String} attr The attribute name to be changed.
232 * @param {String} val The value of specified attribute.
234 changeAttr: function( attr
, val
) {
235 var orgVal
= this.getAttribute( attr
);
236 if ( val
!== orgVal
) {
237 !this._
.attrChanges
&& ( this._
.attrChanges
= {} );
239 // Saved the original attribute val.
240 if ( !( attr
in this._
.attrChanges
) )
241 this._
.attrChanges
[ attr
] = orgVal
;
243 this.setAttribute( attr
, val
);
248 * Low-level method for inserting text into the editable.
249 * See the {@link CKEDITOR.editor#method-insertText} method which is the editor-level API
252 * @param {String} text
254 insertText: function( text
) {
255 // Focus the editor before calling transformPlainTextToHtml. (#12726)
257 this.insertHtml( this.transformPlainTextToHtml( text
), 'text' );
261 * Transforms plain text to HTML based on current selection and {@link CKEDITOR.editor#activeEnterMode}.
264 * @param {String} text Text to transform.
265 * @returns {String} HTML generated from the text.
267 transformPlainTextToHtml: function( text
) {
268 var enterMode
= this.editor
.getSelection().getStartElement().hasAscendant( 'pre', true ) ?
270 this.editor
.activeEnterMode
;
272 return CKEDITOR
.tools
.transformPlainTextToHtml( text
, enterMode
);
276 * Low-level method for inserting HTML into the editable.
277 * See the {@link CKEDITOR.editor#method-insertHtml} method which is the editor-level API
280 * This method will insert HTML into the current selection or a given range. It also creates an undo snapshot,
281 * scrolls the viewport to the insertion and selects the range next to the inserted content.
282 * If you want to insert HTML without additional operations use {@link #method-insertHtmlIntoRange}.
284 * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
286 * @param {String} data The HTML to be inserted.
287 * @param {String} [mode='html'] See {@link CKEDITOR.editor#method-insertHtml}'s param.
288 * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range
289 * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case).
290 * Introduced in CKEditor 4.5.
292 insertHtml: function( data
, mode
, range
) {
293 var editor
= this.editor
;
296 editor
.fire( 'saveSnapshot' );
299 // HTML insertion only considers the first range.
300 // Note: getRanges will be overwritten for tests since we want to test
301 // custom ranges and bypass native selections.
302 range
= editor
.getSelection().getRanges()[ 0 ];
305 // Default mode is 'html'.
306 insert( this, mode
|| 'html', data
, range
);
308 // Make the final range selection.
313 this.editor
.fire( 'afterInsertHtml', {} );
317 * Inserts HTML into the position in the editor determined by the range.
319 * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects inserted
320 * HTML. If you want to do it, use {@link #method-insertHtml}.
322 * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
325 * @param {String} data HTML code to be inserted into the editor.
326 * @param {CKEDITOR.dom.range} range The range as a place of insertion.
327 * @param {String} [mode='html'] Mode in which HTML will be inserted.
328 * See {@link CKEDITOR.editor#method-insertHtml}.
330 insertHtmlIntoRange: function( data
, range
, mode
) {
331 // Default mode is 'html'
332 insert( this, mode
|| 'html', data
, range
);
334 this.editor
.fire( 'afterInsertHtml', { intoRange: range
} );
338 * Low-level method for inserting an element into the editable.
339 * See the {@link CKEDITOR.editor#method-insertElement} method which is the editor-level API
342 * This method will insert the element into the current selection or a given range. It also creates an undo
343 * snapshot, scrolls the viewport to the insertion and selects the range next to the inserted content.
344 * If you want to insert an element without additional operations use {@link #method-insertElementIntoRange}.
346 * @param {CKEDITOR.dom.element} element The element to insert.
347 * @param {CKEDITOR.dom.range} [range] If specified, the element will be inserted into the range
348 * instead of into the selection.
350 insertElement: function( element
, range
) {
351 var editor
= this.editor
;
353 // Prepare for the insertion. For example - focus editor (#11848).
355 editor
.fire( 'saveSnapshot' );
357 var enterMode
= editor
.activeEnterMode
,
358 selection
= editor
.getSelection(),
359 elementName
= element
.getName(),
360 isBlock
= CKEDITOR
.dtd
.$block
[ elementName
];
363 range
= selection
.getRanges()[ 0 ];
366 // Insert element into first range only and ignore the rest (#11183).
367 if ( this.insertElementIntoRange( element
, range
) ) {
368 range
.moveToPosition( element
, CKEDITOR
.POSITION_AFTER_END
);
370 // If we're inserting a block element, the new cursor position must be
371 // optimized. (#3100,#5436,#8950)
373 // Find next, meaningful element.
374 var next
= element
.getNext( function( node
) {
375 return isNotEmpty( node
) && !isBogus( node
);
378 if ( next
&& next
.type
== CKEDITOR
.NODE_ELEMENT
&& next
.is( CKEDITOR
.dtd
.$block
) ) {
379 // If the next one is a text block, move cursor to the start of it's content.
380 if ( next
.getDtd()[ '#' ] )
381 range
.moveToElementEditStart( next
);
382 // Otherwise move cursor to the before end of the last element.
384 range
.moveToElementEditEnd( element
);
386 // Open a new line if the block is inserted at the end of parent.
387 else if ( !next
&& enterMode
!= CKEDITOR
.ENTER_BR
) {
388 next
= range
.fixBlock( true, enterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p' );
389 range
.moveToElementEditStart( next
);
394 // Set up the correct selection.
395 selection
.selectRanges( [ range
] );
401 * Alias for {@link #insertElement}.
404 * @param {CKEDITOR.dom.element} element The element to be inserted.
406 insertElementIntoSelection: function( element
) {
407 this.insertElement( element
);
411 * Inserts an element into the position in the editor determined by the range.
413 * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects the inserted
414 * element. If you want to do it, use the {@link #method-insertElement} method.
416 * @param {CKEDITOR.dom.element} element The element to be inserted.
417 * @param {CKEDITOR.dom.range} range The range as a place of insertion.
418 * @returns {Boolean} Informs whether the insertion was successful.
420 insertElementIntoRange: function( element
, range
) {
421 var editor
= this.editor
,
422 enterMode
= editor
.config
.enterMode
,
423 elementName
= element
.getName(),
424 isBlock
= CKEDITOR
.dtd
.$block
[ elementName
];
426 if ( range
.checkReadOnly() )
429 // Remove the original contents, merge split nodes.
430 range
.deleteContents( 1 );
432 if ( range
.startContainer
.type
== CKEDITOR
.NODE_ELEMENT
) {
433 // If range is placed in intermediate element (not td or th), we need to do three things:
434 // * fill emptied <td/th>s with if browser needs them,
435 // * remove empty text nodes so IE8 won't crash
436 // (http://dev.ckeditor.com/ticket/11183#comment:8),
437 // * fix structure and move range into the <td/th> element.
438 if ( range
.startContainer
.is( { tr: 1, table: 1, tbody: 1, thead: 1, tfoot: 1 } ) ) {
439 fixTableAfterContentsDeletion( range
);
440 } else if ( range
.startContainer
.is( CKEDITOR
.dtd
.$list
) ) {
441 // Similarly there's a need for lists.
442 fixListAfterContentsDelete( range
);
446 // If we're inserting a block at dtd-violated position, split
447 // the parent blocks until we reach blockLimit.
451 while ( ( current
= range
.getCommonAncestor( 0, 1 ) ) &&
452 ( dtd
= CKEDITOR
.dtd
[ current
.getName() ] ) &&
453 !( dtd
&& dtd
[ elementName
] ) ) {
454 // Split up inline elements.
455 if ( current
.getName() in CKEDITOR
.dtd
.span
)
456 range
.splitElement( current
);
458 // If we're in an empty block which indicate a new paragraph,
459 // simply replace it with the inserting block.(#3664)
460 else if ( range
.checkStartOfBlock() && range
.checkEndOfBlock() ) {
461 range
.setStartBefore( current
);
462 range
.collapse( true );
465 range
.splitBlock( enterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p', editor
.editable() );
470 // Insert the new node.
471 range
.insertNode( element
);
473 // Return true if insertion was successful.
478 * @see CKEDITOR.editor#setData
480 setData: function( data
, isSnapshot
) {
482 data
= this.editor
.dataProcessor
.toHtml( data
);
484 this.setHtml( data
);
485 this.fixInitialSelection();
487 // Editable is ready after first setData.
488 if ( this.status
== 'unloaded' )
489 this.status
= 'ready';
491 this.editor
.fire( 'dataReady' );
495 * @see CKEDITOR.editor#getData
497 getData: function( isSnapshot
) {
498 var data
= this.getHtml();
501 data
= this.editor
.dataProcessor
.toDataFormat( data
);
507 * Changes the read-only state of this editable.
509 * @param {Boolean} isReadOnly
511 setReadOnly: function( isReadOnly
) {
512 this.setAttribute( 'contenteditable', !isReadOnly
);
516 * Detaches this editable object from the DOM (removes classes, listeners, etc.)
519 // Cleanup the element.
520 this.removeClass( 'cke_editable' );
522 this.status
= 'detached';
524 // Save the editor reference which will be lost after
525 // calling detach from super class.
526 var editor
= this.editor
;
530 delete editor
.document
;
531 delete editor
.window
;
535 * Checks if the editable is one of the host page elements, indicates
536 * an inline editing environment.
540 isInline: function() {
541 return this.getDocument().equals( CKEDITOR
.document
);
545 * Fixes the selection and focus which may be in incorrect state after
546 * editable's inner HTML was overwritten.
548 * If the editable did not have focus, then the selection will be fixed when the editable
549 * is focused for the first time. If the editable already had focus, then the selection will
550 * be fixed immediately.
552 * To understand the problem see:
554 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
555 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusafterundoing
556 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/selectionafterfocusing
557 * * http://tests.ckeditor.dev:1030/tests/plugins/newpage/manual/selectionafternewpage
562 fixInitialSelection: function() {
565 // Deal with IE8- IEQM (the old MS selection) first.
566 if ( CKEDITOR
.env
.ie
&& ( CKEDITOR
.env
.version
< 9 || CKEDITOR
.env
.quirks
) ) {
567 if ( this.hasFocus
) {
575 // If editable did not have focus, fix the selection when it is first focused.
576 if ( !this.hasFocus
) {
577 this.once( 'focus', function() {
579 }, null, null, -999 );
580 // If editable had focus, fix the selection immediately.
586 function fixSelection() {
587 var $doc
= that
.getDocument().$,
588 $sel
= $doc
.getSelection();
590 if ( requiresFix( $sel
) ) {
591 var range
= new CKEDITOR
.dom
.range( that
);
592 range
.moveToElementEditStart( that
);
594 var $range
= $doc
.createRange();
595 $range
.setStart( range
.startContainer
.$, range
.startOffset
);
596 $range
.collapse( true );
598 $sel
.removeAllRanges();
599 $sel
.addRange( $range
);
603 function requiresFix( $sel
) {
604 // This condition covers most broken cases after setting data.
605 if ( $sel
.anchorNode
&& $sel
.anchorNode
== that
.$ ) {
610 // http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
611 // (the inline editor TC)
612 if ( CKEDITOR
.env
.webkit
) {
613 var active
= that
.getDocument().getActive();
614 if ( active
&& active
.equals( that
) && !$sel
.anchorNode
) {
620 function fixMSSelection() {
621 var $doc
= that
.getDocument().$,
622 $sel
= $doc
.selection
,
623 active
= that
.getDocument().getActive();
625 if ( $sel
.type
== 'None' && active
.equals( that
) ) {
626 var range
= new CKEDITOR
.dom
.range( that
),
628 $range
= $doc
.body
.createTextRange();
630 range
.moveToElementEditStart( that
);
632 parentElement
= range
.startContainer
;
633 if ( parentElement
.type
!= CKEDITOR
.NODE_ELEMENT
) {
634 parentElement
= parentElement
.getParent();
637 $range
.moveToElementText( parentElement
.$ );
638 $range
.collapse( true );
645 * The base of the {@link CKEDITOR.editor#getSelectedHtml} method.
648 * @method getHtmlFromRange
649 * @param {CKEDITOR.dom.range} range
650 * @returns {CKEDITOR.dom.documentFragment}
652 getHtmlFromRange: function( range
) {
653 // There's nothing to return if range is collapsed.
654 if ( range
.collapsed
)
655 return new CKEDITOR
.dom
.documentFragment( range
.document
);
657 // Info object passed between methods.
659 doc: this.getDocument(),
660 // Leave original range object untouched.
664 getHtmlFromRangeHelpers
.eol
.detect( that
, this );
665 getHtmlFromRangeHelpers
.bogus
.exclude( that
);
666 getHtmlFromRangeHelpers
.cell
.shrink( that
);
668 that
.fragment
= that
.range
.cloneContents();
670 getHtmlFromRangeHelpers
.tree
.rebuild( that
, this );
671 getHtmlFromRangeHelpers
.eol
.fix( that
, this );
673 return new CKEDITOR
.dom
.documentFragment( that
.fragment
.$ );
677 * The base of the {@link CKEDITOR.editor#extractSelectedHtml} method.
679 * **Note:** The range is modified so it matches the desired selection after extraction
680 * even though the selection is not made.
683 * @param {CKEDITOR.dom.range} range
684 * @param {Boolean} [removeEmptyBlock=false] See {@link CKEDITOR.editor#extractSelectedHtml}'s parameter.
685 * Note that the range will not be modified if this parameter is set to `true`.
686 * @returns {CKEDITOR.dom.documentFragment} The extracted fragment of the editable content.
688 extractHtmlFromRange: function( range
, removeEmptyBlock
) {
689 var helpers
= extractHtmlFromRangeHelpers
,
694 // Since it is quite hard to build a valid documentFragment
695 // out of extracted contents because DOM changes, let's mimic
696 // extracted HTML with #getHtmlFromRange. Yep. It's a hack.
697 extractedFragment
= this.getHtmlFromRange( range
);
699 // Collapsed range means that there's nothing to extract.
700 if ( range
.collapsed
) {
702 return extractedFragment
;
705 // Include inline element if possible.
706 range
.enlarge( CKEDITOR
.ENLARGE_INLINE
, 1 );
708 // This got to be done before bookmarks are created because purging
709 // depends on the position of the range at the boundaries of the table,
710 // usually distorted by bookmark spans.
711 helpers
.table
.detectPurge( that
);
713 // We'll play with DOM, let's hold the position of the range.
714 that
.bookmark
= range
.createBookmark();
715 // While bookmarked, make unaccessible, to make sure that none of the methods
716 // will try to use it (they should use that.bookmark).
717 // This is done because ranges get desynchronized with the DOM when more bookmarks
718 // is created (as for instance that.targetBookmark).
721 // The range to be restored after extraction should be kept
722 // outside of the range, so it's not removed by range.extractContents.
723 var targetRange
= this.editor
.createRange();
724 targetRange
.moveToPosition( that
.bookmark
.startNode
, CKEDITOR
.POSITION_BEFORE_START
);
725 that
.targetBookmark
= targetRange
.createBookmark();
727 // Execute content-specific detections.
728 helpers
.list
.detectMerge( that
, this );
729 helpers
.table
.detectRanges( that
, this );
730 helpers
.block
.detectMerge( that
, this );
732 // Simply, do the job.
733 if ( that
.tableContentsRanges
) {
734 helpers
.table
.deleteRanges( that
);
736 // Done here only to remove bookmark's spans.
737 range
.moveToBookmark( that
.bookmark
);
740 // To use the range we need to restore the bookmark and make
741 // the range accessible again.
742 range
.moveToBookmark( that
.bookmark
);
744 range
.extractContents( helpers
.detectExtractMerge( that
) );
747 // Move working range to desired, pre-computed position.
748 range
.moveToBookmark( that
.targetBookmark
);
750 // Make sure range is always anchored in an element. For consistency.
753 // It my happen that the uncollapsed range which referred to a valid selection,
754 // will be placed in an uneditable location after being collapsed:
755 // <tr>[<td>x</td>]</tr> -> <tr>[]<td>x</td></tr> -> <tr><td>[]x</td></tr>
756 helpers
.fixUneditableRangePosition( range
);
758 // Execute content-specific post-extract routines.
759 helpers
.list
.merge( that
, this );
760 helpers
.table
.purge( that
, this );
761 helpers
.block
.merge( that
, this );
763 // Remove empty block, duh!
764 if ( removeEmptyBlock
) {
765 var path
= range
.startPath();
767 // <p><b>^</b></p> is empty block.
769 range
.checkStartOfBlock() &&
770 range
.checkEndOfBlock() &&
772 !range
.root
.equals( path
.block
) &&
773 // Do not remove a block with bookmarks. (#13465)
774 !hasBookmarks( path
.block
) ) {
775 range
.moveToPosition( path
.block
, CKEDITOR
.POSITION_BEFORE_START
);
779 // Auto paragraph, if needed.
780 helpers
.autoParagraph( this.editor
, range
);
782 // Let's have a bogus next to the caret, if needed.
783 if ( isEmpty( range
.startContainer
) )
784 range
.startContainer
.appendBogus();
787 // Merge inline siblings if any around the caret.
788 range
.startContainer
.mergeSiblings();
790 return extractedFragment
;
794 * Editable element bootstrapping.
799 var editor
= this.editor
;
801 // Handle the load/read of editor data/snapshot.
802 this.attachListener( editor
, 'beforeGetData', function() {
803 var data
= this.getData();
805 // Post processing html output of wysiwyg editable.
806 if ( !this.is( 'textarea' ) ) {
807 // Reset empty if the document contains only one empty paragraph.
808 if ( editor
.config
.ignoreEmptyParagraph
!== false )
809 data
= data
.replace( emptyParagraphRegexp
, function( match
, lookback
) {
814 editor
.setData( data
, null, 1 );
817 this.attachListener( editor
, 'getSnapshot', function( evt
) {
818 evt
.data
= this.getData( 1 );
821 this.attachListener( editor
, 'afterSetData', function() {
822 this.setData( editor
.getData( 1 ) );
824 this.attachListener( editor
, 'loadSnapshot', function( evt
) {
825 this.setData( evt
.data
, 1 );
828 // Delegate editor focus/blur to editable.
829 this.attachListener( editor
, 'beforeFocus', function() {
830 var sel
= editor
.getSelection(),
831 ieSel
= sel
&& sel
.getNative();
833 // IE considers control-type element as separate
834 // focus host when selected, avoid destroying the
835 // selection in such case. (#5812) (#8949)
836 if ( ieSel
&& ieSel
.type
== 'Control' )
842 this.attachListener( editor
, 'insertHtml', function( evt
) {
843 this.insertHtml( evt
.data
.dataValue
, evt
.data
.mode
, evt
.data
.range
);
845 this.attachListener( editor
, 'insertElement', function( evt
) {
846 this.insertElement( evt
.data
);
848 this.attachListener( editor
, 'insertText', function( evt
) {
849 this.insertText( evt
.data
);
852 // Update editable state.
853 this.setReadOnly( editor
.readOnly
);
855 // The editable class.
856 this.attachClass( 'cke_editable' );
858 // The element mode css class.
859 if ( editor
.elementMode
== CKEDITOR
.ELEMENT_MODE_INLINE
) {
860 this.attachClass( 'cke_editable_inline' );
861 } else if ( editor
.elementMode
== CKEDITOR
.ELEMENT_MODE_REPLACE
||
862 editor
.elementMode
== CKEDITOR
.ELEMENT_MODE_APPENDTO
) {
863 this.attachClass( 'cke_editable_themed' );
866 this.attachClass( 'cke_contents_' + editor
.config
.contentsLangDirection
);
868 // Setup editor keystroke handlers on this element.
869 var keystrokeHandler
= editor
.keystrokeHandler
;
871 // If editor is read-only, then make sure that BACKSPACE key
872 // is blocked to prevent browser history navigation.
873 keystrokeHandler
.blockedKeystrokes
[ 8 ] = +editor
.readOnly
;
875 editor
.keystrokeHandler
.attach( this );
877 // Update focus states.
878 this.on( 'blur', function() {
879 this.hasFocus
= false;
882 this.on( 'focus', function() {
883 this.hasFocus
= true;
886 if ( CKEDITOR
.env
.webkit
) {
887 // [WebKit] Save scrollTop value so it can be used when restoring locked selection. (#14659)
888 this.on( 'scroll', function() {
889 editor
._
.previousScrollTop
= editor
.editable().$.scrollTop
;
893 // [Edge] This is the other part of the workaround for Edge which restores saved
894 // scrollTop value and removes listener which is not needed anymore. (#14825)
895 if ( CKEDITOR
.env
.edge
&& CKEDITOR
.env
.version
> 14 ) {
897 var fixScrollOnFocus = function() {
898 var editable
= editor
.editable();
900 if ( editor
._
.previousScrollTop
!= null && editable
.getDocument().equals( CKEDITOR
.document
) ) {
901 editable
.$.scrollTop
= editor
._
.previousScrollTop
;
902 editor
._
.previousScrollTop
= null;
903 this.removeListener( 'scroll', fixScrollOnFocus
);
907 this.on( 'scroll', fixScrollOnFocus
);
910 // Register to focus manager.
911 editor
.focusManager
.add( this );
913 // Inherit the initial focus on editable element.
914 if ( this.equals( CKEDITOR
.document
.getActive() ) ) {
915 this.hasFocus
= true;
916 // Pending until this editable has attached.
917 editor
.once( 'contentDom', function() {
918 editor
.focusManager
.focus( this );
922 // Apply tab index on demand, with original direction saved.
923 if ( this.isInline() ) {
925 // tabIndex of the editable is different than editor's one.
926 // Update the attribute of the editable.
927 this.changeAttr( 'tabindex', editor
.tabIndex
);
930 // The above is all we'll be doing for a <textarea> editable.
931 if ( this.is( 'textarea' ) )
934 // The DOM document which the editing acts upon.
935 editor
.document
= this.getDocument();
936 editor
.window
= this.getWindow();
938 var doc
= editor
.document
;
940 this.changeAttr( 'spellcheck', !editor
.config
.disableNativeSpellChecker
);
942 // Apply contents direction on demand, with original direction saved.
943 var dir
= editor
.config
.contentsLangDirection
;
944 if ( this.getDirection( 1 ) != dir
)
945 this.changeAttr( 'dir', dir
);
947 // Create the content stylesheet for this document.
948 var styles
= CKEDITOR
.getCss();
950 var head
= doc
.getHead(),
951 stylesElement
= head
.getCustomData( 'stylesheet' );
953 if ( !stylesElement
) {
954 var sheet
= doc
.appendStyleText( styles
);
955 sheet
= new CKEDITOR
.dom
.element( sheet
.ownerNode
|| sheet
.owningElement
);
956 head
.setCustomData( 'stylesheet', sheet
);
957 sheet
.data( 'cke-temp', 1 );
958 } else if ( styles
!= stylesElement
.getText() ) {
959 CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 9 ? stylesElement
.$.styleSheet
.cssText
= styles : stylesElement
.setText( styles
);
963 // Update the stylesheet sharing count.
964 var ref
= doc
.getCustomData( 'stylesheet_ref' ) || 0;
965 doc
.setCustomData( 'stylesheet_ref', ref
+ 1 );
967 // Pass this configuration to styles system.
968 this.setCustomData( 'cke_includeReadonly', !editor
.config
.disableReadonlyStyling
);
970 // Prevent the browser opening read-only links. (#6032 & #10912)
971 this.attachListener( this, 'click', function( evt
) {
974 var link
= new CKEDITOR
.dom
.elementPath( evt
.getTarget(), this ).contains( 'a' );
976 if ( link
&& evt
.$.button
!= 2 && link
.isReadOnly() )
977 evt
.preventDefault();
980 var backspaceOrDelete
= { 8: 1, 46: 1 };
982 // Override keystrokes which should have deletion behavior
983 // on fully selected element . (#4047) (#7645)
984 this.attachListener( editor
, 'key', function( evt
) {
985 if ( editor
.readOnly
)
988 // Use getKey directly in order to ignore modifiers.
989 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
990 var keyCode
= evt
.data
.domEvent
.getKey(),
993 // Backspace OR Delete.
994 if ( keyCode
in backspaceOrDelete
) {
995 var sel
= editor
.getSelection(),
997 range
= sel
.getRanges()[ 0 ],
998 path
= range
.startPath(),
1005 // [IE<11] Remove selected image/anchor/etc here to avoid going back in history. (#10055)
1006 ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 11 && ( selected
= sel
.getSelectedElement() ) ) ||
1007 // Remove the entire list/table on fully selected content. (#7645)
1008 ( selected
= getSelectedTableList( sel
) ) ) {
1009 // Make undo snapshot.
1010 editor
.fire( 'saveSnapshot' );
1012 // Delete any element that 'hasLayout' (e.g. hr,table) in IE8 will
1013 // break up the selection, safely manage it here. (#4795)
1014 range
.moveToPosition( selected
, CKEDITOR
.POSITION_BEFORE_START
);
1015 // Remove the control manually.
1019 editor
.fire( 'saveSnapshot' );
1022 } else if ( range
.collapsed
) {
1023 // Handle the following special cases: (#6217)
1024 // 1. Del/Backspace key before/after table;
1025 // 2. Backspace Key after start of table.
1026 if ( ( block
= path
.block
) &&
1027 ( next
= block
[ rtl
? 'getPrevious' : 'getNext' ]( isNotWhitespace
) ) &&
1028 ( next
.type
== CKEDITOR
.NODE_ELEMENT
) &&
1029 next
.is( 'table' ) &&
1030 range
[ rtl
? 'checkStartOfBlock' : 'checkEndOfBlock' ]() ) {
1031 editor
.fire( 'saveSnapshot' );
1033 // Remove the current empty block.
1034 if ( range
[ rtl
? 'checkEndOfBlock' : 'checkStartOfBlock' ]() )
1037 // Move cursor to the beginning/end of table cell.
1038 range
[ 'moveToElementEdit' + ( rtl
? 'End' : 'Start' ) ]( next
);
1041 editor
.fire( 'saveSnapshot' );
1045 else if ( path
.blockLimit
&& path
.blockLimit
.is( 'td' ) &&
1046 ( parent
= path
.blockLimit
.getAscendant( 'table' ) ) &&
1047 range
.checkBoundaryOfElement( parent
, rtl
? CKEDITOR
.START : CKEDITOR
.END
) &&
1048 ( next
= parent
[ rtl
? 'getPrevious' : 'getNext' ]( isNotWhitespace
) ) ) {
1049 editor
.fire( 'saveSnapshot' );
1051 // Move cursor to the end of previous block.
1052 range
[ 'moveToElementEdit' + ( rtl
? 'End' : 'Start' ) ]( next
);
1054 // Remove any previous empty block.
1055 if ( range
.checkStartOfBlock() && range
.checkEndOfBlock() )
1060 editor
.fire( 'saveSnapshot' );
1064 // BACKSPACE/DEL pressed at the start/end of table cell.
1065 else if ( ( parent
= path
.contains( [ 'td', 'th', 'caption' ] ) ) &&
1066 range
.checkBoundaryOfElement( parent
, rtl
? CKEDITOR
.START : CKEDITOR
.END
) ) {
1076 // On IE>=11 we need to fill blockless editable with <br> if it was deleted.
1077 if ( editor
.blockless
&& CKEDITOR
.env
.ie
&& CKEDITOR
.env
.needsBrFiller
) {
1078 this.attachListener( this, 'keyup', function( evt
) {
1079 if ( evt
.data
.getKeystroke() in backspaceOrDelete
&& !this.getFirst( isNotEmpty
) ) {
1082 // Set the selection before bogus, because IE tends to put it after.
1083 var range
= editor
.createRange();
1084 range
.moveToPosition( this, CKEDITOR
.POSITION_AFTER_START
);
1090 this.attachListener( this, 'dblclick', function( evt
) {
1091 if ( editor
.readOnly
)
1094 var data
= { element: evt
.data
.getTarget() };
1095 editor
.fire( 'doubleclick', data
);
1098 // Prevent automatic submission in IE #6336
1099 CKEDITOR
.env
.ie
&& this.attachListener( this, 'click', blockInputClick
);
1101 // Gecko/Webkit need some help when selecting control type elements. (#3448)
1102 // We apply same behavior for IE Edge. (#13386)
1103 if ( !CKEDITOR
.env
.ie
|| CKEDITOR
.env
.edge
) {
1104 this.attachListener( this, 'mousedown', function( ev
) {
1105 var control
= ev
.data
.getTarget();
1106 // #11727. Note: htmlDP assures that input/textarea/select have contenteditable=false
1107 // attributes. However, they also have data-cke-editable attribute, so isReadOnly() returns false,
1108 // and therefore those elements are correctly selected by this code.
1109 if ( control
.is( 'img', 'hr', 'input', 'textarea', 'select' ) && !control
.isReadOnly() ) {
1110 editor
.getSelection().selectElement( control
);
1112 // Prevent focus from stealing from the editable. (#9515)
1113 if ( control
.is( 'input', 'textarea', 'select' ) )
1114 ev
.data
.preventDefault();
1119 // For some reason, after click event is done, IE Edge loses focus on the selected element. (#13386)
1120 if ( CKEDITOR
.env
.edge
) {
1121 this.attachListener( this, 'mouseup', function( ev
) {
1122 var selectedElement
= ev
.data
.getTarget();
1123 if ( selectedElement
&& selectedElement
.is( 'img' ) ) {
1124 editor
.getSelection().selectElement( selectedElement
);
1129 // Prevent right click from selecting an empty block even
1130 // when selection is anchored inside it. (#5845)
1131 if ( CKEDITOR
.env
.gecko
) {
1132 this.attachListener( this, 'mouseup', function( ev
) {
1133 if ( ev
.data
.$.button
== 2 ) {
1134 var target
= ev
.data
.getTarget();
1136 if ( !target
.getOuterHtml().replace( emptyParagraphRegexp
, '' ) ) {
1137 var range
= editor
.createRange();
1138 range
.moveToElementEditStart( target
);
1139 range
.select( true );
1145 // Webkit: avoid from editing form control elements content.
1146 if ( CKEDITOR
.env
.webkit
) {
1147 // Prevent from tick checkbox/radiobox/select
1148 this.attachListener( this, 'click', function( ev
) {
1149 if ( ev
.data
.getTarget().is( 'input', 'select' ) )
1150 ev
.data
.preventDefault();
1153 // Prevent from editig textfield/textarea value.
1154 this.attachListener( this, 'mouseup', function( ev
) {
1155 if ( ev
.data
.getTarget().is( 'input', 'textarea' ) )
1156 ev
.data
.preventDefault();
1160 // Prevent Webkit/Blink from going rogue when joining
1161 // blocks on BACKSPACE/DEL (#11861,#9998).
1162 if ( CKEDITOR
.env
.webkit
) {
1163 this.attachListener( editor
, 'key', function( evt
) {
1164 if ( editor
.readOnly
) {
1168 // Use getKey directly in order to ignore modifiers.
1169 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
1170 var key
= evt
.data
.domEvent
.getKey();
1172 if ( !( key
in backspaceOrDelete
) )
1175 var backspace
= key
== 8,
1176 range
= editor
.getSelection().getRanges()[ 0 ],
1177 startPath
= range
.startPath();
1179 if ( range
.collapsed
) {
1180 if ( !mergeBlocksCollapsedSelection( editor
, range
, backspace
, startPath
) )
1183 if ( !mergeBlocksNonCollapsedSelection( editor
, range
, startPath
) )
1187 // Scroll to the new position of the caret (#11960).
1188 editor
.getSelection().scrollIntoView();
1189 editor
.fire( 'saveSnapshot' );
1192 }, this, null, 100 ); // Later is better – do not override existing listeners.
1198 detach: function() {
1199 // Update the editor cached data with current data.
1200 this.editor
.setData( this.editor
.getData(), 0, 1 );
1202 this.clearListeners();
1203 this.restoreAttrs();
1205 // Cleanup our custom classes.
1207 if ( ( classes
= this.removeCustomData( 'classes' ) ) ) {
1208 while ( classes
.length
)
1209 this.removeClass( classes
.pop() );
1212 // Remove contents stylesheet from document if it's the last usage.
1213 if ( !this.is( 'textarea' ) ) {
1214 var doc
= this.getDocument(),
1215 head
= doc
.getHead();
1216 if ( head
.getCustomData( 'stylesheet' ) ) {
1217 var refs
= doc
.getCustomData( 'stylesheet_ref' );
1218 if ( !( --refs
) ) {
1219 doc
.removeCustomData( 'stylesheet_ref' );
1220 var sheet
= head
.removeCustomData( 'stylesheet' );
1223 doc
.setCustomData( 'stylesheet_ref', refs
);
1228 this.editor
.fire( 'contentDomUnload' );
1230 // Free up the editor reference.
1237 * Creates, retrieves or detaches an editable element of the editor.
1238 * This method should always be used instead of calling {@link CKEDITOR.editable} directly.
1241 * @member CKEDITOR.editor
1242 * @param {CKEDITOR.dom.element/CKEDITOR.editable} [elementOrEditable] The
1243 * DOM element to become the editable or a {@link CKEDITOR.editable} object.
1245 CKEDITOR
.editor
.prototype.editable = function( element
) {
1246 var editable
= this._
.editable
;
1248 // This editor has already associated with
1249 // an editable element, silently fails.
1250 if ( editable
&& element
)
1253 if ( arguments
.length
) {
1254 editable
= this._
.editable
= element
? ( element
instanceof CKEDITOR
.editable
? element : new CKEDITOR
.editable( this, element
) ) :
1255 // Detach the editable from editor.
1256 ( editable
&& editable
.detach(), null );
1259 // Just retrieve the editable.
1263 CKEDITOR
.on( 'instanceLoaded', function( evt
) {
1264 var editor
= evt
.editor
;
1266 // and flag that the element was locked by our code so it'll be editable by the editor functions (#6046).
1267 editor
.on( 'insertElement', function( evt
) {
1268 var element
= evt
.data
;
1269 if ( element
.type
== CKEDITOR
.NODE_ELEMENT
&& ( element
.is( 'input' ) || element
.is( 'textarea' ) ) ) {
1270 // // The element is still not inserted yet, force attribute-based check.
1271 if ( element
.getAttribute( 'contentEditable' ) != 'false' )
1272 element
.data( 'cke-editable', element
.hasAttribute( 'contenteditable' ) ? 'true' : '1' );
1273 element
.setAttribute( 'contentEditable', false );
1277 editor
.on( 'selectionChange', function( evt
) {
1278 if ( editor
.readOnly
)
1281 // Auto fixing on some document structure weakness to enhance usabilities. (#3190 and #3189)
1282 var sel
= editor
.getSelection();
1283 // Do it only when selection is not locked. (#8222)
1284 if ( sel
&& !sel
.isLocked
) {
1285 var isDirty
= editor
.checkDirty();
1287 // Lock undoM before touching DOM to prevent
1288 // recording these changes as separate snapshot.
1289 editor
.fire( 'lockSnapshot' );
1291 editor
.fire( 'unlockSnapshot' );
1293 !isDirty
&& editor
.resetDirty();
1298 CKEDITOR
.on( 'instanceCreated', function( evt
) {
1299 var editor
= evt
.editor
;
1301 editor
.on( 'mode', function() {
1303 var editable
= editor
.editable();
1305 // Setup proper ARIA roles and properties for inline editable, classic
1306 // (iframe-based) editable is instead handled by plugin.
1307 if ( editable
&& editable
.isInline() ) {
1309 var ariaLabel
= editor
.title
;
1311 editable
.changeAttr( 'role', 'textbox' );
1312 editable
.changeAttr( 'aria-label', ariaLabel
);
1315 editable
.changeAttr( 'title', ariaLabel
);
1317 var helpLabel
= editor
.fire( 'ariaEditorHelpLabel', {} ).label
;
1319 // Put the voice label in different spaces, depending on element mode, so
1320 // the DOM element get auto detached on mode reload or editor destroy.
1321 var ct
= this.ui
.space( this.elementMode
== CKEDITOR
.ELEMENT_MODE_INLINE
? 'top' : 'contents' );
1323 var ariaDescId
= CKEDITOR
.tools
.getNextId(),
1324 desc
= CKEDITOR
.dom
.element
.createFromHtml( '<span id="' + ariaDescId
+ '" class="cke_voice_label">' + helpLabel
+ '</span>' );
1326 editable
.changeAttr( 'aria-describedby', ariaDescId
);
1333 // #9222: Show text cursor in Gecko.
1334 // Show default cursor over control elements on all non-IEs.
1335 CKEDITOR
.addCss( '.cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}' );
1339 // Bazillion helpers for the editable class and above listeners.
1343 isNotWhitespace
= CKEDITOR
.dom
.walker
.whitespaces( true ),
1344 isNotBookmark
= CKEDITOR
.dom
.walker
.bookmark( false, true ),
1345 isEmpty
= CKEDITOR
.dom
.walker
.empty(),
1346 isBogus
= CKEDITOR
.dom
.walker
.bogus(),
1347 // Matching an empty paragraph at the end of document.
1348 emptyParagraphRegexp
= /(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>| |\u00A0| )?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi;
1350 // Auto-fixing block-less content by wrapping paragraph (#3190), prevent
1351 // non-exitable-block by padding extra br.(#3189)
1352 // Returns truly value when dom was changed, falsy otherwise.
1353 function fixDom( evt
) {
1354 var editor
= evt
.editor
,
1355 path
= evt
.data
.path
,
1356 blockLimit
= path
.blockLimit
,
1357 selection
= evt
.data
.selection
,
1358 range
= selection
.getRanges()[ 0 ],
1359 selectionUpdateNeeded
;
1361 if ( CKEDITOR
.env
.gecko
|| ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.needsBrFiller
) ) {
1362 var blockNeedsFiller
= needsBrFiller( selection
, path
);
1363 if ( blockNeedsFiller
) {
1364 blockNeedsFiller
.appendBogus();
1365 // IE tends to place selection after appended bogus, so we need to
1366 // select the original range (placed before bogus).
1367 selectionUpdateNeeded
= CKEDITOR
.env
.ie
;
1371 // When we're in block enter mode, a new paragraph will be established
1372 // to encapsulate inline contents inside editable. (#3657)
1373 // Don't autoparagraph if browser (namely - IE) incorrectly anchored selection
1374 // inside non-editable content. This happens e.g. if non-editable block is the only
1375 // content of editable.
1376 if ( shouldAutoParagraph( editor
, path
.block
, blockLimit
) && range
.collapsed
&& !range
.getCommonAncestor().isReadOnly() ) {
1377 var testRng
= range
.clone();
1378 testRng
.enlarge( CKEDITOR
.ENLARGE_BLOCK_CONTENTS
);
1379 var walker
= new CKEDITOR
.dom
.walker( testRng
);
1380 walker
.guard = function( node
) {
1381 return !isNotEmpty( node
) ||
1382 node
.type
== CKEDITOR
.NODE_COMMENT
||
1386 // 1. Inline content discovered under cursor;
1387 // 2. Empty editable.
1388 if ( !walker
.checkForward() || testRng
.checkStartOfBlock() && testRng
.checkEndOfBlock() ) {
1389 var fixedBlock
= range
.fixBlock( true, editor
.activeEnterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p' );
1391 // For IE<11, we should remove any filler node which was introduced before.
1392 if ( !CKEDITOR
.env
.needsBrFiller
) {
1393 var first
= fixedBlock
.getFirst( isNotEmpty
);
1394 if ( first
&& isNbsp( first
) )
1398 selectionUpdateNeeded
= 1;
1400 // Cancel this selection change in favor of the next (correct). (#6811)
1405 if ( selectionUpdateNeeded
)
1409 // Checks whether current selection requires br filler to be appended.
1410 // @returns Block which needs filler or falsy value.
1411 function needsBrFiller( selection
, path
) {
1412 // Fake selection does not need filler, because it is fake.
1413 if ( selection
.isFake
)
1416 // Ensure bogus br could help to move cursor (out of styles) to the end of block. (#7041)
1417 var pathBlock
= path
.block
|| path
.blockLimit
,
1418 lastNode
= pathBlock
&& pathBlock
.getLast( isNotEmpty
);
1420 // Check some specialities of the current path block:
1421 // 1. It is really displayed as block; (#7221)
1422 // 2. It doesn't end with one inner block; (#7467)
1423 // 3. It doesn't have bogus br yet.
1425 pathBlock
&& pathBlock
.isBlockBoundary() &&
1426 !( lastNode
&& lastNode
.type
== CKEDITOR
.NODE_ELEMENT
&& lastNode
.isBlockBoundary() ) &&
1427 !pathBlock
.is( 'pre' ) && !pathBlock
.getBogus()
1432 function blockInputClick( evt
) {
1433 var element
= evt
.data
.getTarget();
1434 if ( element
.is( 'input' ) ) {
1435 var type
= element
.getAttribute( 'type' );
1436 if ( type
== 'submit' || type
== 'reset' )
1437 evt
.data
.preventDefault();
1441 function isNotEmpty( node
) {
1442 return isNotWhitespace( node
) && isNotBookmark( node
);
1445 function isNbsp( node
) {
1446 return node
.type
== CKEDITOR
.NODE_TEXT
&& CKEDITOR
.tools
.trim( node
.getText() ).match( /^(?: |\xa0)$/ );
1449 function isNotBubbling( fn
, src
) {
1450 return function( evt
) {
1451 var other
= evt
.data
.$.toElement
|| evt
.data
.$.fromElement
|| evt
.data
.$.relatedTarget
;
1453 // First of all, other may simply be null/undefined.
1454 // Second of all, at least early versions of Spartan returned empty objects from evt.relatedTarget,
1455 // so let's also check the node type.
1456 other
= ( other
&& other
.nodeType
== CKEDITOR
.NODE_ELEMENT
) ? new CKEDITOR
.dom
.element( other
) : null;
1458 if ( !( other
&& ( src
.equals( other
) || src
.contains( other
) ) ) )
1459 fn
.call( this, evt
);
1463 function hasBookmarks( element
) {
1464 // We use getElementsByTag() instead of find() to retain compatibility with IE quirks mode.
1465 var potentialBookmarks
= element
.getElementsByTag( 'span' ),
1469 if ( potentialBookmarks
) {
1470 while ( ( child
= potentialBookmarks
.getItem( i
++ ) ) ) {
1471 if ( !isNotBookmark( child
) ) {
1480 // Check if the entire table/list contents is selected.
1481 function getSelectedTableList( sel
) {
1483 range
= sel
.getRanges()[ 0 ],
1484 editable
= sel
.root
,
1485 path
= range
.startPath(),
1486 structural
= { table: 1, ul: 1, ol: 1, dl: 1 };
1488 if ( path
.contains( structural
) ) {
1489 // Clone the original range.
1490 var walkerRng
= range
.clone();
1492 // Enlarge the range: X<ul><li>[Y]</li></ul>X => [X<ul><li>]Y</li></ul>X
1493 walkerRng
.collapse( 1 );
1494 walkerRng
.setStartAt( editable
, CKEDITOR
.POSITION_AFTER_START
);
1496 // Create a new walker.
1497 var walker
= new CKEDITOR
.dom
.walker( walkerRng
);
1499 // Assign a new guard to the walker.
1500 walker
.guard
= guard();
1502 // Go backwards checking for selected structural node.
1503 walker
.checkBackward();
1505 // If there's a selected structured element when checking backwards,
1506 // then check the same forwards.
1508 // Clone the original range.
1509 walkerRng
= range
.clone();
1511 // Enlarge the range (assuming <ul> is selected element from guard):
1513 // X<ul><li>[Y]</li></ul>X => X<ul><li>Y[</li></ul>]X
1515 // If the walker went deeper down DOM than a while ago when traversing
1516 // backwards, then it doesn't make sense: an element must be selected
1517 // symmetrically. By placing range end **after previously selected node**,
1518 // we make sure we don't go no deeper in DOM when going forwards.
1519 walkerRng
.collapse();
1520 walkerRng
.setEndAt( selected
, CKEDITOR
.POSITION_AFTER_END
);
1522 // Create a new walker.
1523 walker
= new CKEDITOR
.dom
.walker( walkerRng
);
1525 // Assign a new guard to the walker.
1526 walker
.guard
= guard( true );
1528 // Reset selected node.
1531 // Go forwards checking for selected structural node.
1532 walker
.checkForward();
1540 function guard( forwardGuard
) {
1541 return function( node
, isWalkOut
) {
1542 // Save the encountered node as selected if going down the DOM structure
1543 // and the node is structured element.
1544 if ( isWalkOut
&& node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( structural
) )
1547 // Stop the walker when either traversing another non-empty node at the same
1548 // DOM level as in previous step.
1549 // NOTE: When going forwards, stop if encountered a bogus.
1550 if ( !isWalkOut
&& isNotEmpty( node
) && !( forwardGuard
&& isBogus( node
) ) )
1556 // Whether in given context (pathBlock, pathBlockLimit and editor settings)
1557 // editor should automatically wrap inline contents with blocks.
1558 function shouldAutoParagraph( editor
, pathBlock
, pathBlockLimit
) {
1559 // Check whether pathBlock equals pathBlockLimit to support nested editable (#12162).
1560 return editor
.config
.autoParagraph
!== false &&
1561 editor
.activeEnterMode
!= CKEDITOR
.ENTER_BR
&&
1563 ( editor
.editable().equals( pathBlockLimit
) && !pathBlock
) ||
1564 ( pathBlock
&& pathBlock
.getAttribute( 'contenteditable' ) == 'true' )
1568 function autoParagraphTag( editor
) {
1569 return ( editor
.activeEnterMode
!= CKEDITOR
.ENTER_BR
&& editor
.config
.autoParagraph
!== false ) ? editor
.activeEnterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p' : false;
1573 // Functions related to insertXXX methods
1575 insert
= ( function() {
1578 var DTD
= CKEDITOR
.dtd
;
1580 // Inserts the given (valid) HTML into the range position (with range content deleted),
1581 // guarantee it's result to be a valid DOM tree.
1582 function insert( editable
, type
, data
, range
) {
1583 var editor
= editable
.editor
,
1586 if ( type
== 'unfiltered_html' ) {
1591 // Check range spans in non-editable.
1592 if ( range
.checkReadOnly() )
1595 // RANGE PREPARATIONS
1597 var path
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, range
.root
),
1598 // Let root be the nearest block that's impossible to be split
1599 // during html processing.
1600 blockLimit
= path
.blockLimit
|| range
.root
,
1601 // The "state" value.
1604 dontFilter: dontFilter
,
1608 blockLimit: blockLimit
,
1609 // During pre-processing / preparations startContainer of affectedRange should be placed
1610 // in this element in which inserted or moved (in case when we merge blocks) content
1611 // could create situation that will need merging inline elements.
1613 // <div><b>A</b>^B</div> + <b>C</b> => <div><b>A</b><b>C</b>B</div> - affected container is <div>.
1614 // <p><b>A[B</b></p><p><b>C]D</b></p> + E => <p><b>AE</b></p><p><b>D</b></p> =>
1615 // <p><b>AE</b><b>D</b></p> - affected container is <p> (in text mode).
1616 mergeCandidates: [],
1620 prepareRangeToDataInsertion( that
);
1624 // Select range and stop execution.
1625 // If data has been totally emptied after the filtering,
1626 // any insertion is pointless (#10339).
1627 if ( data
&& processDataForInsertion( that
, data
) ) {
1629 insertDataIntoRange( that
);
1633 // Set final range position and clean up.
1635 cleanupAfterInsertion( that
);
1638 // Prepare range to its data deletion.
1639 // Delete its contents.
1640 // Prepare it to insertion.
1641 function prepareRangeToDataInsertion( that
) {
1642 var range
= that
.range
,
1643 mergeCandidates
= that
.mergeCandidates
,
1644 node
, marker
, path
, startPath
, endPath
, previous
, bm
;
1646 // If range starts in inline element then insert a marker, so empty
1647 // inline elements won't be removed while range.deleteContents
1648 // and we will be able to move range back into this element.
1649 // E.g. 'aa<b>[bb</b>]cc' -> (after deleting) 'aa<b><span/></b>cc'
1650 if ( that
.type
== 'text' && range
.shrink( CKEDITOR
.SHRINK_ELEMENT
, true, false ) ) {
1651 marker
= CKEDITOR
.dom
.element
.createFromHtml( '<span> </span>', range
.document
);
1652 range
.insertNode( marker
);
1653 range
.setStartAfter( marker
);
1656 // By using path we can recover in which element was startContainer
1657 // before deleting contents.
1658 // Start and endPathElements will be used to squash selected blocks, after removing
1659 // selection contents. See rule 5.
1660 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
);
1661 that
.endPath
= endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
);
1663 if ( !range
.collapsed
) {
1664 // Anticipate the possibly empty block at the end of range after deletion.
1665 node
= endPath
.block
|| endPath
.blockLimit
;
1666 var ancestor
= range
.getCommonAncestor();
1667 if ( node
&& !( node
.equals( ancestor
) || node
.contains( ancestor
) ) && range
.checkEndOfBlock() ) {
1668 that
.zombies
.push( node
);
1671 range
.deleteContents();
1675 // Move range into the previous block.
1677 ( previous
= getRangePrevious( range
) ) && checkIfElement( previous
) && previous
.isBlockBoundary() &&
1678 // Check if previousNode was parent of range's startContainer before deleteContents.
1679 startPath
.contains( previous
)
1681 range
.moveToPosition( previous
, CKEDITOR
.POSITION_BEFORE_END
);
1684 mergeAncestorElementsOfSelectionEnds( range
, that
.blockLimit
, startPath
, endPath
);
1688 // If marker was created then move collapsed range into its place.
1689 range
.setEndBefore( marker
);
1694 // Split inline elements so HTML will be inserted with its own styles.
1695 path
= range
.startPath();
1696 if ( ( node
= path
.contains( isInline
, false, 1 ) ) ) {
1697 range
.splitElement( node
);
1698 that
.inlineStylesRoot
= node
;
1699 that
.inlineStylesPeak
= path
.lastElement
;
1702 // Record inline merging candidates for later cleanup in place.
1703 bm
= range
.createBookmark();
1705 // 1. Inline siblings.
1706 node
= bm
.startNode
.getPrevious( isNotEmpty
);
1707 node
&& checkIfElement( node
) && isInline( node
) && mergeCandidates
.push( node
);
1708 node
= bm
.startNode
.getNext( isNotEmpty
);
1709 node
&& checkIfElement( node
) && isInline( node
) && mergeCandidates
.push( node
);
1711 // 2. Inline parents.
1712 node
= bm
.startNode
;
1713 while ( ( node
= node
.getParent() ) && isInline( node
) )
1714 mergeCandidates
.push( node
);
1716 range
.moveToBookmark( bm
);
1719 function processDataForInsertion( that
, data
) {
1720 var range
= that
.range
;
1722 // Rule 8. - wrap entire data in inline styles.
1723 // (e.g. <p><b>x^z</b></p> + <p>a</p><p>b</p> -> <b><p>a</p><p>b</p></b>)
1724 // Incorrect tags order will be fixed by htmlDataProcessor.
1725 if ( that
.type
== 'text' && that
.inlineStylesRoot
)
1726 data
= wrapDataWithInlineStyles( data
, that
);
1729 var context
= that
.blockLimit
.getName();
1731 // Wrap data to be inserted, to avoid losing leading whitespaces
1732 // when going through the below procedure.
1733 if ( /^\s+|\s+$/.test( data
) && 'span' in CKEDITOR
.dtd
[ context
] ) {
1734 var protect
= '<span data-cke-marker="1"> </span>';
1735 data
= protect
+ data
+ protect
;
1738 // Process the inserted html, in context of the insertion root.
1739 // Don't use the "fix for body" feature as auto paragraphing must
1740 // be handled during insertion.
1741 data
= that
.editor
.dataProcessor
.toHtml( data
, {
1744 protectedWhitespaces: !!protect
,
1745 dontFilter: that
.dontFilter
,
1746 // Use the current, contextual settings.
1747 filter: that
.editor
.activeFilter
,
1748 enterMode: that
.editor
.activeEnterMode
1752 // Build the node list for insertion.
1753 var doc
= range
.document
,
1754 wrapper
= doc
.createElement( 'body' );
1756 wrapper
.setHtml( data
);
1758 // Eventually remove the temporaries.
1760 wrapper
.getFirst().remove();
1761 wrapper
.getLast().remove();
1765 var block
= range
.startPath().block
;
1766 if ( block
&& // Apply when there exists path block after deleting selection's content...
1767 !( block
.getChildCount() == 1 && block
.getBogus() ) ) { // ... and the only content of this block isn't a bogus.
1768 stripBlockTagIfSingleLine( wrapper
);
1771 that
.dataWrapper
= wrapper
;
1776 function insertDataIntoRange( that
) {
1777 var range
= that
.range
,
1778 doc
= range
.document
,
1780 blockLimit
= that
.blockLimit
,
1781 nodesData
, nodeData
, node
,
1784 bogusNeededBlocks
= [],
1785 pathBlock
, fixBlock
,
1786 splittingContainer
= 0,
1788 insertionContainer
, toSplit
, newContainer
,
1789 startContainer
= range
.startContainer
,
1790 endContainer
= that
.endPath
.elements
[ 0 ],
1792 // If endContainer was merged into startContainer: <p>a[b</p><p>c]d</p>
1793 // or it's equal to startContainer: <p>a^b</p>
1794 // or different situation happened :P
1795 // then there's no separate container for the end of selection.
1796 pos
= endContainer
.getPosition( startContainer
),
1797 separateEndContainer
= !!endContainer
.getCommonAncestor( startContainer
) && // endC is not detached.
1798 pos
!= CKEDITOR
.POSITION_IDENTICAL
&& !( pos
& CKEDITOR
.POSITION_CONTAINS
+ CKEDITOR
.POSITION_IS_CONTAINED
); // endC & endS are in separate branches.
1800 nodesData
= extractNodesData( that
.dataWrapper
, that
);
1802 removeBrsAdjacentToPastedBlocks( nodesData
, range
);
1804 for ( ; nodeIndex
< nodesData
.length
; nodeIndex
++ ) {
1805 nodeData
= nodesData
[ nodeIndex
];
1807 // Ignore trailing <brs>
1808 if ( nodeData
.isLineBreak
&& splitOnLineBreak( range
, blockLimit
, nodeData
) ) {
1809 // Do not move caret towards the text (in cleanupAfterInsertion),
1810 // because caret was placed after a line break.
1811 dontMoveCaret
= nodeIndex
> 0;
1815 path
= range
.startPath();
1817 // Auto paragraphing.
1818 if ( !nodeData
.isBlock
&& shouldAutoParagraph( that
.editor
, path
.block
, path
.blockLimit
) && ( fixBlock
= autoParagraphTag( that
.editor
) ) ) {
1819 fixBlock
= doc
.createElement( fixBlock
);
1820 fixBlock
.appendBogus();
1821 range
.insertNode( fixBlock
);
1822 if ( CKEDITOR
.env
.needsBrFiller
&& ( bogus
= fixBlock
.getBogus() ) )
1824 range
.moveToPosition( fixBlock
, CKEDITOR
.POSITION_BEFORE_END
);
1827 node
= range
.startPath().block
;
1829 // Remove any bogus element on the current path block for now, and mark
1830 // it for later compensation.
1831 if ( node
&& !node
.equals( pathBlock
) ) {
1832 bogus
= node
.getBogus();
1835 bogusNeededBlocks
.push( node
);
1841 // First not allowed node reached - start splitting original container
1842 if ( nodeData
.firstNotAllowed
)
1843 splittingContainer
= 1;
1845 if ( splittingContainer
&& nodeData
.isElement
) {
1846 insertionContainer
= range
.startContainer
;
1849 // Find the first ancestor that can contain current node.
1850 // This one won't be split.
1851 while ( insertionContainer
&& !DTD
[ insertionContainer
.getName() ][ nodeData
.name
] ) {
1852 if ( insertionContainer
.equals( blockLimit
) ) {
1853 insertionContainer
= null;
1857 toSplit
= insertionContainer
;
1858 insertionContainer
= insertionContainer
.getParent();
1861 // If split has to be done - do it and mark both ends as a possible zombies.
1862 if ( insertionContainer
) {
1864 newContainer
= range
.splitElement( toSplit
);
1865 that
.zombies
.push( newContainer
);
1866 that
.zombies
.push( toSplit
);
1869 // Unable to make the insertion happen in place, resort to the content filter.
1871 // If everything worked fine insertionContainer == blockLimit here.
1872 filteredNodes
= filterElement( nodeData
.node
, blockLimit
.getName(), !nodeIndex
, nodeIndex
== nodesData
.length
- 1 );
1876 if ( filteredNodes
) {
1877 while ( ( node
= filteredNodes
.pop() ) )
1878 range
.insertNode( node
);
1881 // Insert current node at the start of range.
1882 range
.insertNode( nodeData
.node
);
1885 // Move range to the endContainer for the final allowed elements.
1886 if ( nodeData
.lastNotAllowed
&& nodeIndex
< nodesData
.length
- 1 ) {
1887 // If separateEndContainer exists move range there.
1888 // Otherwise try to move range to container created during splitting.
1889 // If this doesn't work - don't move range.
1890 newContainer
= separateEndContainer
? endContainer : newContainer
;
1891 newContainer
&& range
.setEndAt( newContainer
, CKEDITOR
.POSITION_AFTER_START
);
1892 splittingContainer
= 0;
1895 // Collapse range after insertion to end.
1899 // Rule 9. Non-editable content should be selected as a whole.
1900 if ( isSingleNonEditableElement( nodesData
) ) {
1901 dontMoveCaret
= true;
1902 node
= nodesData
[ 0 ].node
;
1903 range
.setStartAt( node
, CKEDITOR
.POSITION_BEFORE_START
);
1904 range
.setEndAt( node
, CKEDITOR
.POSITION_AFTER_END
);
1907 that
.dontMoveCaret
= dontMoveCaret
;
1908 that
.bogusNeededBlocks
= bogusNeededBlocks
;
1911 function cleanupAfterInsertion( that
) {
1912 var range
= that
.range
,
1913 node
, testRange
, movedIntoInline
,
1914 bogusNeededBlocks
= that
.bogusNeededBlocks
,
1915 // Create a bookmark to defend against the following range deconstructing operations.
1916 bm
= range
.createBookmark();
1918 // Remove all elements that could be created while splitting nodes
1919 // with ranges at its start|end.
1920 // E.g. remove <div><p></p></div>
1921 // But not <div><p> </p></div>
1922 // And replace <div><p><span data="cke-bookmark"/></p></div> with found bookmark.
1923 while ( ( node
= that
.zombies
.pop() ) ) {
1924 // Detached element.
1925 if ( !node
.getParent() )
1928 testRange
= range
.clone();
1929 testRange
.moveToElementEditStart( node
);
1930 testRange
.removeEmptyBlocksAtEnd();
1933 if ( bogusNeededBlocks
) {
1934 // Bring back all block bogus nodes.
1935 while ( ( node
= bogusNeededBlocks
.pop() ) ) {
1936 if ( CKEDITOR
.env
.needsBrFiller
)
1939 node
.append( range
.document
.createText( '\u00a0' ) );
1943 // Eventually merge identical inline elements.
1944 while ( ( node
= that
.mergeCandidates
.pop() ) )
1945 node
.mergeSiblings();
1947 range
.moveToBookmark( bm
);
1950 // Shrink range to the BEFOREEND of previous innermost editable node in source order.
1952 if ( !that
.dontMoveCaret
) {
1953 node
= getRangePrevious( range
);
1955 while ( node
&& checkIfElement( node
) && !node
.is( DTD
.$empty
) ) {
1956 if ( node
.isBlockBoundary() )
1957 range
.moveToPosition( node
, CKEDITOR
.POSITION_BEFORE_END
);
1959 // Don't move into inline element (which ends with a text node)
1960 // found which contains white-space at its end.
1961 // If not - move range's end to the end of this element.
1962 if ( isInline( node
) && node
.getHtml().match( /(\s| )$/g ) ) {
1963 movedIntoInline
= null;
1967 movedIntoInline
= range
.clone();
1968 movedIntoInline
.moveToPosition( node
, CKEDITOR
.POSITION_BEFORE_END
);
1971 node
= node
.getLast( isNotEmpty
);
1974 movedIntoInline
&& range
.moveToRange( movedIntoInline
);
1980 // HELPERS ------------------------------------------------------------
1983 function checkIfElement( node
) {
1984 return node
.type
== CKEDITOR
.NODE_ELEMENT
;
1987 function extractNodesData( dataWrapper
, that
) {
1988 var node
, sibling
, nodeName
, allowed
,
1990 startContainer
= that
.range
.startContainer
,
1991 path
= that
.range
.startPath(),
1992 allowedNames
= DTD
[ startContainer
.getName() ],
1994 nodesList
= dataWrapper
.getChildren(),
1995 nodesCount
= nodesList
.count(),
1996 firstNotAllowed
= -1,
1997 lastNotAllowed
= -1,
2001 // Selection start within a list.
2002 var insideOfList
= path
.contains( DTD
.$list
);
2004 for ( ; nodeIndex
< nodesCount
; ++nodeIndex
) {
2005 node
= nodesList
.getItem( nodeIndex
);
2007 if ( checkIfElement( node
) ) {
2008 nodeName
= node
.getName();
2010 // Extract only the list items, when insertion happens
2011 // inside of a list, reads as rearrange list items. (#7957)
2012 if ( insideOfList
&& nodeName
in CKEDITOR
.dtd
.$list
) {
2013 nodesData
= nodesData
.concat( extractNodesData( node
, that
) );
2017 allowed
= !!allowedNames
[ nodeName
];
2019 // Mark <brs data-cke-eol="1"> at the beginning and at the end.
2020 if ( nodeName
== 'br' && node
.data( 'cke-eol' ) && ( !nodeIndex
|| nodeIndex
== nodesCount
- 1 ) ) {
2021 sibling
= nodeIndex
? nodesData
[ nodeIndex
- 1 ].node : nodesList
.getItem( nodeIndex
+ 1 );
2023 // Line break has to have sibling which is not an <br>.
2024 lineBreak
= sibling
&& ( !checkIfElement( sibling
) || !sibling
.is( 'br' ) );
2025 // Line break has block element as a sibling.
2026 blockSibling
= sibling
&& checkIfElement( sibling
) && DTD
.$block
[ sibling
.getName() ];
2029 if ( firstNotAllowed
== -1 && !allowed
)
2030 firstNotAllowed
= nodeIndex
;
2032 lastNotAllowed
= nodeIndex
;
2036 isLineBreak: lineBreak
,
2037 isBlock: node
.isBlockBoundary(),
2038 hasBlockSibling: blockSibling
,
2047 nodesData
.push( { isElement: 0, node: node
, allowed: 1 } );
2051 // Mark first node that cannot be inserted directly into startContainer
2052 // and last node for which startContainer has to be split.
2053 if ( firstNotAllowed
> -1 )
2054 nodesData
[ firstNotAllowed
].firstNotAllowed
= 1;
2055 if ( lastNotAllowed
> -1 )
2056 nodesData
[ lastNotAllowed
].lastNotAllowed
= 1;
2061 // TODO: Review content transformation rules on filtering element.
2062 function filterElement( element
, parentName
, isFirst
, isLast
) {
2063 var nodes
= filterElementInner( element
, parentName
),
2065 nodesCount
= nodes
.length
,
2069 lastSpaceIndex
= -1;
2071 // Remove duplicated spaces and spaces at the:
2072 // * beginnig if filtered element isFirst (isFirst that's going to be inserted)
2073 // * end if filtered element isLast.
2074 for ( ; nodeIndex
< nodesCount
; nodeIndex
++ ) {
2075 node
= nodes
[ nodeIndex
];
2077 if ( node
== ' ' ) {
2078 // Don't push doubled space and if it's leading space for insertion.
2079 if ( !afterSpace
&& !( isFirst
&& !nodeIndex
) ) {
2080 nodes2
.push( new CKEDITOR
.dom
.text( ' ' ) );
2081 lastSpaceIndex
= nodes2
.length
;
2085 nodes2
.push( node
);
2090 // Remove trailing space.
2091 if ( isLast
&& lastSpaceIndex
== nodes2
.length
)
2097 function filterElementInner( element
, parentName
) {
2099 children
= element
.getChildren(),
2100 childrenCount
= children
.count(),
2103 allowedNames
= DTD
[ parentName
],
2104 surroundBySpaces
= !element
.is( DTD
.$inline
) || element
.is( 'br' );
2106 if ( surroundBySpaces
)
2109 for ( ; childIndex
< childrenCount
; childIndex
++ ) {
2110 child
= children
.getItem( childIndex
);
2112 if ( checkIfElement( child
) && !child
.is( allowedNames
) )
2113 nodes
= nodes
.concat( filterElementInner( child
, parentName
) );
2115 nodes
.push( child
);
2118 if ( surroundBySpaces
)
2124 function getRangePrevious( range
) {
2125 return checkIfElement( range
.startContainer
) && range
.startContainer
.getChild( range
.startOffset
- 1 );
2128 function isInline( node
) {
2129 return node
&& checkIfElement( node
) && ( node
.is( DTD
.$removeEmpty
) || node
.is( 'a' ) && !node
.isBlockBoundary() );
2132 // Checks if only non-editable element is being inserted.
2133 function isSingleNonEditableElement( nodesData
) {
2134 if ( nodesData
.length
!= 1 )
2137 var nodeData
= nodesData
[ 0 ];
2139 return nodeData
.isElement
&& ( nodeData
.node
.getAttribute( 'contenteditable' ) == 'false' );
2142 var blockMergedTags
= { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, li: 1, pre: 1, dl: 1, blockquote: 1 };
2144 // See rule 5. in TCs.
2145 // Initial situation:
2146 // <ul><li>AA^</li></ul><ul><li>BB</li></ul>
2147 // We're looking for 2nd <ul>, comparing with 1st <ul> and merging.
2148 // We're not merging if caret is between these elements.
2149 function mergeAncestorElementsOfSelectionEnds( range
, blockLimit
, startPath
, endPath
) {
2150 var walkerRange
= range
.clone(),
2151 walker
, nextNode
, previousNode
;
2153 walkerRange
.setEndAt( blockLimit
, CKEDITOR
.POSITION_BEFORE_END
);
2154 walker
= new CKEDITOR
.dom
.walker( walkerRange
);
2156 if ( ( nextNode
= walker
.next() ) && // Find next source node
2157 checkIfElement( nextNode
) && // which is an element
2158 blockMergedTags
[ nextNode
.getName() ] && // that can be merged.
2159 ( previousNode
= nextNode
.getPrevious() ) && // Take previous one
2160 checkIfElement( previousNode
) && // which also has to be an element.
2161 !previousNode
.getParent().equals( range
.startContainer
) && // Fail if caret is on the same level.
2162 // This means that caret is between these nodes.
2163 startPath
.contains( previousNode
) && // Elements path of start of selection has
2164 endPath
.contains( nextNode
) && // to contain prevNode and vice versa.
2165 nextNode
.isIdentical( previousNode
) // Check if elements are identical.
2167 // Merge blocks and repeat.
2168 nextNode
.moveChildren( previousNode
);
2170 mergeAncestorElementsOfSelectionEnds( range
, blockLimit
, startPath
, endPath
);
2174 // If last node that will be inserted is a block (but not a <br>)
2175 // and it will be inserted right before <br> remove this <br>.
2176 // Do the same for the first element that will be inserted and preceding <br>.
2177 function removeBrsAdjacentToPastedBlocks( nodesData
, range
) {
2178 var succeedingNode
= range
.endContainer
.getChild( range
.endOffset
),
2179 precedingNode
= range
.endContainer
.getChild( range
.endOffset
- 1 );
2181 if ( succeedingNode
)
2182 remove( succeedingNode
, nodesData
[ nodesData
.length
- 1 ] );
2184 if ( precedingNode
&& remove( precedingNode
, nodesData
[ 0 ] ) ) {
2185 // If preceding <br> was removed - move range left.
2186 range
.setEnd( range
.endContainer
, range
.endOffset
- 1 );
2190 function remove( maybeBr
, maybeBlockData
) {
2191 if ( maybeBlockData
.isBlock
&& maybeBlockData
.isElement
&& !maybeBlockData
.node
.is( 'br' ) &&
2192 checkIfElement( maybeBr
) && maybeBr
.is( 'br' ) ) {
2199 // Return 1 if <br> should be skipped when inserting, 0 otherwise.
2200 function splitOnLineBreak( range
, blockLimit
, nodeData
) {
2201 var firstBlockAscendant
, pos
;
2203 if ( nodeData
.hasBlockSibling
)
2206 firstBlockAscendant
= range
.startContainer
.getAscendant( DTD
.$block
, 1 );
2207 if ( !firstBlockAscendant
|| !firstBlockAscendant
.is( { div: 1, p: 1 } ) )
2210 pos
= firstBlockAscendant
.getPosition( blockLimit
);
2212 if ( pos
== CKEDITOR
.POSITION_IDENTICAL
|| pos
== CKEDITOR
.POSITION_CONTAINS
)
2215 var newContainer
= range
.splitElement( firstBlockAscendant
);
2216 range
.moveToPosition( newContainer
, CKEDITOR
.POSITION_AFTER_START
);
2221 var stripSingleBlockTags
= { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1 },
2222 inlineButNotBr
= CKEDITOR
.tools
.extend( {}, DTD
.$inline
);
2223 delete inlineButNotBr
.br
;
2226 function stripBlockTagIfSingleLine( dataWrapper
) {
2227 var block
, children
;
2229 if ( dataWrapper
.getChildCount() == 1 && // Only one node bein inserted.
2230 checkIfElement( block
= dataWrapper
.getFirst() ) && // And it's an element.
2231 block
.is( stripSingleBlockTags
) && // That's <p> or <div> or header.
2232 !block
.hasAttribute( 'contenteditable' ) // It's not a non-editable block or nested editable.
2234 // Check children not containing block.
2235 children
= block
.getElementsByTag( '*' );
2236 for ( var i
= 0, child
, count
= children
.count(); i
< count
; i
++ ) {
2237 child
= children
.getItem( i
);
2238 if ( !child
.is( inlineButNotBr
) )
2242 block
.moveChildren( block
.getParent( 1 ) );
2247 function wrapDataWithInlineStyles( data
, that
) {
2248 var element
= that
.inlineStylesPeak
,
2249 doc
= element
.getDocument(),
2250 wrapper
= doc
.createText( '{cke-peak}' ),
2251 limit
= that
.inlineStylesRoot
.getParent();
2253 while ( !element
.equals( limit
) ) {
2254 wrapper
= wrapper
.appendTo( element
.clone() );
2255 element
= element
.getParent();
2258 // Don't use String.replace because it fails in IE7 if special replacement
2259 // characters ($$, $&, etc.) are in data (#10367).
2260 return wrapper
.getOuterHtml().split( '{cke-peak}' ).join( data
);
2266 function afterInsert( editable
) {
2267 var editor
= editable
.editor
;
2269 // Scroll using selection, not ranges, to affect native pastes.
2270 editor
.getSelection().scrollIntoView();
2272 // Save snaps after the whole execution completed.
2273 // This's a workaround for make DOM modification's happened after
2274 // 'insertElement' to be included either, e.g. Form-based dialogs' 'commitContents'
2276 setTimeout( function() {
2277 editor
.fire( 'saveSnapshot' );
2281 // 1. Fixes a range which is a result of deleteContents() and is placed in an intermediate element (see dtd.$intermediate),
2282 // inside a table. A goal is to find a closest <td> or <th> element and when this fails, recreate the structure of the table.
2283 // 2. Fixes empty cells by appending bogus <br>s or deleting empty text nodes in IE<=8 case.
2284 fixTableAfterContentsDeletion
= ( function() {
2285 // Creates an element walker which can only "go deeper". It won't
2286 // move out from any element. Therefore it can be used to find <td>x</td> in cases like:
2287 // <table><tbody><tr><td>x</td></tr></tbody>^<tfoot>...
2288 function getFixTableSelectionWalker( testRange
) {
2289 var walker
= new CKEDITOR
.dom
.walker( testRange
);
2290 walker
.guard = function( node
, isMovingOut
) {
2293 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
)
2294 return node
.is( CKEDITOR
.dtd
.$tableContent
);
2296 walker
.evaluator = function( node
) {
2297 return node
.type
== CKEDITOR
.NODE_ELEMENT
;
2303 function fixTableStructure( element
, newElementName
, appendToStart
) {
2304 var temp
= element
.getDocument().createElement( newElementName
);
2305 element
.append( temp
, appendToStart
);
2309 // Fix empty cells. This means:
2310 // * add bogus <br> if browser needs it
2311 // * remove empty text nodes on IE8, because it will crash (http://dev.ckeditor.com/ticket/11183#comment:8).
2312 function fixEmptyCells( cells
) {
2313 var i
= cells
.count(),
2316 for ( i
; i
-- > 0; ) {
2317 cell
= cells
.getItem( i
);
2319 if ( !CKEDITOR
.tools
.trim( cell
.getHtml() ) ) {
2321 if ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 9 && cell
.getChildCount() )
2322 cell
.getFirst().remove();
2327 return function( range
) {
2328 var container
= range
.startContainer
,
2329 table
= container
.getAscendant( 'table', 1 ),
2332 appendToStart
= false;
2334 fixEmptyCells( table
.getElementsByTag( 'td' ) );
2335 fixEmptyCells( table
.getElementsByTag( 'th' ) );
2338 testRange
= range
.clone();
2339 testRange
.setStart( container
, 0 );
2340 deeperSibling
= getFixTableSelectionWalker( testRange
).lastBackward();
2342 // If left is empty, look right.
2343 if ( !deeperSibling
) {
2344 testRange
= range
.clone();
2345 testRange
.setEndAt( container
, CKEDITOR
.POSITION_BEFORE_END
);
2346 deeperSibling
= getFixTableSelectionWalker( testRange
).lastForward();
2347 appendToStart
= true;
2350 // If there's no deeper nested element in both direction - container is empty - we'll use it then.
2351 if ( !deeperSibling
)
2352 deeperSibling
= container
;
2356 // We found a table what means that it's empty - remove it completely.
2357 if ( deeperSibling
.is( 'table' ) ) {
2358 range
.setStartAt( deeperSibling
, CKEDITOR
.POSITION_BEFORE_START
);
2359 range
.collapse( true );
2360 deeperSibling
.remove();
2364 // Found an empty txxx element - append tr.
2365 if ( deeperSibling
.is( { tbody: 1, thead: 1, tfoot: 1 } ) )
2366 deeperSibling
= fixTableStructure( deeperSibling
, 'tr', appendToStart
);
2368 // Found an empty tr element - append td/th.
2369 if ( deeperSibling
.is( 'tr' ) )
2370 deeperSibling
= fixTableStructure( deeperSibling
, deeperSibling
.getParent().is( 'thead' ) ? 'th' : 'td', appendToStart
);
2372 // To avoid setting selection after bogus, remove it from the current cell.
2373 // We can safely do that, because we'll insert element into that cell.
2374 var bogus
= deeperSibling
.getBogus();
2378 range
.moveToPosition( deeperSibling
, appendToStart
? CKEDITOR
.POSITION_AFTER_START : CKEDITOR
.POSITION_BEFORE_END
);
2382 fixListAfterContentsDelete
= ( function() {
2383 // Creates an element walker which operates only within lists.
2384 function getFixListSelectionWalker( testRange
) {
2385 var walker
= new CKEDITOR
.dom
.walker( testRange
);
2386 walker
.guard = function( node
, isMovingOut
) {
2389 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
)
2390 return node
.is( CKEDITOR
.dtd
.$list
) || node
.is( CKEDITOR
.dtd
.$listItem
);
2392 walker
.evaluator = function( node
) {
2393 return node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( CKEDITOR
.dtd
.$listItem
);
2399 return function( range
) {
2400 var container
= range
.startContainer
,
2401 appendToStart
= false,
2406 testRange
= range
.clone();
2407 testRange
.setStart( container
, 0 );
2408 deeperSibling
= getFixListSelectionWalker( testRange
).lastBackward();
2410 // If left is empty, look right.
2411 if ( !deeperSibling
) {
2412 testRange
= range
.clone();
2413 testRange
.setEndAt( container
, CKEDITOR
.POSITION_BEFORE_END
);
2414 deeperSibling
= getFixListSelectionWalker( testRange
).lastForward();
2415 appendToStart
= true;
2418 // If there's no deeper nested element in both direction - container is empty - we'll use it then.
2419 if ( !deeperSibling
)
2420 deeperSibling
= container
;
2422 // We found a list what means that it's empty - remove it completely.
2423 if ( deeperSibling
.is( CKEDITOR
.dtd
.$list
) ) {
2424 range
.setStartAt( deeperSibling
, CKEDITOR
.POSITION_BEFORE_START
);
2425 range
.collapse( true );
2426 deeperSibling
.remove();
2430 // To avoid setting selection after bogus, remove it from the target list item.
2431 // We can safely do that, because we'll insert element into that cell.
2432 var bogus
= deeperSibling
.getBogus();
2436 range
.moveToPosition( deeperSibling
, appendToStart
? CKEDITOR
.POSITION_AFTER_START : CKEDITOR
.POSITION_BEFORE_END
);
2441 function mergeBlocksCollapsedSelection( editor
, range
, backspace
, startPath
) {
2442 var startBlock
= startPath
.block
;
2444 // Selection must be collapsed and to be anchored in a block.
2448 // Exclude cases where, i.e. if pressed arrow key, selection
2449 // would move within the same block (merge inside a block).
2450 if ( !range
[ backspace
? 'checkStartOfBlock' : 'checkEndOfBlock' ]() )
2453 // Make sure, there's an editable position to put selection,
2454 // which i.e. would be used if pressed arrow key, but abort
2455 // if such position exists but means a selected non-editable element.
2456 if ( !range
.moveToClosestEditablePosition( startBlock
, !backspace
) || !range
.collapsed
)
2459 // Handle special case, when block's sibling is a <hr>. Delete it and keep selection
2460 // in the same place (http://dev.ckeditor.com/ticket/11861#comment:9).
2461 if ( range
.startContainer
.type
== CKEDITOR
.NODE_ELEMENT
) {
2462 var touched
= range
.startContainer
.getChild( range
.startOffset
- ( backspace
? 1 : 0 ) );
2463 if ( touched
&& touched
.type
== CKEDITOR
.NODE_ELEMENT
&& touched
.is( 'hr' ) ) {
2464 editor
.fire( 'saveSnapshot' );
2470 var siblingBlock
= range
.startPath().block
;
2472 // Abort if an editable position exists, but either it's not
2473 // in a block or that block is the parent of the start block
2474 // (merging child into parent).
2475 if ( !siblingBlock
|| ( siblingBlock
&& siblingBlock
.contains( startBlock
) ) )
2478 editor
.fire( 'saveSnapshot' );
2480 // Remove bogus to avoid duplicated boguses.
2482 if ( ( bogus
= ( backspace
? siblingBlock : startBlock
).getBogus() ) )
2485 // Save selection. It will be restored.
2486 var selection
= editor
.getSelection(),
2487 bookmarks
= selection
.createBookmarks();
2490 ( backspace
? startBlock : siblingBlock
).moveChildren( backspace
? siblingBlock : startBlock
, false );
2492 // Also merge children along with parents.
2493 startPath
.lastElement
.mergeSiblings();
2495 // Cut off removable branch of the DOM tree.
2496 pruneEmptyDisjointAncestors( startBlock
, siblingBlock
, !backspace
);
2498 // Restore selection.
2499 selection
.selectBookmarks( bookmarks
);
2504 function mergeBlocksNonCollapsedSelection( editor
, range
, startPath
) {
2505 var startBlock
= startPath
.block
,
2506 endPath
= range
.endPath(),
2507 endBlock
= endPath
.block
;
2509 // Selection must be anchored in two different blocks.
2510 if ( !startBlock
|| !endBlock
|| startBlock
.equals( endBlock
) )
2513 editor
.fire( 'saveSnapshot' );
2515 // Remove bogus to avoid duplicated boguses.
2517 if ( ( bogus
= startBlock
.getBogus() ) )
2520 // Changing end container to element from text node (#12503).
2521 range
.enlarge( CKEDITOR
.ENLARGE_INLINE
);
2523 // Delete range contents. Do NOT merge. Merging is weird.
2524 range
.deleteContents();
2526 // If something has left of the block to be merged, clean it up.
2527 // It may happen when merging with list items.
2528 if ( endBlock
.getParent() ) {
2529 // Move children to the first block.
2530 endBlock
.moveChildren( startBlock
, false );
2532 // ...and merge them if that's possible.
2533 startPath
.lastElement
.mergeSiblings();
2535 // If expanded selection, things are always merged like with BACKSPACE.
2536 pruneEmptyDisjointAncestors( startBlock
, endBlock
, true );
2539 // Make sure the result selection is collapsed.
2540 range
= editor
.getSelection().getRanges()[ 0 ];
2541 range
.collapse( 1 );
2543 // Optimizing range containers from text nodes to elements (#12503).
2545 if ( range
.startContainer
.getHtml() === '' ) {
2546 range
.startContainer
.appendBogus();
2554 // Finds the innermost child of common parent, which,
2555 // if removed, removes nothing but the contents of the element.
2557 // before: <div><p><strong>first</strong></p><p>second</p></div>
2558 // after: <div><p>second</p></div>
2560 // before: <div><p>x<strong>first</strong></p><p>second</p></div>
2561 // after: <div><p>x</p><p>second</p></div>
2563 // isPruneToEnd=true
2564 // before: <div><p><strong>first</strong></p><p>second</p></div>
2565 // after: <div><p><strong>first</strong></p></div>
2567 // @param {CKEDITOR.dom.element} first
2568 // @param {CKEDITOR.dom.element} second
2569 // @param {Boolean} isPruneToEnd
2570 function pruneEmptyDisjointAncestors( first
, second
, isPruneToEnd
) {
2571 var commonParent
= first
.getCommonAncestor( second
),
2572 node
= isPruneToEnd
? second : first
,
2573 removableParent
= node
;
2575 while ( ( node
= node
.getParent() ) && !commonParent
.equals( node
) && node
.getChildCount() == 1 )
2576 removableParent
= node
;
2578 removableParent
.remove();
2582 // Helpers for editable.getHtmlFromRange.
2584 getHtmlFromRangeHelpers
= {
2586 detect: function( that
, editable
) {
2587 var range
= that
.range
,
2588 rangeStart
= range
.clone(),
2589 rangeEnd
= range
.clone(),
2591 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, editable
),
2592 endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
, editable
);
2594 // Note: checkBoundaryOfElement will not work on original range as CKEDITOR.START|END
2595 // means that range start|end must be literally anchored at block start|end, e.g.
2597 // <p>a{</p><p>}b</p>
2599 // will return false for both paragraphs but two similar ranges
2601 // <p>a{}</p><p>{}b</p>
2603 // will return true if checked separately.
2604 rangeStart
.collapse( 1 );
2605 rangeEnd
.collapse();
2607 if ( startPath
.block
&& rangeStart
.checkBoundaryOfElement( startPath
.block
, CKEDITOR
.END
) ) {
2608 range
.setStartAfter( startPath
.block
);
2609 that
.prependEolBr
= 1;
2612 if ( endPath
.block
&& rangeEnd
.checkBoundaryOfElement( endPath
.block
, CKEDITOR
.START
) ) {
2613 range
.setEndBefore( endPath
.block
);
2614 that
.appendEolBr
= 1;
2618 fix: function( that
, editable
) {
2619 var doc
= editable
.getDocument(),
2622 // Append <br data-cke-eol="1"> to the fragment.
2623 if ( that
.appendEolBr
) {
2624 appended
= this.createEolBr( doc
);
2625 that
.fragment
.append( appended
);
2628 // Prepend <br data-cke-eol="1"> to the fragment but avoid duplicates. Such
2629 // elements should never follow each other in DOM.
2630 if ( that
.prependEolBr
&& ( !appended
|| appended
.getPrevious() ) ) {
2631 that
.fragment
.append( this.createEolBr( doc
), 1 );
2635 createEolBr: function( doc
) {
2636 return doc
.createElement( 'br', {
2645 exclude: function( that
) {
2646 var boundaryNodes
= that
.range
.getBoundaryNodes(),
2647 startNode
= boundaryNodes
.startNode
,
2648 endNode
= boundaryNodes
.endNode
;
2650 // If bogus is the last node in range but not the only node, exclude it.
2651 if ( endNode
&& isBogus( endNode
) && ( !startNode
|| !startNode
.equals( endNode
) ) )
2652 that
.range
.setEndBefore( endNode
);
2657 rebuild: function( that
, editable
) {
2658 var range
= that
.range
,
2659 node
= range
.getCommonAncestor(),
2661 // A path relative to the common ancestor.
2662 commonPath
= new CKEDITOR
.dom
.elementPath( node
, editable
),
2663 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, editable
),
2664 endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
, editable
),
2667 if ( node
.type
== CKEDITOR
.NODE_TEXT
)
2668 node
= node
.getParent();
2670 // Fix DOM of partially enclosed tables
2671 // <table><tbody><tr><td>a{b</td><td>c}d</td></tr></tbody></table>
2672 // Full table is returned
2673 // <table><tbody><tr><td>b</td><td>c</td></tr></tbody></table>
2675 // <td>b</td><td>c</td>
2676 if ( commonPath
.blockLimit
.is( { tr: 1, table: 1 } ) ) {
2677 var tableParent
= commonPath
.contains( 'table' ).getParent();
2679 limit = function( node
) {
2680 return !node
.equals( tableParent
);
2684 // Fix DOM in the following case
2685 // <ol><li>a{b<ul><li>c}d</li></ul></li></ol>
2686 // Full list is returned
2687 // <ol><li>b<ul><li>c</li></ul></li></ol>
2689 // b<ul><li>c</li></ul>
2690 else if ( commonPath
.block
&& commonPath
.block
.is( CKEDITOR
.dtd
.$listItem
) ) {
2691 var startList
= startPath
.contains( CKEDITOR
.dtd
.$list
),
2692 endList
= endPath
.contains( CKEDITOR
.dtd
.$list
);
2694 if ( !startList
.equals( endList
) ) {
2695 var listParent
= commonPath
.contains( CKEDITOR
.dtd
.$list
).getParent();
2697 limit = function( node
) {
2698 return !node
.equals( listParent
);
2703 // If not defined, use generic limit function.
2705 limit = function( node
) {
2706 return !node
.equals( commonPath
.block
) && !node
.equals( commonPath
.blockLimit
);
2710 this.rebuildFragment( that
, editable
, node
, limit
);
2713 rebuildFragment: function( that
, editable
, node
, checkLimit
) {
2716 while ( node
&& !node
.equals( editable
) && checkLimit( node
) ) {
2717 // Don't clone children. Preserve element ids.
2718 clone
= node
.clone( 0, 1 );
2719 that
.fragment
.appendTo( clone
);
2720 that
.fragment
= clone
;
2722 node
= node
.getParent();
2728 // Handle range anchored in table row with a single cell enclosed:
2729 // <table><tbody><tr>[<td>a</td>]</tr></tbody></table>
2731 // <table><tbody><tr><td>{a}</td></tr></tbody></table>
2732 shrink: function( that
) {
2733 var range
= that
.range
,
2734 startContainer
= range
.startContainer
,
2735 endContainer
= range
.endContainer
,
2736 startOffset
= range
.startOffset
,
2737 endOffset
= range
.endOffset
;
2739 if ( startContainer
.type
== CKEDITOR
.NODE_ELEMENT
&& startContainer
.equals( endContainer
) && startContainer
.is( 'tr' ) && ++startOffset
== endOffset
) {
2740 range
.shrink( CKEDITOR
.SHRINK_TEXT
);
2747 // Helpers for editable.extractHtmlFromRange.
2749 extractHtmlFromRangeHelpers
= ( function() {
2750 function optimizeBookmarkNode( node
, toStart
) {
2751 var parent
= node
.getParent();
2753 if ( parent
.is( CKEDITOR
.dtd
.$inline
) )
2754 node
[ toStart
? 'insertBefore' : 'insertAfter' ]( parent
);
2757 function mergeElements( merged
, startBookmark
, endBookmark
) {
2758 optimizeBookmarkNode( startBookmark
);
2759 optimizeBookmarkNode( endBookmark
, 1 );
2762 while ( ( next
= endBookmark
.getNext() ) ) {
2763 next
.insertAfter( startBookmark
);
2765 // Update startBookmark after insertion to avoid the reversal of nodes (#13449).
2766 startBookmark
= next
;
2769 if ( isEmpty( merged
) )
2773 function getPath( startElement
, root
) {
2774 return new CKEDITOR
.dom
.elementPath( startElement
, root
);
2777 // Creates a range from a bookmark without removing the bookmark.
2778 function createRangeFromBookmark( root
, bookmark
) {
2779 var range
= new CKEDITOR
.dom
.range( root
);
2780 range
.setStartAfter( bookmark
.startNode
);
2781 range
.setEndBefore( bookmark
.endNode
);
2786 detectMerge: function( that
, editable
) {
2787 var range
= createRangeFromBookmark( editable
, that
.bookmark
),
2788 startPath
= range
.startPath(),
2789 endPath
= range
.endPath(),
2791 startList
= startPath
.contains( CKEDITOR
.dtd
.$list
),
2792 endList
= endPath
.contains( CKEDITOR
.dtd
.$list
);
2795 // Both lists must exist
2796 startList
&& endList
&&
2797 // ...and be of the same type
2798 // startList.getName() == endList.getName() &&
2799 // ...and share the same parent (same level in the tree)
2800 startList
.getParent().equals( endList
.getParent() ) &&
2801 // ...and must be different.
2802 !startList
.equals( endList
);
2804 that
.mergeListItems
=
2805 startPath
.block
&& endPath
.block
&&
2806 // Both containers must be list items
2807 startPath
.block
.is( CKEDITOR
.dtd
.$listItem
) && endPath
.block
.is( CKEDITOR
.dtd
.$listItem
);
2809 // Create merge bookmark.
2810 if ( that
.mergeList
|| that
.mergeListItems
) {
2811 var rangeClone
= range
.clone();
2813 rangeClone
.setStartBefore( that
.bookmark
.startNode
);
2814 rangeClone
.setEndAfter( that
.bookmark
.endNode
);
2816 that
.mergeListBookmark
= rangeClone
.createBookmark();
2820 merge: function( that
, editable
) {
2821 if ( !that
.mergeListBookmark
)
2824 var startNode
= that
.mergeListBookmark
.startNode
,
2825 endNode
= that
.mergeListBookmark
.endNode
,
2827 startPath
= getPath( startNode
, editable
),
2828 endPath
= getPath( endNode
, editable
);
2830 if ( that
.mergeList
) {
2831 var firstList
= startPath
.contains( CKEDITOR
.dtd
.$list
),
2832 secondList
= endPath
.contains( CKEDITOR
.dtd
.$list
);
2834 if ( !firstList
.equals( secondList
) ) {
2835 secondList
.moveChildren( firstList
);
2836 secondList
.remove();
2840 if ( that
.mergeListItems
) {
2841 var firstListItem
= startPath
.contains( CKEDITOR
.dtd
.$listItem
),
2842 secondListItem
= endPath
.contains( CKEDITOR
.dtd
.$listItem
);
2844 if ( !firstListItem
.equals( secondListItem
) ) {
2845 mergeElements( secondListItem
, startNode
, endNode
);
2849 // Remove bookmark nodes.
2856 // Detects whether blocks should be merged once contents are extracted.
2857 detectMerge: function( that
, editable
) {
2858 // Don't merge blocks if lists or tables are already involved.
2859 if ( that
.tableContentsRanges
|| that
.mergeListBookmark
)
2862 var rangeClone
= new CKEDITOR
.dom
.range( editable
);
2864 rangeClone
.setStartBefore( that
.bookmark
.startNode
);
2865 rangeClone
.setEndAfter( that
.bookmark
.endNode
);
2867 that
.mergeBlockBookmark
= rangeClone
.createBookmark();
2870 merge: function( that
, editable
) {
2871 if ( !that
.mergeBlockBookmark
|| that
.purgeTableBookmark
)
2874 var startNode
= that
.mergeBlockBookmark
.startNode
,
2875 endNode
= that
.mergeBlockBookmark
.endNode
,
2877 startPath
= getPath( startNode
, editable
),
2878 endPath
= getPath( endNode
, editable
),
2880 firstBlock
= startPath
.block
,
2881 secondBlock
= endPath
.block
;
2883 if ( firstBlock
&& secondBlock
&& !firstBlock
.equals( secondBlock
) ) {
2884 mergeElements( secondBlock
, startNode
, endNode
);
2887 // Remove bookmark nodes.
2893 var table
= ( function() {
2894 var tableEditable
= { td: 1, th: 1, caption: 1 };
2896 // Returns an array of ranges which should be entirely extracted.
2898 // <table><tr>[<td>xx</td><td>y}y</td></tr></table>
2900 // <table><tr><td>[xx]</td><td>[y}y</td></tr></table>
2901 function findTableContentsRanges( range
) {
2902 // Leaving the below for debugging purposes.
2904 // console.log( 'findTableContentsRanges' );
2905 // console.log( bender.tools.range.getWithHtml( range.root, range ) );
2907 var contentsRanges
= [],
2909 walker
= new CKEDITOR
.dom
.walker( range
),
2910 startCell
= range
.startPath().contains( tableEditable
),
2911 endCell
= range
.endPath().contains( tableEditable
),
2914 walker
.guard = function( node
, leaving
) {
2915 // Guard may be executed on some node boundaries multiple times,
2916 // what results in creating more than one range for each selected cell. (#12964)
2917 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
) {
2918 var key
= 'visited_' + ( leaving
? 'out' : 'in' );
2919 if ( node
.getCustomData( key
) ) {
2923 CKEDITOR
.dom
.element
.setMarker( database
, node
, key
, 1 );
2926 // Handle partial selection in a cell in which the range starts:
2927 // <td><p>x{xx</p></td>...
2929 // <td><p>x{xx</p>]</td>
2930 if ( leaving
&& startCell
&& node
.equals( startCell
) ) {
2931 editableRange
= range
.clone();
2932 editableRange
.setEndAt( startCell
, CKEDITOR
.POSITION_BEFORE_END
);
2933 contentsRanges
.push( editableRange
);
2937 // Handle partial selection in a cell in which the range ends.
2938 if ( !leaving
&& endCell
&& node
.equals( endCell
) ) {
2939 editableRange
= range
.clone();
2940 editableRange
.setStartAt( endCell
, CKEDITOR
.POSITION_AFTER_START
);
2941 contentsRanges
.push( editableRange
);
2945 // Handle all other cells visited by the walker.
2946 // We need to check whether the cell is disjoint with
2947 // the start and end cells to correctly handle case like:
2948 // <td>x{x</td><td><table>..<td>y}y</td>..</table></td>
2949 // without the check the second cell's content would be entirely removed.
2950 if ( !leaving
&& checkRemoveCellContents( node
) ) {
2951 editableRange
= range
.clone();
2952 editableRange
.selectNodeContents( node
);
2953 contentsRanges
.push( editableRange
);
2957 walker
.lastForward();
2959 // Clear all markers so next extraction will not be affected by this one.
2960 CKEDITOR
.dom
.element
.clearAllMarkers( database
);
2962 return contentsRanges
;
2964 function checkRemoveCellContents( node
) {
2967 node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( tableEditable
) &&
2968 // Must be disjoint with the range's startCell if exists.
2969 ( !startCell
|| checkDisjointNodes( node
, startCell
) ) &&
2970 // Must be disjoint with the range's endCell if exists.
2971 ( !endCell
|| checkDisjointNodes( node
, endCell
) )
2976 // Returns a normalized common ancestor of a range.
2977 // If the real common ancestor is located somewhere in between a table and a td/th/caption,
2978 // then the table will be returned.
2979 function getNormalizedAncestor( range
) {
2980 var common
= range
.getCommonAncestor();
2982 if ( common
.is( CKEDITOR
.dtd
.$tableContent
) && !common
.is( tableEditable
) ) {
2983 common
= common
.getAscendant( 'table', true );
2989 // Check whether node1 and node2 are disjoint, so are:
2991 // * not contained in each other.
2992 function checkDisjointNodes( node1
, node2
) {
2993 var disallowedPositions
= CKEDITOR
.POSITION_CONTAINS
+ CKEDITOR
.POSITION_IS_CONTAINED
,
2994 pos
= node1
.getPosition( node2
);
2996 // Baaah... IDENTICAL is 0, so we can't simplify this ;/.
2997 return pos
=== CKEDITOR
.POSITION_IDENTICAL
?
2999 ( ( pos
& disallowedPositions
) === 0 );
3003 // Detects whether to purge entire list.
3004 detectPurge: function( that
) {
3005 var range
= that
.range
,
3006 walkerRange
= range
.clone();
3008 walkerRange
.enlarge( CKEDITOR
.ENLARGE_ELEMENT
);
3010 var walker
= new CKEDITOR
.dom
.walker( walkerRange
),
3013 // Count the number of table editables in the range. If there's more than one,
3014 // table MAY be removed completely (it's a cross-cell range). Otherwise, only
3015 // the contents of the cell are usually removed.
3016 walker
.evaluator = function( node
) {
3017 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( tableEditable
) ) {
3022 walker
.checkForward();
3024 if ( editablesCount
> 1 ) {
3025 var startTable
= range
.startPath().contains( 'table' ),
3026 endTable
= range
.endPath().contains( 'table' );
3028 if ( startTable
&& endTable
&& range
.checkBoundaryOfElement( startTable
, CKEDITOR
.START
) && range
.checkBoundaryOfElement( endTable
, CKEDITOR
.END
) ) {
3029 var rangeClone
= that
.range
.clone();
3031 rangeClone
.setStartBefore( startTable
);
3032 rangeClone
.setEndAfter( endTable
);
3034 that
.purgeTableBookmark
= rangeClone
.createBookmark();
3041 // This method tries to discover whether the range starts or ends somewhere in a table
3042 // (it is not interested whether the range contains a table, because in such case
3043 // the extractContents() methods does the job correctly).
3044 // If the range meets these criteria, then the method tries to discover and store the following:
3046 // * that.tableSurroundingRange - a part of the range which is located outside of any table which
3047 // will be touched (note: when range is located in a single cell it does not touch the table).
3048 // This range can be placed at:
3049 // * at the beginning: <p>he{re</p><table>..]..</table>
3050 // * in the middle: <table>..[..</table><p>here</p><table>..]..</table>
3051 // * at the end: <table>..[..</table><p>he}re</p>
3052 // * that.tableContentsRanges - an array of ranges with contents of td/th/caption that should be removed.
3053 // This assures that calling extractContents() does not change the structure of the table(s).
3054 detectRanges: function( that
, editable
) {
3055 var range
= createRangeFromBookmark( editable
, that
.bookmark
),
3056 surroundingRange
= range
.clone(),
3060 // Find a common ancestor and normalize it (so the following paths contain tables).
3061 commonAncestor
= getNormalizedAncestor( range
),
3063 // Create paths using the normalized ancestor, so tables beyond the context
3064 // of the input range are not found.
3065 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, commonAncestor
),
3066 endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
, commonAncestor
),
3068 startTable
= startPath
.contains( 'table' ),
3069 endTable
= endPath
.contains( 'table' ),
3071 tableContentsRanges
;
3073 // Nothing to do here - the range doesn't touch any table or
3074 // it contains a table, but that table is fully selected so it will be simply fully removed
3075 // by the normal algorithm.
3076 if ( !startTable
&& !endTable
) {
3080 // Handle two disjoint tables case:
3081 // <table>..[..</table><p>ab</p><table>..]..</table>
3082 // is handled as (respectively: findTableContents( left ), surroundingRange, findTableContents( right )):
3083 // <table>..[..</table>][<p>ab</p>][<table>..]..</table>
3084 // Check that tables are disjoint to exclude a case when start equals end or one is contained
3086 if ( startTable
&& endTable
&& checkDisjointNodes( startTable
, endTable
) ) {
3087 that
.tableSurroundingRange
= surroundingRange
;
3088 surroundingRange
.setStartAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
3089 surroundingRange
.setEndAt( endTable
, CKEDITOR
.POSITION_BEFORE_START
);
3091 leftRange
= range
.clone();
3092 leftRange
.setEndAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
3094 rightRange
= range
.clone();
3095 rightRange
.setStartAt( endTable
, CKEDITOR
.POSITION_BEFORE_START
);
3097 tableContentsRanges
= findTableContentsRanges( leftRange
).concat( findTableContentsRanges( rightRange
) );
3099 // Divide the initial range into two parts:
3100 // * range which contains the part containing the table,
3101 // * surroundingRange which contains the part outside the table.
3103 // The surroundingRange exists only if one of the range ends is
3104 // located outside the table.
3106 // <p>a{b</p><table>..]..</table><p>cd</p>
3107 // becomes (respectively: surroundingRange, range):
3108 // <p>a{b</p>][<table>..]..</table><p>cd</p>
3109 else if ( !startTable
) {
3110 that
.tableSurroundingRange
= surroundingRange
;
3111 surroundingRange
.setEndAt( endTable
, CKEDITOR
.POSITION_BEFORE_START
);
3113 range
.setStartAt( endTable
, CKEDITOR
.POSITION_AFTER_START
);
3115 // <p>ab</p><table>..[..</table><p>c}d</p>
3116 // becomes (respectively range, surroundingRange):
3117 // <p>ab</p><table>..[..</table>][<p>c}d</p>
3118 else if ( !endTable
) {
3119 that
.tableSurroundingRange
= surroundingRange
;
3120 surroundingRange
.setStartAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
3122 range
.setEndAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
3125 // Use already calculated or calculate for the remaining range.
3126 that
.tableContentsRanges
= tableContentsRanges
? tableContentsRanges : findTableContentsRanges( range
);
3128 // Leaving the below for debugging purposes.
3130 // if ( that.tableSurroundingRange ) {
3131 // console.log( 'tableSurroundingRange' );
3132 // console.log( bender.tools.range.getWithHtml( that.tableSurroundingRange.root, that.tableSurroundingRange ) );
3135 // console.log( 'tableContentsRanges' );
3136 // that.tableContentsRanges.forEach( function( range ) {
3137 // console.log( bender.tools.range.getWithHtml( range.root, range ) );
3141 deleteRanges: function( that
) {
3144 // Delete table cell contents.
3145 while ( ( range
= that
.tableContentsRanges
.pop() ) ) {
3146 range
.extractContents();
3148 if ( isEmpty( range
.startContainer
) )
3149 range
.startContainer
.appendBogus();
3152 // Finally delete surroundings of the table.
3153 if ( that
.tableSurroundingRange
) {
3154 that
.tableSurroundingRange
.extractContents();
3158 purge: function( that
) {
3159 if ( !that
.purgeTableBookmark
)
3164 rangeClone
= range
.clone(),
3165 // How about different enter modes?
3166 block
= doc
.createElement( 'p' );
3168 block
.insertBefore( that
.purgeTableBookmark
.startNode
);
3170 rangeClone
.moveToBookmark( that
.purgeTableBookmark
);
3171 rangeClone
.deleteContents();
3173 that
.range
.moveToPosition( block
, CKEDITOR
.POSITION_AFTER_START
);
3183 // Detects whether use "mergeThen" argument in range.extractContents().
3184 detectExtractMerge: function( that
) {
3185 // Don't merge if playing with lists.
3187 that
.range
.startPath().contains( CKEDITOR
.dtd
.$listItem
) &&
3188 that
.range
.endPath().contains( CKEDITOR
.dtd
.$listItem
)
3192 fixUneditableRangePosition: function( range
) {
3193 if ( !range
.startContainer
.getDtd()[ '#' ] ) {
3194 range
.moveToClosestEditablePosition( null, true );
3198 // Perform auto paragraphing if needed.
3199 autoParagraph: function( editor
, range
) {
3200 var path
= range
.startPath(),
3203 if ( shouldAutoParagraph( editor
, path
.block
, path
.blockLimit
) && ( fixBlock
= autoParagraphTag( editor
) ) ) {
3204 fixBlock
= range
.document
.createElement( fixBlock
);
3205 fixBlock
.appendBogus();
3206 range
.insertNode( fixBlock
);
3207 range
.moveToPosition( fixBlock
, CKEDITOR
.POSITION_AFTER_START
);
3216 * Whether the editor must output an empty value (`''`) if its content only consists
3217 * of an empty paragraph.
3219 * config.ignoreEmptyParagraph = false;
3221 * @cfg {Boolean} [ignoreEmptyParagraph=true]
3222 * @member CKEDITOR.config
3226 * Event fired by the editor in order to get accessibility help label.
3227 * The event is responded to by a component which provides accessibility
3228 * help (i.e. the `a11yhelp` plugin) hence the editor is notified whether
3229 * accessibility help is available.
3233 * editor.on( 'ariaEditorHelpLabel', function( evt ) {
3234 * evt.data.label = editor.lang.common.editorHelp;
3239 * var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
3242 * @event ariaEditorHelpLabel
3243 * @param {String} label The label to be used.
3244 * @member CKEDITOR.editor
3248 * Event fired when the user double-clicks in the editable area.
3249 * The event allows to open a dialog window for a clicked element in a convenient way:
3251 * editor.on( 'doubleclick', function( evt ) {
3252 * var element = evt.data.element;
3254 * if ( element.is( 'table' ) )
3255 * evt.data.dialog = 'tableProperties';
3258 * **Note:** To handle double-click on a widget use {@link CKEDITOR.plugins.widget#doubleclick}.
3260 * @event doubleclick
3262 * @param {CKEDITOR.dom.element} data.element The double-clicked element.
3263 * @param {String} data.dialog The dialog window to be opened. If set by the listener,
3264 * the specified dialog window will be opened.
3265 * @member CKEDITOR.editor