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. (http://dev.ckeditor.com/ticket/14825)
80 if ( CKEDITOR
.env
.edge
&& CKEDITOR
.env
.version
> 14 && !this.hasFocus
&& this.getDocument().equals( CKEDITOR
.document
) ) {
81 this.editor
._
.previousScrollTop
= this.$.scrollTop
;
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
) ) {
90 // We have no control over exactly what happens when the native `focus` method is called,
91 // so save the scroll position and restore it later.
92 if ( CKEDITOR
.env
.chrome
) {
93 var scrollPos
= this.$.scrollTop
;
95 this.$.scrollTop
= scrollPos
;
101 // IE throws unspecified error when focusing editable after closing dialog opened on nested editable.
102 if ( !CKEDITOR
.env
.ie
)
106 // Remedy if Safari doens't applies focus properly. (http://dev.ckeditor.com/ticket/279)
107 if ( CKEDITOR
.env
.safari
&& !this.isInline() ) {
108 active
= CKEDITOR
.document
.getActive();
109 if ( !active
.equals( this.getWindow().getFrame() ) )
110 this.getWindow().focus();
116 * Overrides {@link CKEDITOR.dom.element#on} to have special `focus/blur` handling.
117 * The `focusin/focusout` events are used in IE to replace regular `focus/blur` events
118 * because we want to avoid the asynchronous nature of later ones.
120 on: function( name
, fn
) {
121 var args
= Array
.prototype.slice
.call( arguments
, 0 );
123 if ( CKEDITOR
.env
.ie
&& ( /^focus|blur$/ ).exec( name
) ) {
124 name
= name
== 'focus' ? 'focusin' : 'focusout';
126 // The "focusin/focusout" events bubbled, e.g. If there are elements with layout
127 // they fire this event when clicking in to edit them but it must be ignored
128 // to allow edit their contents. (http://dev.ckeditor.com/ticket/4682)
129 fn
= isNotBubbling( fn
, this );
134 return CKEDITOR
.dom
.element
.prototype.on
.apply( this, args
);
138 * Registers an event listener that needs to be removed when detaching this editable.
139 * This means that it will be automatically removed when {@link #detach} is executed,
140 * for example on {@link CKEDITOR.editor#setMode changing editor mode} or destroying editor.
142 * Except for `obj` all other arguments have the same meaning as in {@link CKEDITOR.event#on}.
144 * This method is strongly related to the {@link CKEDITOR.editor#contentDom} and
145 * {@link CKEDITOR.editor#contentDomUnload} events, because they are fired
146 * when an editable is being attached and detached. Therefore, this method is usually used
147 * in the following way:
149 * editor.on( 'contentDom', function() {
150 * var editable = editor.editable();
151 * editable.attachListener( editable, 'mousedown', function() {
156 * This code will attach the `mousedown` listener every time a new editable is attached
157 * to the editor, which in classic (`iframe`-based) editor happens every time the
158 * data or the mode is set. This listener will also be removed when that editable is detached.
160 * It is also possible to attach a listener to another object (e.g. to a document).
162 * editor.on( 'contentDom', function() {
163 * editor.editable().attachListener( editor.document, 'mousedown', function() {
168 * @param {CKEDITOR.event} obj The element/object to which the listener will be attached. Every object
169 * which inherits from {@link CKEDITOR.event} may be used including {@link CKEDITOR.dom.element},
170 * {@link CKEDITOR.dom.document}, and {@link CKEDITOR.editable}.
171 * @param {String} eventName The name of the event that will be listened to.
172 * @param {Function} listenerFunction The function listening to the
173 * event. A single {@link CKEDITOR.eventInfo} object instance
174 * containing all the event data is passed to this function.
175 * @param {Object} [scopeObj] The object used to scope the listener
176 * call (the `this` object). If omitted, the current object is used.
177 * @param {Object} [listenerData] Data to be sent as the
178 * {@link CKEDITOR.eventInfo#listenerData} when calling the listener.
179 * @param {Number} [priority=10] The listener priority. Lower priority
180 * listeners are called first. Listeners with the same priority
181 * value are called in the registration order.
182 * @returns {Object} An object containing the `removeListener`
183 * function that can be used to remove the listener at any time.
185 attachListener: function( obj
/*, event, fn, scope, listenerData, priority*/ ) {
186 !this._
.listeners
&& ( this._
.listeners
= [] );
187 // Register the listener.
188 var args
= Array
.prototype.slice
.call( arguments
, 1 ),
189 listener
= obj
.on
.apply( obj
, args
);
191 this._
.listeners
.push( listener
);
197 * Remove all event listeners registered from {@link #attachListener}.
199 clearListeners: function() {
200 var listeners
= this._
.listeners
;
201 // Don't get broken by this.
203 while ( listeners
.length
)
204 listeners
.pop().removeListener();
209 * Restore all attribution changes made by {@link #changeAttr }.
211 restoreAttrs: function() {
212 var changes
= this._
.attrChanges
, orgVal
;
213 for ( var attr
in changes
) {
214 if ( changes
.hasOwnProperty( attr
) ) {
215 orgVal
= changes
[ attr
];
216 // Restore original attribute.
217 orgVal
!== null ? this.setAttribute( attr
, orgVal
) : this.removeAttribute( attr
);
223 * Adds a CSS class name to this editable that needs to be removed on detaching.
225 * @param {String} className The class name to be added.
226 * @see CKEDITOR.dom.element#addClass
228 attachClass: function( cls
) {
229 var classes
= this.getCustomData( 'classes' );
230 if ( !this.hasClass( cls
) ) {
231 !classes
&& ( classes
= [] ), classes
.push( cls
);
232 this.setCustomData( 'classes', classes
);
233 this.addClass( cls
);
238 * Make an attribution change that would be reverted on editable detaching.
239 * @param {String} attr The attribute name to be changed.
240 * @param {String} val The value of specified attribute.
242 changeAttr: function( attr
, val
) {
243 var orgVal
= this.getAttribute( attr
);
244 if ( val
!== orgVal
) {
245 !this._
.attrChanges
&& ( this._
.attrChanges
= {} );
247 // Saved the original attribute val.
248 if ( !( attr
in this._
.attrChanges
) )
249 this._
.attrChanges
[ attr
] = orgVal
;
251 this.setAttribute( attr
, val
);
256 * Low-level method for inserting text into the editable.
257 * See the {@link CKEDITOR.editor#method-insertText} method which is the editor-level API
260 * @param {String} text
262 insertText: function( text
) {
263 // Focus the editor before calling transformPlainTextToHtml. (http://dev.ckeditor.com/ticket/12726)
265 this.insertHtml( this.transformPlainTextToHtml( text
), 'text' );
269 * Transforms plain text to HTML based on current selection and {@link CKEDITOR.editor#activeEnterMode}.
272 * @param {String} text Text to transform.
273 * @returns {String} HTML generated from the text.
275 transformPlainTextToHtml: function( text
) {
276 var enterMode
= this.editor
.getSelection().getStartElement().hasAscendant( 'pre', true ) ?
278 this.editor
.activeEnterMode
;
280 return CKEDITOR
.tools
.transformPlainTextToHtml( text
, enterMode
);
284 * Low-level method for inserting HTML into the editable.
285 * See the {@link CKEDITOR.editor#method-insertHtml} method which is the editor-level API
288 * This method will insert HTML into the current selection or a given range. It also creates an undo snapshot,
289 * scrolls the viewport to the insertion and selects the range next to the inserted content.
290 * If you want to insert HTML without additional operations use {@link #method-insertHtmlIntoRange}.
292 * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
294 * @param {String} data The HTML to be inserted.
295 * @param {String} [mode='html'] See {@link CKEDITOR.editor#method-insertHtml}'s param.
296 * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range
297 * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case).
298 * Introduced in CKEditor 4.5.
300 insertHtml: function( data
, mode
, range
) {
301 var editor
= this.editor
;
304 editor
.fire( 'saveSnapshot' );
307 // HTML insertion only considers the first range.
308 // Note: getRanges will be overwritten for tests since we want to test
309 // custom ranges and bypass native selections.
310 range
= editor
.getSelection().getRanges()[ 0 ];
313 // Default mode is 'html'.
314 insert( this, mode
|| 'html', data
, range
);
316 // Make the final range selection.
321 this.editor
.fire( 'afterInsertHtml', {} );
325 * Inserts HTML into the position in the editor determined by the range.
327 * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects inserted
328 * HTML. If you want to do it, use {@link #method-insertHtml}.
330 * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
333 * @param {String} data HTML code to be inserted into the editor.
334 * @param {CKEDITOR.dom.range} range The range as a place of insertion.
335 * @param {String} [mode='html'] Mode in which HTML will be inserted.
336 * See {@link CKEDITOR.editor#method-insertHtml}.
338 insertHtmlIntoRange: function( data
, range
, mode
) {
339 // Default mode is 'html'
340 insert( this, mode
|| 'html', data
, range
);
342 this.editor
.fire( 'afterInsertHtml', { intoRange: range
} );
346 * Low-level method for inserting an element into the editable.
347 * See the {@link CKEDITOR.editor#method-insertElement} method which is the editor-level API
350 * This method will insert the element into the current selection or a given range. It also creates an undo
351 * snapshot, scrolls the viewport to the insertion and selects the range next to the inserted content.
352 * If you want to insert an element without additional operations use {@link #method-insertElementIntoRange}.
354 * @param {CKEDITOR.dom.element} element The element to insert.
355 * @param {CKEDITOR.dom.range} [range] If specified, the element will be inserted into the range
356 * instead of into the selection.
358 insertElement: function( element
, range
) {
359 var editor
= this.editor
;
361 // Prepare for the insertion. For example - focus editor (http://dev.ckeditor.com/ticket/11848).
363 editor
.fire( 'saveSnapshot' );
365 var enterMode
= editor
.activeEnterMode
,
366 selection
= editor
.getSelection(),
367 elementName
= element
.getName(),
368 isBlock
= CKEDITOR
.dtd
.$block
[ elementName
];
371 range
= selection
.getRanges()[ 0 ];
374 // Insert element into first range only and ignore the rest (http://dev.ckeditor.com/ticket/11183).
375 if ( this.insertElementIntoRange( element
, range
) ) {
376 range
.moveToPosition( element
, CKEDITOR
.POSITION_AFTER_END
);
378 // If we're inserting a block element, the new cursor position must be
379 // optimized. (http://dev.ckeditor.com/ticket/3100,http://dev.ckeditor.com/ticket/5436,http://dev.ckeditor.com/ticket/8950)
381 // Find next, meaningful element.
382 var next
= element
.getNext( function( node
) {
383 return isNotEmpty( node
) && !isBogus( node
);
386 if ( next
&& next
.type
== CKEDITOR
.NODE_ELEMENT
&& next
.is( CKEDITOR
.dtd
.$block
) ) {
387 // If the next one is a text block, move cursor to the start of it's content.
388 if ( next
.getDtd()[ '#' ] )
389 range
.moveToElementEditStart( next
);
390 // Otherwise move cursor to the before end of the last element.
392 range
.moveToElementEditEnd( element
);
394 // Open a new line if the block is inserted at the end of parent.
395 else if ( !next
&& enterMode
!= CKEDITOR
.ENTER_BR
) {
396 next
= range
.fixBlock( true, enterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p' );
397 range
.moveToElementEditStart( next
);
402 // Set up the correct selection.
403 selection
.selectRanges( [ range
] );
409 * Alias for {@link #insertElement}.
412 * @param {CKEDITOR.dom.element} element The element to be inserted.
414 insertElementIntoSelection: function( element
) {
415 this.insertElement( element
);
419 * Inserts an element into the position in the editor determined by the range.
421 * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects the inserted
422 * element. If you want to do it, use the {@link #method-insertElement} method.
424 * @param {CKEDITOR.dom.element} element The element to be inserted.
425 * @param {CKEDITOR.dom.range} range The range as a place of insertion.
426 * @returns {Boolean} Informs whether the insertion was successful.
428 insertElementIntoRange: function( element
, range
) {
429 var editor
= this.editor
,
430 enterMode
= editor
.config
.enterMode
,
431 elementName
= element
.getName(),
432 isBlock
= CKEDITOR
.dtd
.$block
[ elementName
];
434 if ( range
.checkReadOnly() )
437 // Remove the original contents, merge split nodes.
438 range
.deleteContents( 1 );
440 if ( range
.startContainer
.type
== CKEDITOR
.NODE_ELEMENT
) {
441 // If range is placed in intermediate element (not td or th), we need to do three things:
442 // * fill emptied <td/th>s with if browser needs them,
443 // * remove empty text nodes so IE8 won't crash
444 // (http://dev.ckeditor.com/ticket/11183#comment:8),
445 // * fix structure and move range into the <td/th> element.
446 if ( range
.startContainer
.is( { tr: 1, table: 1, tbody: 1, thead: 1, tfoot: 1 } ) ) {
447 fixTableAfterContentsDeletion( range
);
448 } else if ( range
.startContainer
.is( CKEDITOR
.dtd
.$list
) ) {
449 // Similarly there's a need for lists.
450 fixListAfterContentsDelete( range
);
454 // If we're inserting a block at dtd-violated position, split
455 // the parent blocks until we reach blockLimit.
459 while ( ( current
= range
.getCommonAncestor( 0, 1 ) ) &&
460 ( dtd
= CKEDITOR
.dtd
[ current
.getName() ] ) &&
461 !( dtd
&& dtd
[ elementName
] ) ) {
462 // Split up inline elements.
463 if ( current
.getName() in CKEDITOR
.dtd
.span
)
464 range
.splitElement( current
);
466 // If we're in an empty block which indicate a new paragraph,
467 // simply replace it with the inserting block.(http://dev.ckeditor.com/ticket/3664)
468 else if ( range
.checkStartOfBlock() && range
.checkEndOfBlock() ) {
469 range
.setStartBefore( current
);
470 range
.collapse( true );
473 range
.splitBlock( enterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p', editor
.editable() );
478 // Insert the new node.
479 range
.insertNode( element
);
481 // Return true if insertion was successful.
486 * @see CKEDITOR.editor#setData
488 setData: function( data
, isSnapshot
) {
490 data
= this.editor
.dataProcessor
.toHtml( data
);
492 this.setHtml( data
);
493 this.fixInitialSelection();
495 // Editable is ready after first setData.
496 if ( this.status
== 'unloaded' )
497 this.status
= 'ready';
499 this.editor
.fire( 'dataReady' );
503 * @see CKEDITOR.editor#getData
505 getData: function( isSnapshot
) {
506 var data
= this.getHtml();
509 data
= this.editor
.dataProcessor
.toDataFormat( data
);
515 * Changes the read-only state of this editable.
517 * @param {Boolean} isReadOnly
519 setReadOnly: function( isReadOnly
) {
520 this.setAttribute( 'contenteditable', !isReadOnly
);
524 * Detaches this editable object from the DOM (removes classes, listeners, etc.)
527 // Cleanup the element.
528 this.removeClass( 'cke_editable' );
530 this.status
= 'detached';
532 // Save the editor reference which will be lost after
533 // calling detach from super class.
534 var editor
= this.editor
;
538 delete editor
.document
;
539 delete editor
.window
;
543 * Checks if the editable is one of the host page elements, indicates
544 * an inline editing environment.
548 isInline: function() {
549 return this.getDocument().equals( CKEDITOR
.document
);
553 * Fixes the selection and focus which may be in incorrect state after
554 * editable's inner HTML was overwritten.
556 * If the editable did not have focus, then the selection will be fixed when the editable
557 * is focused for the first time. If the editable already had focus, then the selection will
558 * be fixed immediately.
560 * To understand the problem see:
562 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
563 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusafterundoing
564 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/selectionafterfocusing
565 * * http://tests.ckeditor.dev:1030/tests/plugins/newpage/manual/selectionafternewpage
570 fixInitialSelection: function() {
573 // Deal with IE8- IEQM (the old MS selection) first.
574 if ( CKEDITOR
.env
.ie
&& ( CKEDITOR
.env
.version
< 9 || CKEDITOR
.env
.quirks
) ) {
575 if ( this.hasFocus
) {
583 // If editable did not have focus, fix the selection when it is first focused.
584 if ( !this.hasFocus
) {
585 this.once( 'focus', function() {
587 }, null, null, -999 );
588 // If editable had focus, fix the selection immediately.
594 function fixSelection() {
595 var $doc
= that
.getDocument().$,
596 $sel
= $doc
.getSelection();
598 if ( requiresFix( $sel
) ) {
599 var range
= new CKEDITOR
.dom
.range( that
);
600 range
.moveToElementEditStart( that
);
602 var $range
= $doc
.createRange();
603 $range
.setStart( range
.startContainer
.$, range
.startOffset
);
604 $range
.collapse( true );
606 $sel
.removeAllRanges();
607 $sel
.addRange( $range
);
611 function requiresFix( $sel
) {
612 // This condition covers most broken cases after setting data.
613 if ( $sel
.anchorNode
&& $sel
.anchorNode
== that
.$ ) {
618 // http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
619 // (the inline editor TC)
620 if ( CKEDITOR
.env
.webkit
) {
621 var active
= that
.getDocument().getActive();
622 if ( active
&& active
.equals( that
) && !$sel
.anchorNode
) {
628 function fixMSSelection() {
629 var $doc
= that
.getDocument().$,
630 $sel
= $doc
.selection
,
631 active
= that
.getDocument().getActive();
633 if ( $sel
.type
== 'None' && active
.equals( that
) ) {
634 var range
= new CKEDITOR
.dom
.range( that
),
636 $range
= $doc
.body
.createTextRange();
638 range
.moveToElementEditStart( that
);
640 parentElement
= range
.startContainer
;
641 if ( parentElement
.type
!= CKEDITOR
.NODE_ELEMENT
) {
642 parentElement
= parentElement
.getParent();
645 $range
.moveToElementText( parentElement
.$ );
646 $range
.collapse( true );
653 * The base of the {@link CKEDITOR.editor#getSelectedHtml} method.
656 * @method getHtmlFromRange
657 * @param {CKEDITOR.dom.range} range
658 * @returns {CKEDITOR.dom.documentFragment}
660 getHtmlFromRange: function( range
) {
661 // There's nothing to return if range is collapsed.
662 if ( range
.collapsed
)
663 return new CKEDITOR
.dom
.documentFragment( range
.document
);
665 // Info object passed between methods.
667 doc: this.getDocument(),
668 // Leave original range object untouched.
672 getHtmlFromRangeHelpers
.eol
.detect( that
, this );
673 getHtmlFromRangeHelpers
.bogus
.exclude( that
);
674 getHtmlFromRangeHelpers
.cell
.shrink( that
);
676 that
.fragment
= that
.range
.cloneContents();
678 getHtmlFromRangeHelpers
.tree
.rebuild( that
, this );
679 getHtmlFromRangeHelpers
.eol
.fix( that
, this );
681 return new CKEDITOR
.dom
.documentFragment( that
.fragment
.$ );
685 * The base of the {@link CKEDITOR.editor#extractSelectedHtml} method.
687 * **Note:** The range is modified so it matches the desired selection after extraction
688 * even though the selection is not made.
691 * @param {CKEDITOR.dom.range} range
692 * @param {Boolean} [removeEmptyBlock=false] See {@link CKEDITOR.editor#extractSelectedHtml}'s parameter.
693 * Note that the range will not be modified if this parameter is set to `true`.
694 * @returns {CKEDITOR.dom.documentFragment} The extracted fragment of the editable content.
696 extractHtmlFromRange: function( range
, removeEmptyBlock
) {
697 var helpers
= extractHtmlFromRangeHelpers
,
702 // Since it is quite hard to build a valid documentFragment
703 // out of extracted contents because DOM changes, let's mimic
704 // extracted HTML with #getHtmlFromRange. Yep. It's a hack.
705 extractedFragment
= this.getHtmlFromRange( range
);
707 // Collapsed range means that there's nothing to extract.
708 if ( range
.collapsed
) {
710 return extractedFragment
;
713 // Include inline element if possible.
714 range
.enlarge( CKEDITOR
.ENLARGE_INLINE
, 1 );
716 // This got to be done before bookmarks are created because purging
717 // depends on the position of the range at the boundaries of the table,
718 // usually distorted by bookmark spans.
719 helpers
.table
.detectPurge( that
);
721 // We'll play with DOM, let's hold the position of the range.
722 that
.bookmark
= range
.createBookmark();
723 // While bookmarked, make unaccessible, to make sure that none of the methods
724 // will try to use it (they should use that.bookmark).
725 // This is done because ranges get desynchronized with the DOM when more bookmarks
726 // is created (as for instance that.targetBookmark).
729 // The range to be restored after extraction should be kept
730 // outside of the range, so it's not removed by range.extractContents.
731 var targetRange
= this.editor
.createRange();
732 targetRange
.moveToPosition( that
.bookmark
.startNode
, CKEDITOR
.POSITION_BEFORE_START
);
733 that
.targetBookmark
= targetRange
.createBookmark();
735 // Execute content-specific detections.
736 helpers
.list
.detectMerge( that
, this );
737 helpers
.table
.detectRanges( that
, this );
738 helpers
.block
.detectMerge( that
, this );
740 // Simply, do the job.
741 if ( that
.tableContentsRanges
) {
742 helpers
.table
.deleteRanges( that
);
744 // Done here only to remove bookmark's spans.
745 range
.moveToBookmark( that
.bookmark
);
748 // To use the range we need to restore the bookmark and make
749 // the range accessible again.
750 range
.moveToBookmark( that
.bookmark
);
752 range
.extractContents( helpers
.detectExtractMerge( that
) );
755 // Move working range to desired, pre-computed position.
756 range
.moveToBookmark( that
.targetBookmark
);
758 // Make sure range is always anchored in an element. For consistency.
761 // It my happen that the uncollapsed range which referred to a valid selection,
762 // will be placed in an uneditable location after being collapsed:
763 // <tr>[<td>x</td>]</tr> -> <tr>[]<td>x</td></tr> -> <tr><td>[]x</td></tr>
764 helpers
.fixUneditableRangePosition( range
);
766 // Execute content-specific post-extract routines.
767 helpers
.list
.merge( that
, this );
768 helpers
.table
.purge( that
, this );
769 helpers
.block
.merge( that
, this );
771 // Remove empty block, duh!
772 if ( removeEmptyBlock
) {
773 var path
= range
.startPath();
775 // <p><b>^</b></p> is empty block.
777 range
.checkStartOfBlock() &&
778 range
.checkEndOfBlock() &&
780 !range
.root
.equals( path
.block
) &&
781 // Do not remove a block with bookmarks. (http://dev.ckeditor.com/ticket/13465)
782 !hasBookmarks( path
.block
) ) {
783 range
.moveToPosition( path
.block
, CKEDITOR
.POSITION_BEFORE_START
);
787 // Auto paragraph, if needed.
788 helpers
.autoParagraph( this.editor
, range
);
790 // Let's have a bogus next to the caret, if needed.
791 if ( isEmpty( range
.startContainer
) )
792 range
.startContainer
.appendBogus();
795 // Merge inline siblings if any around the caret.
796 range
.startContainer
.mergeSiblings();
798 return extractedFragment
;
802 * Editable element bootstrapping.
807 var editor
= this.editor
;
809 // Handle the load/read of editor data/snapshot.
810 this.attachListener( editor
, 'beforeGetData', function() {
811 var data
= this.getData();
813 // Post processing html output of wysiwyg editable.
814 if ( !this.is( 'textarea' ) ) {
815 // Reset empty if the document contains only one empty paragraph.
816 if ( editor
.config
.ignoreEmptyParagraph
!== false )
817 data
= data
.replace( emptyParagraphRegexp
, function( match
, lookback
) {
822 editor
.setData( data
, null, 1 );
825 this.attachListener( editor
, 'getSnapshot', function( evt
) {
826 evt
.data
= this.getData( 1 );
829 this.attachListener( editor
, 'afterSetData', function() {
830 this.setData( editor
.getData( 1 ) );
832 this.attachListener( editor
, 'loadSnapshot', function( evt
) {
833 this.setData( evt
.data
, 1 );
836 // Delegate editor focus/blur to editable.
837 this.attachListener( editor
, 'beforeFocus', function() {
838 var sel
= editor
.getSelection(),
839 ieSel
= sel
&& sel
.getNative();
841 // IE considers control-type element as separate
842 // focus host when selected, avoid destroying the
843 // selection in such case. (http://dev.ckeditor.com/ticket/5812) (http://dev.ckeditor.com/ticket/8949)
844 if ( ieSel
&& ieSel
.type
== 'Control' )
850 this.attachListener( editor
, 'insertHtml', function( evt
) {
851 this.insertHtml( evt
.data
.dataValue
, evt
.data
.mode
, evt
.data
.range
);
853 this.attachListener( editor
, 'insertElement', function( evt
) {
854 this.insertElement( evt
.data
);
856 this.attachListener( editor
, 'insertText', function( evt
) {
857 this.insertText( evt
.data
);
860 // Update editable state.
861 this.setReadOnly( editor
.readOnly
);
863 // The editable class.
864 this.attachClass( 'cke_editable' );
866 // The element mode css class.
867 if ( editor
.elementMode
== CKEDITOR
.ELEMENT_MODE_INLINE
) {
868 this.attachClass( 'cke_editable_inline' );
869 } else if ( editor
.elementMode
== CKEDITOR
.ELEMENT_MODE_REPLACE
||
870 editor
.elementMode
== CKEDITOR
.ELEMENT_MODE_APPENDTO
) {
871 this.attachClass( 'cke_editable_themed' );
874 this.attachClass( 'cke_contents_' + editor
.config
.contentsLangDirection
);
876 // Setup editor keystroke handlers on this element.
877 var keystrokeHandler
= editor
.keystrokeHandler
;
879 // If editor is read-only, then make sure that BACKSPACE key
880 // is blocked to prevent browser history navigation.
881 keystrokeHandler
.blockedKeystrokes
[ 8 ] = +editor
.readOnly
;
883 editor
.keystrokeHandler
.attach( this );
885 // Update focus states.
886 this.on( 'blur', function() {
887 this.hasFocus
= false;
890 this.on( 'focus', function() {
891 this.hasFocus
= true;
894 if ( CKEDITOR
.env
.webkit
) {
895 // [WebKit] Save scrollTop value so it can be used when restoring locked selection. (http://dev.ckeditor.com/ticket/14659)
896 this.on( 'scroll', function() {
897 editor
._
.previousScrollTop
= editor
.editable().$.scrollTop
;
901 // [Edge] This is the other part of the workaround for Edge which restores saved
902 // scrollTop value and removes listener which is not needed anymore. (http://dev.ckeditor.com/ticket/14825)
903 if ( CKEDITOR
.env
.edge
&& CKEDITOR
.env
.version
> 14 ) {
905 var fixScrollOnFocus = function() {
906 var editable
= editor
.editable();
908 if ( editor
._
.previousScrollTop
!= null && editable
.getDocument().equals( CKEDITOR
.document
) ) {
909 editable
.$.scrollTop
= editor
._
.previousScrollTop
;
910 editor
._
.previousScrollTop
= null;
911 this.removeListener( 'scroll', fixScrollOnFocus
);
915 this.on( 'scroll', fixScrollOnFocus
);
918 // Register to focus manager.
919 editor
.focusManager
.add( this );
921 // Inherit the initial focus on editable element.
922 if ( this.equals( CKEDITOR
.document
.getActive() ) ) {
923 this.hasFocus
= true;
924 // Pending until this editable has attached.
925 editor
.once( 'contentDom', function() {
926 editor
.focusManager
.focus( this );
930 // Apply tab index on demand, with original direction saved.
931 if ( this.isInline() ) {
933 // tabIndex of the editable is different than editor's one.
934 // Update the attribute of the editable.
935 this.changeAttr( 'tabindex', editor
.tabIndex
);
938 // The above is all we'll be doing for a <textarea> editable.
939 if ( this.is( 'textarea' ) )
942 // The DOM document which the editing acts upon.
943 editor
.document
= this.getDocument();
944 editor
.window
= this.getWindow();
946 var doc
= editor
.document
;
948 this.changeAttr( 'spellcheck', !editor
.config
.disableNativeSpellChecker
);
950 // Apply contents direction on demand, with original direction saved.
951 var dir
= editor
.config
.contentsLangDirection
;
952 if ( this.getDirection( 1 ) != dir
)
953 this.changeAttr( 'dir', dir
);
955 // Create the content stylesheet for this document.
956 var styles
= CKEDITOR
.getCss();
958 var head
= doc
.getHead(),
959 stylesElement
= head
.getCustomData( 'stylesheet' );
961 if ( !stylesElement
) {
962 var sheet
= doc
.appendStyleText( styles
);
963 sheet
= new CKEDITOR
.dom
.element( sheet
.ownerNode
|| sheet
.owningElement
);
964 head
.setCustomData( 'stylesheet', sheet
);
965 sheet
.data( 'cke-temp', 1 );
966 } else if ( styles
!= stylesElement
.getText() ) {
967 CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 9 ? stylesElement
.$.styleSheet
.cssText
= styles : stylesElement
.setText( styles
);
971 // Update the stylesheet sharing count.
972 var ref
= doc
.getCustomData( 'stylesheet_ref' ) || 0;
973 doc
.setCustomData( 'stylesheet_ref', ref
+ 1 );
975 // Pass this configuration to styles system.
976 this.setCustomData( 'cke_includeReadonly', !editor
.config
.disableReadonlyStyling
);
978 // Prevent the browser opening read-only links. (http://dev.ckeditor.com/ticket/6032 & http://dev.ckeditor.com/ticket/10912)
979 this.attachListener( this, 'click', function( evt
) {
982 var link
= new CKEDITOR
.dom
.elementPath( evt
.getTarget(), this ).contains( 'a' );
984 if ( link
&& evt
.$.button
!= 2 && link
.isReadOnly() )
985 evt
.preventDefault();
988 var backspaceOrDelete
= { 8: 1, 46: 1 };
990 // Override keystrokes which should have deletion behavior
991 // on fully selected element . (http://dev.ckeditor.com/ticket/4047) (http://dev.ckeditor.com/ticket/7645)
992 this.attachListener( editor
, 'key', function( evt
) {
993 if ( editor
.readOnly
)
996 // Use getKey directly in order to ignore modifiers.
997 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
998 var keyCode
= evt
.data
.domEvent
.getKey(),
1001 // Prevent of reading path of empty range (http://dev.ckeditor.com/ticket/13096, #457).
1002 var sel
= editor
.getSelection();
1003 if ( sel
.getRanges().length
=== 0 ) {
1007 // Backspace OR Delete.
1008 if ( keyCode
in backspaceOrDelete
) {
1010 range
= sel
.getRanges()[ 0 ],
1011 path
= range
.startPath(),
1019 // [IE<11] Remove selected image/anchor/etc here to avoid going back in history. (http://dev.ckeditor.com/ticket/10055)
1020 ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 11 && ( selected
= sel
.getSelectedElement() ) ) ||
1021 // Remove the entire list/table on fully selected content. (http://dev.ckeditor.com/ticket/7645)
1022 ( selected
= getSelectedTableList( sel
) ) ) {
1023 // Make undo snapshot.
1024 editor
.fire( 'saveSnapshot' );
1026 // Delete any element that 'hasLayout' (e.g. hr,table) in IE8 will
1027 // break up the selection, safely manage it here. (http://dev.ckeditor.com/ticket/4795)
1028 range
.moveToPosition( selected
, CKEDITOR
.POSITION_BEFORE_START
);
1029 // Remove the control manually.
1033 editor
.fire( 'saveSnapshot' );
1036 } else if ( range
.collapsed
) {
1037 // Handle the following special cases: (http://dev.ckeditor.com/ticket/6217)
1038 // 1. Del/Backspace key before/after table;
1039 // 2. Backspace Key after start of table.
1040 if ( ( block
= path
.block
) &&
1041 ( next
= block
[ rtl
? 'getPrevious' : 'getNext' ]( isNotWhitespace
) ) &&
1042 ( next
.type
== CKEDITOR
.NODE_ELEMENT
) &&
1043 next
.is( 'table' ) &&
1044 range
[ rtl
? 'checkStartOfBlock' : 'checkEndOfBlock' ]() ) {
1045 editor
.fire( 'saveSnapshot' );
1047 // Remove the current empty block.
1048 if ( range
[ rtl
? 'checkEndOfBlock' : 'checkStartOfBlock' ]() )
1051 // Move cursor to the beginning/end of table cell.
1052 range
[ 'moveToElementEdit' + ( rtl
? 'End' : 'Start' ) ]( next
);
1055 editor
.fire( 'saveSnapshot' );
1059 else if ( path
.blockLimit
&& path
.blockLimit
.is( 'td' ) &&
1060 ( parent
= path
.blockLimit
.getAscendant( 'table' ) ) &&
1061 range
.checkBoundaryOfElement( parent
, rtl
? CKEDITOR
.START : CKEDITOR
.END
) &&
1062 ( next
= parent
[ rtl
? 'getPrevious' : 'getNext' ]( isNotWhitespace
) ) ) {
1063 editor
.fire( 'saveSnapshot' );
1065 // Move cursor to the end of previous block.
1066 range
[ 'moveToElementEdit' + ( rtl
? 'End' : 'Start' ) ]( next
);
1068 // Remove any previous empty block.
1069 if ( range
.checkStartOfBlock() && range
.checkEndOfBlock() )
1074 editor
.fire( 'saveSnapshot' );
1078 // BACKSPACE/DEL pressed at the start/end of table cell.
1079 else if ( ( parent
= path
.contains( [ 'td', 'th', 'caption' ] ) ) &&
1080 range
.checkBoundaryOfElement( parent
, rtl
? CKEDITOR
.START : CKEDITOR
.END
) ) {
1090 // On IE>=11 we need to fill blockless editable with <br> if it was deleted.
1091 if ( editor
.blockless
&& CKEDITOR
.env
.ie
&& CKEDITOR
.env
.needsBrFiller
) {
1092 this.attachListener( this, 'keyup', function( evt
) {
1093 if ( evt
.data
.getKeystroke() in backspaceOrDelete
&& !this.getFirst( isNotEmpty
) ) {
1096 // Set the selection before bogus, because IE tends to put it after.
1097 var range
= editor
.createRange();
1098 range
.moveToPosition( this, CKEDITOR
.POSITION_AFTER_START
);
1104 this.attachListener( this, 'dblclick', function( evt
) {
1105 if ( editor
.readOnly
)
1108 var data
= { element: evt
.data
.getTarget() };
1109 editor
.fire( 'doubleclick', data
);
1112 // Prevent automatic submission in IE http://dev.ckeditor.com/ticket/6336
1113 CKEDITOR
.env
.ie
&& this.attachListener( this, 'click', blockInputClick
);
1115 // Gecko/Webkit need some help when selecting control type elements. (http://dev.ckeditor.com/ticket/3448)
1116 // We apply same behavior for IE Edge. (http://dev.ckeditor.com/ticket/13386)
1117 if ( !CKEDITOR
.env
.ie
|| CKEDITOR
.env
.edge
) {
1118 this.attachListener( this, 'mousedown', function( ev
) {
1119 var control
= ev
.data
.getTarget();
1120 // http://dev.ckeditor.com/ticket/11727. Note: htmlDP assures that input/textarea/select have contenteditable=false
1121 // attributes. However, they also have data-cke-editable attribute, so isReadOnly() returns false,
1122 // and therefore those elements are correctly selected by this code.
1123 if ( control
.is( 'img', 'hr', 'input', 'textarea', 'select' ) && !control
.isReadOnly() ) {
1124 editor
.getSelection().selectElement( control
);
1126 // Prevent focus from stealing from the editable. (http://dev.ckeditor.com/ticket/9515)
1127 if ( control
.is( 'input', 'textarea', 'select' ) )
1128 ev
.data
.preventDefault();
1133 // For some reason, after click event is done, IE Edge loses focus on the selected element. (http://dev.ckeditor.com/ticket/13386)
1134 if ( CKEDITOR
.env
.edge
) {
1135 this.attachListener( this, 'mouseup', function( ev
) {
1136 var selectedElement
= ev
.data
.getTarget();
1137 if ( selectedElement
&& selectedElement
.is( 'img' ) ) {
1138 editor
.getSelection().selectElement( selectedElement
);
1143 // Prevent right click from selecting an empty block even
1144 // when selection is anchored inside it. (http://dev.ckeditor.com/ticket/5845)
1145 if ( CKEDITOR
.env
.gecko
) {
1146 this.attachListener( this, 'mouseup', function( ev
) {
1147 if ( ev
.data
.$.button
== 2 ) {
1148 var target
= ev
.data
.getTarget();
1150 if ( !target
.getOuterHtml().replace( emptyParagraphRegexp
, '' ) ) {
1151 var range
= editor
.createRange();
1152 range
.moveToElementEditStart( target
);
1153 range
.select( true );
1159 // Webkit: avoid from editing form control elements content.
1160 if ( CKEDITOR
.env
.webkit
) {
1161 // Prevent from tick checkbox/radiobox/select
1162 this.attachListener( this, 'click', function( ev
) {
1163 if ( ev
.data
.getTarget().is( 'input', 'select' ) )
1164 ev
.data
.preventDefault();
1167 // Prevent from editig textfield/textarea value.
1168 this.attachListener( this, 'mouseup', function( ev
) {
1169 if ( ev
.data
.getTarget().is( 'input', 'textarea' ) )
1170 ev
.data
.preventDefault();
1174 // Prevent Webkit/Blink from going rogue when joining
1175 // blocks on BACKSPACE/DEL (http://dev.ckeditor.com/ticket/11861,http://dev.ckeditor.com/ticket/9998).
1176 if ( CKEDITOR
.env
.webkit
) {
1177 this.attachListener( editor
, 'key', function( evt
) {
1178 if ( editor
.readOnly
) {
1182 // Use getKey directly in order to ignore modifiers.
1183 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
1184 var key
= evt
.data
.domEvent
.getKey();
1186 if ( !( key
in backspaceOrDelete
) )
1189 // Prevent of reading path of empty range (http://dev.ckeditor.com/ticket/13096, #457).
1190 var sel
= editor
.getSelection();
1191 if ( sel
.getRanges().length
=== 0 ) {
1195 var backspace
= key
== 8,
1196 range
= sel
.getRanges()[ 0 ],
1197 startPath
= range
.startPath();
1199 if ( range
.collapsed
) {
1200 if ( !mergeBlocksCollapsedSelection( editor
, range
, backspace
, startPath
) )
1203 if ( !mergeBlocksNonCollapsedSelection( editor
, range
, startPath
) )
1207 // Scroll to the new position of the caret (http://dev.ckeditor.com/ticket/11960).
1208 editor
.getSelection().scrollIntoView();
1209 editor
.fire( 'saveSnapshot' );
1212 }, this, null, 100 ); // Later is better – do not override existing listeners.
1218 detach: function() {
1219 // Update the editor cached data with current data.
1220 this.editor
.setData( this.editor
.getData(), 0, 1 );
1222 this.clearListeners();
1223 this.restoreAttrs();
1225 // Cleanup our custom classes.
1227 if ( ( classes
= this.removeCustomData( 'classes' ) ) ) {
1228 while ( classes
.length
)
1229 this.removeClass( classes
.pop() );
1232 // Remove contents stylesheet from document if it's the last usage.
1233 if ( !this.is( 'textarea' ) ) {
1234 var doc
= this.getDocument(),
1235 head
= doc
.getHead();
1236 if ( head
.getCustomData( 'stylesheet' ) ) {
1237 var refs
= doc
.getCustomData( 'stylesheet_ref' );
1238 if ( !( --refs
) ) {
1239 doc
.removeCustomData( 'stylesheet_ref' );
1240 var sheet
= head
.removeCustomData( 'stylesheet' );
1243 doc
.setCustomData( 'stylesheet_ref', refs
);
1248 this.editor
.fire( 'contentDomUnload' );
1250 // Free up the editor reference.
1257 * Creates, retrieves or detaches an editable element of the editor.
1258 * This method should always be used instead of calling {@link CKEDITOR.editable} directly.
1261 * @member CKEDITOR.editor
1262 * @param {CKEDITOR.dom.element/CKEDITOR.editable} [elementOrEditable] The
1263 * DOM element to become the editable or a {@link CKEDITOR.editable} object.
1264 * @returns {CKEDITOR.dom.element/null} The editor's editable element, or `null` if not available.
1266 CKEDITOR
.editor
.prototype.editable = function( element
) {
1267 var editable
= this._
.editable
;
1269 // This editor has already associated with
1270 // an editable element, silently fails.
1271 if ( editable
&& element
)
1274 if ( arguments
.length
) {
1275 editable
= this._
.editable
= element
? ( element
instanceof CKEDITOR
.editable
? element : new CKEDITOR
.editable( this, element
) ) :
1276 // Detach the editable from editor.
1277 ( editable
&& editable
.detach(), null );
1280 // Just retrieve the editable.
1284 CKEDITOR
.on( 'instanceLoaded', function( evt
) {
1285 var editor
= evt
.editor
;
1287 // and flag that the element was locked by our code so it'll be editable by the editor functions (http://dev.ckeditor.com/ticket/6046).
1288 editor
.on( 'insertElement', function( evt
) {
1289 var element
= evt
.data
;
1290 if ( element
.type
== CKEDITOR
.NODE_ELEMENT
&& ( element
.is( 'input' ) || element
.is( 'textarea' ) ) ) {
1291 // // The element is still not inserted yet, force attribute-based check.
1292 if ( element
.getAttribute( 'contentEditable' ) != 'false' )
1293 element
.data( 'cke-editable', element
.hasAttribute( 'contenteditable' ) ? 'true' : '1' );
1294 element
.setAttribute( 'contentEditable', false );
1298 editor
.on( 'selectionChange', function( evt
) {
1299 if ( editor
.readOnly
)
1302 // Auto fixing on some document structure weakness to enhance usabilities. (http://dev.ckeditor.com/ticket/3190 and http://dev.ckeditor.com/ticket/3189)
1303 var sel
= editor
.getSelection();
1304 // Do it only when selection is not locked. (http://dev.ckeditor.com/ticket/8222)
1305 if ( sel
&& !sel
.isLocked
) {
1306 var isDirty
= editor
.checkDirty();
1308 // Lock undoM before touching DOM to prevent
1309 // recording these changes as separate snapshot.
1310 editor
.fire( 'lockSnapshot' );
1312 editor
.fire( 'unlockSnapshot' );
1314 !isDirty
&& editor
.resetDirty();
1319 CKEDITOR
.on( 'instanceCreated', function( evt
) {
1320 var editor
= evt
.editor
;
1322 editor
.on( 'mode', function() {
1324 var editable
= editor
.editable();
1326 // Setup proper ARIA roles and properties for inline editable, classic
1327 // (iframe-based) editable is instead handled by plugin.
1328 if ( editable
&& editable
.isInline() ) {
1330 var ariaLabel
= editor
.title
;
1332 editable
.changeAttr( 'role', 'textbox' );
1333 editable
.changeAttr( 'aria-label', ariaLabel
);
1336 editable
.changeAttr( 'title', ariaLabel
);
1338 var helpLabel
= editor
.fire( 'ariaEditorHelpLabel', {} ).label
;
1340 // Put the voice label in different spaces, depending on element mode, so
1341 // the DOM element get auto detached on mode reload or editor destroy.
1342 var ct
= this.ui
.space( this.elementMode
== CKEDITOR
.ELEMENT_MODE_INLINE
? 'top' : 'contents' );
1344 var ariaDescId
= CKEDITOR
.tools
.getNextId(),
1345 desc
= CKEDITOR
.dom
.element
.createFromHtml( '<span id="' + ariaDescId
+ '" class="cke_voice_label">' + helpLabel
+ '</span>' );
1347 editable
.changeAttr( 'aria-describedby', ariaDescId
);
1354 // http://dev.ckeditor.com/ticket/9222: Show text cursor in Gecko.
1355 // Show default cursor over control elements on all non-IEs.
1356 CKEDITOR
.addCss( '.cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}' );
1360 // Bazillion helpers for the editable class and above listeners.
1364 isNotWhitespace
= CKEDITOR
.dom
.walker
.whitespaces( true ),
1365 isNotBookmark
= CKEDITOR
.dom
.walker
.bookmark( false, true ),
1366 isEmpty
= CKEDITOR
.dom
.walker
.empty(),
1367 isBogus
= CKEDITOR
.dom
.walker
.bogus(),
1368 // Matching an empty paragraph at the end of document.
1369 emptyParagraphRegexp
= /(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>| |\u00A0| )?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi;
1371 // Auto-fixing block-less content by wrapping paragraph (http://dev.ckeditor.com/ticket/3190), prevent
1372 // non-exitable-block by padding extra br.(http://dev.ckeditor.com/ticket/3189)
1373 // Returns truly value when dom was changed, falsy otherwise.
1374 function fixDom( evt
) {
1375 var editor
= evt
.editor
,
1376 path
= evt
.data
.path
,
1377 blockLimit
= path
.blockLimit
,
1378 selection
= evt
.data
.selection
,
1379 range
= selection
.getRanges()[ 0 ],
1380 selectionUpdateNeeded
;
1382 if ( CKEDITOR
.env
.gecko
|| ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.needsBrFiller
) ) {
1383 var blockNeedsFiller
= needsBrFiller( selection
, path
);
1384 if ( blockNeedsFiller
) {
1385 blockNeedsFiller
.appendBogus();
1386 // IE tends to place selection after appended bogus, so we need to
1387 // select the original range (placed before bogus).
1388 selectionUpdateNeeded
= CKEDITOR
.env
.ie
;
1392 // When we're in block enter mode, a new paragraph will be established
1393 // to encapsulate inline contents inside editable. (http://dev.ckeditor.com/ticket/3657)
1394 // Don't autoparagraph if browser (namely - IE) incorrectly anchored selection
1395 // inside non-editable content. This happens e.g. if non-editable block is the only
1396 // content of editable.
1397 if ( shouldAutoParagraph( editor
, path
.block
, blockLimit
) && range
.collapsed
&& !range
.getCommonAncestor().isReadOnly() ) {
1398 var testRng
= range
.clone();
1399 testRng
.enlarge( CKEDITOR
.ENLARGE_BLOCK_CONTENTS
);
1400 var walker
= new CKEDITOR
.dom
.walker( testRng
);
1401 walker
.guard = function( node
) {
1402 return !isNotEmpty( node
) ||
1403 node
.type
== CKEDITOR
.NODE_COMMENT
||
1407 // 1. Inline content discovered under cursor;
1408 // 2. Empty editable.
1409 if ( !walker
.checkForward() || testRng
.checkStartOfBlock() && testRng
.checkEndOfBlock() ) {
1410 var fixedBlock
= range
.fixBlock( true, editor
.activeEnterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p' );
1412 // For IE<11, we should remove any filler node which was introduced before.
1413 if ( !CKEDITOR
.env
.needsBrFiller
) {
1414 var first
= fixedBlock
.getFirst( isNotEmpty
);
1415 if ( first
&& isNbsp( first
) )
1419 selectionUpdateNeeded
= 1;
1421 // Cancel this selection change in favor of the next (correct). (http://dev.ckeditor.com/ticket/6811)
1426 if ( selectionUpdateNeeded
)
1430 // Checks whether current selection requires br filler to be appended.
1431 // @returns Block which needs filler or falsy value.
1432 function needsBrFiller( selection
, path
) {
1433 // Fake selection does not need filler, because it is fake.
1434 if ( selection
.isFake
)
1437 // Ensure bogus br could help to move cursor (out of styles) to the end of block. (http://dev.ckeditor.com/ticket/7041)
1438 var pathBlock
= path
.block
|| path
.blockLimit
,
1439 lastNode
= pathBlock
&& pathBlock
.getLast( isNotEmpty
);
1441 // Check some specialities of the current path block:
1442 // 1. It is really displayed as block; (http://dev.ckeditor.com/ticket/7221)
1443 // 2. It doesn't end with one inner block; (http://dev.ckeditor.com/ticket/7467)
1444 // 3. It doesn't have bogus br yet.
1446 pathBlock
&& pathBlock
.isBlockBoundary() &&
1447 !( lastNode
&& lastNode
.type
== CKEDITOR
.NODE_ELEMENT
&& lastNode
.isBlockBoundary() ) &&
1448 !pathBlock
.is( 'pre' ) && !pathBlock
.getBogus()
1453 function blockInputClick( evt
) {
1454 var element
= evt
.data
.getTarget();
1455 if ( element
.is( 'input' ) ) {
1456 var type
= element
.getAttribute( 'type' );
1457 if ( type
== 'submit' || type
== 'reset' )
1458 evt
.data
.preventDefault();
1462 function isNotEmpty( node
) {
1463 return isNotWhitespace( node
) && isNotBookmark( node
);
1466 function isNbsp( node
) {
1467 return node
.type
== CKEDITOR
.NODE_TEXT
&& CKEDITOR
.tools
.trim( node
.getText() ).match( /^(?: |\xa0)$/ );
1470 function isNotBubbling( fn
, src
) {
1471 return function( evt
) {
1472 var other
= evt
.data
.$.toElement
|| evt
.data
.$.fromElement
|| evt
.data
.$.relatedTarget
;
1474 // First of all, other may simply be null/undefined.
1475 // Second of all, at least early versions of Spartan returned empty objects from evt.relatedTarget,
1476 // so let's also check the node type.
1477 other
= ( other
&& other
.nodeType
== CKEDITOR
.NODE_ELEMENT
) ? new CKEDITOR
.dom
.element( other
) : null;
1479 if ( !( other
&& ( src
.equals( other
) || src
.contains( other
) ) ) )
1480 fn
.call( this, evt
);
1484 function hasBookmarks( element
) {
1485 // We use getElementsByTag() instead of find() to retain compatibility with IE quirks mode.
1486 var potentialBookmarks
= element
.getElementsByTag( 'span' ),
1490 if ( potentialBookmarks
) {
1491 while ( ( child
= potentialBookmarks
.getItem( i
++ ) ) ) {
1492 if ( !isNotBookmark( child
) ) {
1501 // Check if the entire table/list contents is selected.
1502 function getSelectedTableList( sel
) {
1504 range
= sel
.getRanges()[ 0 ],
1505 editable
= sel
.root
,
1506 path
= range
.startPath(),
1507 structural
= { table: 1, ul: 1, ol: 1, dl: 1 };
1509 if ( path
.contains( structural
) ) {
1510 // Clone the original range.
1511 var walkerRng
= range
.clone();
1513 // Enlarge the range: X<ul><li>[Y]</li></ul>X => [X<ul><li>]Y</li></ul>X
1514 walkerRng
.collapse( 1 );
1515 walkerRng
.setStartAt( editable
, CKEDITOR
.POSITION_AFTER_START
);
1517 // Create a new walker.
1518 var walker
= new CKEDITOR
.dom
.walker( walkerRng
);
1520 // Assign a new guard to the walker.
1521 walker
.guard
= guard();
1523 // Go backwards checking for selected structural node.
1524 walker
.checkBackward();
1526 // If there's a selected structured element when checking backwards,
1527 // then check the same forwards.
1529 // Clone the original range.
1530 walkerRng
= range
.clone();
1532 // Enlarge the range (assuming <ul> is selected element from guard):
1534 // X<ul><li>[Y]</li></ul>X => X<ul><li>Y[</li></ul>]X
1536 // If the walker went deeper down DOM than a while ago when traversing
1537 // backwards, then it doesn't make sense: an element must be selected
1538 // symmetrically. By placing range end **after previously selected node**,
1539 // we make sure we don't go no deeper in DOM when going forwards.
1540 walkerRng
.collapse();
1541 walkerRng
.setEndAt( selected
, CKEDITOR
.POSITION_AFTER_END
);
1543 // Create a new walker.
1544 walker
= new CKEDITOR
.dom
.walker( walkerRng
);
1546 // Assign a new guard to the walker.
1547 walker
.guard
= guard( true );
1549 // Reset selected node.
1552 // Go forwards checking for selected structural node.
1553 walker
.checkForward();
1561 function guard( forwardGuard
) {
1562 return function( node
, isWalkOut
) {
1563 // Save the encountered node as selected if going down the DOM structure
1564 // and the node is structured element.
1565 if ( isWalkOut
&& node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( structural
) )
1568 // Stop the walker when either traversing another non-empty node at the same
1569 // DOM level as in previous step.
1570 // NOTE: When going forwards, stop if encountered a bogus.
1571 if ( !isWalkOut
&& isNotEmpty( node
) && !( forwardGuard
&& isBogus( node
) ) )
1577 // Whether in given context (pathBlock, pathBlockLimit and editor settings)
1578 // editor should automatically wrap inline contents with blocks.
1579 function shouldAutoParagraph( editor
, pathBlock
, pathBlockLimit
) {
1580 // Check whether pathBlock equals pathBlockLimit to support nested editable (http://dev.ckeditor.com/ticket/12162).
1581 return editor
.config
.autoParagraph
!== false &&
1582 editor
.activeEnterMode
!= CKEDITOR
.ENTER_BR
&&
1584 ( editor
.editable().equals( pathBlockLimit
) && !pathBlock
) ||
1585 ( pathBlock
&& pathBlock
.getAttribute( 'contenteditable' ) == 'true' )
1589 function autoParagraphTag( editor
) {
1590 return ( editor
.activeEnterMode
!= CKEDITOR
.ENTER_BR
&& editor
.config
.autoParagraph
!== false ) ? editor
.activeEnterMode
== CKEDITOR
.ENTER_DIV
? 'div' : 'p' : false;
1594 // Functions related to insertXXX methods
1596 insert
= ( function() {
1599 var DTD
= CKEDITOR
.dtd
;
1601 // Inserts the given (valid) HTML into the range position (with range content deleted),
1602 // guarantee it's result to be a valid DOM tree.
1603 function insert( editable
, type
, data
, range
) {
1604 var editor
= editable
.editor
,
1607 if ( type
== 'unfiltered_html' ) {
1612 // Check range spans in non-editable.
1613 if ( range
.checkReadOnly() )
1616 // RANGE PREPARATIONS
1618 var path
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, range
.root
),
1619 // Let root be the nearest block that's impossible to be split
1620 // during html processing.
1621 blockLimit
= path
.blockLimit
|| range
.root
,
1622 // The "state" value.
1625 dontFilter: dontFilter
,
1629 blockLimit: blockLimit
,
1630 // During pre-processing / preparations startContainer of affectedRange should be placed
1631 // in this element in which inserted or moved (in case when we merge blocks) content
1632 // could create situation that will need merging inline elements.
1634 // <div><b>A</b>^B</div> + <b>C</b> => <div><b>A</b><b>C</b>B</div> - affected container is <div>.
1635 // <p><b>A[B</b></p><p><b>C]D</b></p> + E => <p><b>AE</b></p><p><b>D</b></p> =>
1636 // <p><b>AE</b><b>D</b></p> - affected container is <p> (in text mode).
1637 mergeCandidates: [],
1641 prepareRangeToDataInsertion( that
);
1645 // Select range and stop execution.
1646 // If data has been totally emptied after the filtering,
1647 // any insertion is pointless (http://dev.ckeditor.com/ticket/10339).
1648 if ( data
&& processDataForInsertion( that
, data
) ) {
1650 insertDataIntoRange( that
);
1654 // Set final range position and clean up.
1656 cleanupAfterInsertion( that
);
1659 // Prepare range to its data deletion.
1660 // Delete its contents.
1661 // Prepare it to insertion.
1662 function prepareRangeToDataInsertion( that
) {
1663 var range
= that
.range
,
1664 mergeCandidates
= that
.mergeCandidates
,
1665 node
, marker
, path
, startPath
, endPath
, previous
, bm
;
1667 // If range starts in inline element then insert a marker, so empty
1668 // inline elements won't be removed while range.deleteContents
1669 // and we will be able to move range back into this element.
1670 // E.g. 'aa<b>[bb</b>]cc' -> (after deleting) 'aa<b><span/></b>cc'
1671 if ( that
.type
== 'text' && range
.shrink( CKEDITOR
.SHRINK_ELEMENT
, true, false ) ) {
1672 marker
= CKEDITOR
.dom
.element
.createFromHtml( '<span> </span>', range
.document
);
1673 range
.insertNode( marker
);
1674 range
.setStartAfter( marker
);
1677 // By using path we can recover in which element was startContainer
1678 // before deleting contents.
1679 // Start and endPathElements will be used to squash selected blocks, after removing
1680 // selection contents. See rule 5.
1681 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
);
1682 that
.endPath
= endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
);
1684 if ( !range
.collapsed
) {
1685 // Anticipate the possibly empty block at the end of range after deletion.
1686 node
= endPath
.block
|| endPath
.blockLimit
;
1687 var ancestor
= range
.getCommonAncestor();
1688 if ( node
&& !( node
.equals( ancestor
) || node
.contains( ancestor
) ) && range
.checkEndOfBlock() ) {
1689 that
.zombies
.push( node
);
1692 range
.deleteContents();
1696 // Move range into the previous block.
1698 ( previous
= getRangePrevious( range
) ) && checkIfElement( previous
) && previous
.isBlockBoundary() &&
1699 // Check if previousNode was parent of range's startContainer before deleteContents.
1700 startPath
.contains( previous
)
1702 range
.moveToPosition( previous
, CKEDITOR
.POSITION_BEFORE_END
);
1705 mergeAncestorElementsOfSelectionEnds( range
, that
.blockLimit
, startPath
, endPath
);
1709 // If marker was created then move collapsed range into its place.
1710 range
.setEndBefore( marker
);
1715 // Split inline elements so HTML will be inserted with its own styles.
1716 path
= range
.startPath();
1717 if ( ( node
= path
.contains( isInline
, false, 1 ) ) ) {
1718 range
.splitElement( node
);
1719 that
.inlineStylesRoot
= node
;
1720 that
.inlineStylesPeak
= path
.lastElement
;
1723 // Record inline merging candidates for later cleanup in place.
1724 bm
= range
.createBookmark();
1726 // 1. Inline siblings.
1727 node
= bm
.startNode
.getPrevious( isNotEmpty
);
1728 node
&& checkIfElement( node
) && isInline( node
) && mergeCandidates
.push( node
);
1729 node
= bm
.startNode
.getNext( isNotEmpty
);
1730 node
&& checkIfElement( node
) && isInline( node
) && mergeCandidates
.push( node
);
1732 // 2. Inline parents.
1733 node
= bm
.startNode
;
1734 while ( ( node
= node
.getParent() ) && isInline( node
) )
1735 mergeCandidates
.push( node
);
1737 range
.moveToBookmark( bm
);
1740 function processDataForInsertion( that
, data
) {
1741 var range
= that
.range
;
1743 // Rule 8. - wrap entire data in inline styles.
1744 // (e.g. <p><b>x^z</b></p> + <p>a</p><p>b</p> -> <b><p>a</p><p>b</p></b>)
1745 // Incorrect tags order will be fixed by htmlDataProcessor.
1746 if ( that
.type
== 'text' && that
.inlineStylesRoot
)
1747 data
= wrapDataWithInlineStyles( data
, that
);
1750 var context
= that
.blockLimit
.getName();
1752 // Wrap data to be inserted, to avoid losing leading whitespaces
1753 // when going through the below procedure.
1754 if ( /^\s+|\s+$/.test( data
) && 'span' in CKEDITOR
.dtd
[ context
] ) {
1755 var protect
= '<span data-cke-marker="1"> </span>';
1756 data
= protect
+ data
+ protect
;
1759 // Process the inserted html, in context of the insertion root.
1760 // Don't use the "fix for body" feature as auto paragraphing must
1761 // be handled during insertion.
1762 data
= that
.editor
.dataProcessor
.toHtml( data
, {
1765 protectedWhitespaces: !!protect
,
1766 dontFilter: that
.dontFilter
,
1767 // Use the current, contextual settings.
1768 filter: that
.editor
.activeFilter
,
1769 enterMode: that
.editor
.activeEnterMode
1773 // Build the node list for insertion.
1774 var doc
= range
.document
,
1775 wrapper
= doc
.createElement( 'body' );
1777 wrapper
.setHtml( data
);
1779 // Eventually remove the temporaries.
1781 wrapper
.getFirst().remove();
1782 wrapper
.getLast().remove();
1786 var block
= range
.startPath().block
;
1787 if ( block
&& // Apply when there exists path block after deleting selection's content...
1788 !( block
.getChildCount() == 1 && block
.getBogus() ) ) { // ... and the only content of this block isn't a bogus.
1789 stripBlockTagIfSingleLine( wrapper
);
1792 that
.dataWrapper
= wrapper
;
1797 function insertDataIntoRange( that
) {
1798 var range
= that
.range
,
1799 doc
= range
.document
,
1801 blockLimit
= that
.blockLimit
,
1802 nodesData
, nodeData
, node
,
1805 bogusNeededBlocks
= [],
1806 pathBlock
, fixBlock
,
1807 splittingContainer
= 0,
1809 insertionContainer
, toSplit
, newContainer
,
1810 startContainer
= range
.startContainer
,
1811 endContainer
= that
.endPath
.elements
[ 0 ],
1813 // If endContainer was merged into startContainer: <p>a[b</p><p>c]d</p>
1814 // or it's equal to startContainer: <p>a^b</p>
1815 // or different situation happened :P
1816 // then there's no separate container for the end of selection.
1817 pos
= endContainer
.getPosition( startContainer
),
1818 separateEndContainer
= !!endContainer
.getCommonAncestor( startContainer
) && // endC is not detached.
1819 pos
!= CKEDITOR
.POSITION_IDENTICAL
&& !( pos
& CKEDITOR
.POSITION_CONTAINS
+ CKEDITOR
.POSITION_IS_CONTAINED
); // endC & endS are in separate branches.
1821 nodesData
= extractNodesData( that
.dataWrapper
, that
);
1823 removeBrsAdjacentToPastedBlocks( nodesData
, range
);
1825 for ( ; nodeIndex
< nodesData
.length
; nodeIndex
++ ) {
1826 nodeData
= nodesData
[ nodeIndex
];
1828 // Ignore trailing <brs>
1829 if ( nodeData
.isLineBreak
&& splitOnLineBreak( range
, blockLimit
, nodeData
) ) {
1830 // Do not move caret towards the text (in cleanupAfterInsertion),
1831 // because caret was placed after a line break.
1832 dontMoveCaret
= nodeIndex
> 0;
1836 path
= range
.startPath();
1838 // Auto paragraphing.
1839 if ( !nodeData
.isBlock
&& shouldAutoParagraph( that
.editor
, path
.block
, path
.blockLimit
) && ( fixBlock
= autoParagraphTag( that
.editor
) ) ) {
1840 fixBlock
= doc
.createElement( fixBlock
);
1841 fixBlock
.appendBogus();
1842 range
.insertNode( fixBlock
);
1843 if ( CKEDITOR
.env
.needsBrFiller
&& ( bogus
= fixBlock
.getBogus() ) )
1845 range
.moveToPosition( fixBlock
, CKEDITOR
.POSITION_BEFORE_END
);
1848 node
= range
.startPath().block
;
1850 // Remove any bogus element on the current path block for now, and mark
1851 // it for later compensation.
1852 if ( node
&& !node
.equals( pathBlock
) ) {
1853 bogus
= node
.getBogus();
1856 bogusNeededBlocks
.push( node
);
1862 // First not allowed node reached - start splitting original container
1863 if ( nodeData
.firstNotAllowed
)
1864 splittingContainer
= 1;
1866 if ( splittingContainer
&& nodeData
.isElement
) {
1867 insertionContainer
= range
.startContainer
;
1870 // Find the first ancestor that can contain current node.
1871 // This one won't be split.
1872 while ( insertionContainer
&& !DTD
[ insertionContainer
.getName() ][ nodeData
.name
] ) {
1873 if ( insertionContainer
.equals( blockLimit
) ) {
1874 insertionContainer
= null;
1878 toSplit
= insertionContainer
;
1879 insertionContainer
= insertionContainer
.getParent();
1882 // If split has to be done - do it and mark both ends as a possible zombies.
1883 if ( insertionContainer
) {
1885 newContainer
= range
.splitElement( toSplit
);
1886 that
.zombies
.push( newContainer
);
1887 that
.zombies
.push( toSplit
);
1890 // Unable to make the insertion happen in place, resort to the content filter.
1892 // If everything worked fine insertionContainer == blockLimit here.
1893 filteredNodes
= filterElement( nodeData
.node
, blockLimit
.getName(), !nodeIndex
, nodeIndex
== nodesData
.length
- 1 );
1897 if ( filteredNodes
) {
1898 while ( ( node
= filteredNodes
.pop() ) )
1899 range
.insertNode( node
);
1902 // Insert current node at the start of range.
1903 range
.insertNode( nodeData
.node
);
1906 // Move range to the endContainer for the final allowed elements.
1907 if ( nodeData
.lastNotAllowed
&& nodeIndex
< nodesData
.length
- 1 ) {
1908 // If separateEndContainer exists move range there.
1909 // Otherwise try to move range to container created during splitting.
1910 // If this doesn't work - don't move range.
1911 newContainer
= separateEndContainer
? endContainer : newContainer
;
1912 newContainer
&& range
.setEndAt( newContainer
, CKEDITOR
.POSITION_AFTER_START
);
1913 splittingContainer
= 0;
1916 // Collapse range after insertion to end.
1920 // Rule 9. Non-editable content should be selected as a whole.
1921 if ( isSingleNonEditableElement( nodesData
) ) {
1922 dontMoveCaret
= true;
1923 node
= nodesData
[ 0 ].node
;
1924 range
.setStartAt( node
, CKEDITOR
.POSITION_BEFORE_START
);
1925 range
.setEndAt( node
, CKEDITOR
.POSITION_AFTER_END
);
1928 that
.dontMoveCaret
= dontMoveCaret
;
1929 that
.bogusNeededBlocks
= bogusNeededBlocks
;
1932 function cleanupAfterInsertion( that
) {
1933 var range
= that
.range
,
1934 node
, testRange
, movedIntoInline
,
1935 bogusNeededBlocks
= that
.bogusNeededBlocks
,
1936 // Create a bookmark to defend against the following range deconstructing operations.
1937 bm
= range
.createBookmark();
1939 // Remove all elements that could be created while splitting nodes
1940 // with ranges at its start|end.
1941 // E.g. remove <div><p></p></div>
1942 // But not <div><p> </p></div>
1943 // And replace <div><p><span data="cke-bookmark"/></p></div> with found bookmark.
1944 while ( ( node
= that
.zombies
.pop() ) ) {
1945 // Detached element.
1946 if ( !node
.getParent() )
1949 testRange
= range
.clone();
1950 testRange
.moveToElementEditStart( node
);
1951 testRange
.removeEmptyBlocksAtEnd();
1954 if ( bogusNeededBlocks
) {
1955 // Bring back all block bogus nodes.
1956 while ( ( node
= bogusNeededBlocks
.pop() ) ) {
1957 if ( CKEDITOR
.env
.needsBrFiller
)
1960 node
.append( range
.document
.createText( '\u00a0' ) );
1964 // Eventually merge identical inline elements.
1965 while ( ( node
= that
.mergeCandidates
.pop() ) )
1966 node
.mergeSiblings();
1968 range
.moveToBookmark( bm
);
1971 // Shrink range to the BEFOREEND of previous innermost editable node in source order.
1973 if ( !that
.dontMoveCaret
) {
1974 node
= getRangePrevious( range
);
1976 while ( node
&& checkIfElement( node
) && !node
.is( DTD
.$empty
) ) {
1977 if ( node
.isBlockBoundary() )
1978 range
.moveToPosition( node
, CKEDITOR
.POSITION_BEFORE_END
);
1980 // Don't move into inline element (which ends with a text node)
1981 // found which contains white-space at its end.
1982 // If not - move range's end to the end of this element.
1983 if ( isInline( node
) && node
.getHtml().match( /(\s| )$/g ) ) {
1984 movedIntoInline
= null;
1988 movedIntoInline
= range
.clone();
1989 movedIntoInline
.moveToPosition( node
, CKEDITOR
.POSITION_BEFORE_END
);
1992 node
= node
.getLast( isNotEmpty
);
1995 movedIntoInline
&& range
.moveToRange( movedIntoInline
);
2001 // HELPERS ------------------------------------------------------------
2004 function checkIfElement( node
) {
2005 return node
.type
== CKEDITOR
.NODE_ELEMENT
;
2008 function extractNodesData( dataWrapper
, that
) {
2009 var node
, sibling
, nodeName
, allowed
,
2011 startContainer
= that
.range
.startContainer
,
2012 path
= that
.range
.startPath(),
2013 allowedNames
= DTD
[ startContainer
.getName() ],
2015 nodesList
= dataWrapper
.getChildren(),
2016 nodesCount
= nodesList
.count(),
2017 firstNotAllowed
= -1,
2018 lastNotAllowed
= -1,
2022 // Selection start within a list.
2023 var insideOfList
= path
.contains( DTD
.$list
);
2025 for ( ; nodeIndex
< nodesCount
; ++nodeIndex
) {
2026 node
= nodesList
.getItem( nodeIndex
);
2028 if ( checkIfElement( node
) ) {
2029 nodeName
= node
.getName();
2031 // Extract only the list items, when insertion happens
2032 // inside of a list, reads as rearrange list items. (http://dev.ckeditor.com/ticket/7957)
2033 if ( insideOfList
&& nodeName
in CKEDITOR
.dtd
.$list
) {
2034 nodesData
= nodesData
.concat( extractNodesData( node
, that
) );
2038 allowed
= !!allowedNames
[ nodeName
];
2040 // Mark <brs data-cke-eol="1"> at the beginning and at the end.
2041 if ( nodeName
== 'br' && node
.data( 'cke-eol' ) && ( !nodeIndex
|| nodeIndex
== nodesCount
- 1 ) ) {
2042 sibling
= nodeIndex
? nodesData
[ nodeIndex
- 1 ].node : nodesList
.getItem( nodeIndex
+ 1 );
2044 // Line break has to have sibling which is not an <br>.
2045 lineBreak
= sibling
&& ( !checkIfElement( sibling
) || !sibling
.is( 'br' ) );
2046 // Line break has block element as a sibling.
2047 blockSibling
= sibling
&& checkIfElement( sibling
) && DTD
.$block
[ sibling
.getName() ];
2050 if ( firstNotAllowed
== -1 && !allowed
)
2051 firstNotAllowed
= nodeIndex
;
2053 lastNotAllowed
= nodeIndex
;
2057 isLineBreak: lineBreak
,
2058 isBlock: node
.isBlockBoundary(),
2059 hasBlockSibling: blockSibling
,
2068 nodesData
.push( { isElement: 0, node: node
, allowed: 1 } );
2072 // Mark first node that cannot be inserted directly into startContainer
2073 // and last node for which startContainer has to be split.
2074 if ( firstNotAllowed
> -1 )
2075 nodesData
[ firstNotAllowed
].firstNotAllowed
= 1;
2076 if ( lastNotAllowed
> -1 )
2077 nodesData
[ lastNotAllowed
].lastNotAllowed
= 1;
2082 // TODO: Review content transformation rules on filtering element.
2083 function filterElement( element
, parentName
, isFirst
, isLast
) {
2084 var nodes
= filterElementInner( element
, parentName
),
2086 nodesCount
= nodes
.length
,
2090 lastSpaceIndex
= -1;
2092 // Remove duplicated spaces and spaces at the:
2093 // * beginnig if filtered element isFirst (isFirst that's going to be inserted)
2094 // * end if filtered element isLast.
2095 for ( ; nodeIndex
< nodesCount
; nodeIndex
++ ) {
2096 node
= nodes
[ nodeIndex
];
2098 if ( node
== ' ' ) {
2099 // Don't push doubled space and if it's leading space for insertion.
2100 if ( !afterSpace
&& !( isFirst
&& !nodeIndex
) ) {
2101 nodes2
.push( new CKEDITOR
.dom
.text( ' ' ) );
2102 lastSpaceIndex
= nodes2
.length
;
2106 nodes2
.push( node
);
2111 // Remove trailing space.
2112 if ( isLast
&& lastSpaceIndex
== nodes2
.length
)
2118 function filterElementInner( element
, parentName
) {
2120 children
= element
.getChildren(),
2121 childrenCount
= children
.count(),
2124 allowedNames
= DTD
[ parentName
],
2125 surroundBySpaces
= !element
.is( DTD
.$inline
) || element
.is( 'br' );
2127 if ( surroundBySpaces
)
2130 for ( ; childIndex
< childrenCount
; childIndex
++ ) {
2131 child
= children
.getItem( childIndex
);
2133 if ( checkIfElement( child
) && !child
.is( allowedNames
) )
2134 nodes
= nodes
.concat( filterElementInner( child
, parentName
) );
2136 nodes
.push( child
);
2139 if ( surroundBySpaces
)
2145 function getRangePrevious( range
) {
2146 return checkIfElement( range
.startContainer
) && range
.startContainer
.getChild( range
.startOffset
- 1 );
2149 function isInline( node
) {
2150 return node
&& checkIfElement( node
) && ( node
.is( DTD
.$removeEmpty
) || node
.is( 'a' ) && !node
.isBlockBoundary() );
2153 // Checks if only non-editable element is being inserted.
2154 function isSingleNonEditableElement( nodesData
) {
2155 if ( nodesData
.length
!= 1 )
2158 var nodeData
= nodesData
[ 0 ];
2160 return nodeData
.isElement
&& ( nodeData
.node
.getAttribute( 'contenteditable' ) == 'false' );
2163 var blockMergedTags
= { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, li: 1, pre: 1, dl: 1, blockquote: 1 };
2165 // See rule 5. in TCs.
2166 // Initial situation:
2167 // <ul><li>AA^</li></ul><ul><li>BB</li></ul>
2168 // We're looking for 2nd <ul>, comparing with 1st <ul> and merging.
2169 // We're not merging if caret is between these elements.
2170 function mergeAncestorElementsOfSelectionEnds( range
, blockLimit
, startPath
, endPath
) {
2171 var walkerRange
= range
.clone(),
2172 walker
, nextNode
, previousNode
;
2174 walkerRange
.setEndAt( blockLimit
, CKEDITOR
.POSITION_BEFORE_END
);
2175 walker
= new CKEDITOR
.dom
.walker( walkerRange
);
2177 if ( ( nextNode
= walker
.next() ) && // Find next source node
2178 checkIfElement( nextNode
) && // which is an element
2179 blockMergedTags
[ nextNode
.getName() ] && // that can be merged.
2180 ( previousNode
= nextNode
.getPrevious() ) && // Take previous one
2181 checkIfElement( previousNode
) && // which also has to be an element.
2182 !previousNode
.getParent().equals( range
.startContainer
) && // Fail if caret is on the same level.
2183 // This means that caret is between these nodes.
2184 startPath
.contains( previousNode
) && // Elements path of start of selection has
2185 endPath
.contains( nextNode
) && // to contain prevNode and vice versa.
2186 nextNode
.isIdentical( previousNode
) // Check if elements are identical.
2188 // Merge blocks and repeat.
2189 nextNode
.moveChildren( previousNode
);
2191 mergeAncestorElementsOfSelectionEnds( range
, blockLimit
, startPath
, endPath
);
2195 // If last node that will be inserted is a block (but not a <br>)
2196 // and it will be inserted right before <br> remove this <br>.
2197 // Do the same for the first element that will be inserted and preceding <br>.
2198 function removeBrsAdjacentToPastedBlocks( nodesData
, range
) {
2199 var succeedingNode
= range
.endContainer
.getChild( range
.endOffset
),
2200 precedingNode
= range
.endContainer
.getChild( range
.endOffset
- 1 );
2202 if ( succeedingNode
)
2203 remove( succeedingNode
, nodesData
[ nodesData
.length
- 1 ] );
2205 if ( precedingNode
&& remove( precedingNode
, nodesData
[ 0 ] ) ) {
2206 // If preceding <br> was removed - move range left.
2207 range
.setEnd( range
.endContainer
, range
.endOffset
- 1 );
2211 function remove( maybeBr
, maybeBlockData
) {
2212 if ( maybeBlockData
.isBlock
&& maybeBlockData
.isElement
&& !maybeBlockData
.node
.is( 'br' ) &&
2213 checkIfElement( maybeBr
) && maybeBr
.is( 'br' ) ) {
2220 // Return 1 if <br> should be skipped when inserting, 0 otherwise.
2221 function splitOnLineBreak( range
, blockLimit
, nodeData
) {
2222 var firstBlockAscendant
, pos
;
2224 if ( nodeData
.hasBlockSibling
)
2227 firstBlockAscendant
= range
.startContainer
.getAscendant( DTD
.$block
, 1 );
2228 if ( !firstBlockAscendant
|| !firstBlockAscendant
.is( { div: 1, p: 1 } ) )
2231 pos
= firstBlockAscendant
.getPosition( blockLimit
);
2233 if ( pos
== CKEDITOR
.POSITION_IDENTICAL
|| pos
== CKEDITOR
.POSITION_CONTAINS
)
2236 var newContainer
= range
.splitElement( firstBlockAscendant
);
2237 range
.moveToPosition( newContainer
, CKEDITOR
.POSITION_AFTER_START
);
2242 var stripSingleBlockTags
= { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1 },
2243 inlineButNotBr
= CKEDITOR
.tools
.extend( {}, DTD
.$inline
);
2244 delete inlineButNotBr
.br
;
2247 function stripBlockTagIfSingleLine( dataWrapper
) {
2248 var block
, children
;
2250 if ( dataWrapper
.getChildCount() == 1 && // Only one node bein inserted.
2251 checkIfElement( block
= dataWrapper
.getFirst() ) && // And it's an element.
2252 block
.is( stripSingleBlockTags
) && // That's <p> or <div> or header.
2253 !block
.hasAttribute( 'contenteditable' ) // It's not a non-editable block or nested editable.
2255 // Check children not containing block.
2256 children
= block
.getElementsByTag( '*' );
2257 for ( var i
= 0, child
, count
= children
.count(); i
< count
; i
++ ) {
2258 child
= children
.getItem( i
);
2259 if ( !child
.is( inlineButNotBr
) )
2263 block
.moveChildren( block
.getParent( 1 ) );
2268 function wrapDataWithInlineStyles( data
, that
) {
2269 var element
= that
.inlineStylesPeak
,
2270 doc
= element
.getDocument(),
2271 wrapper
= doc
.createText( '{cke-peak}' ),
2272 limit
= that
.inlineStylesRoot
.getParent();
2274 while ( !element
.equals( limit
) ) {
2275 wrapper
= wrapper
.appendTo( element
.clone() );
2276 element
= element
.getParent();
2279 // Don't use String.replace because it fails in IE7 if special replacement
2280 // characters ($$, $&, etc.) are in data (http://dev.ckeditor.com/ticket/10367).
2281 return wrapper
.getOuterHtml().split( '{cke-peak}' ).join( data
);
2287 function afterInsert( editable
) {
2288 var editor
= editable
.editor
;
2290 // Scroll using selection, not ranges, to affect native pastes.
2291 editor
.getSelection().scrollIntoView();
2293 // Save snaps after the whole execution completed.
2294 // This's a workaround for make DOM modification's happened after
2295 // 'insertElement' to be included either, e.g. Form-based dialogs' 'commitContents'
2297 setTimeout( function() {
2298 editor
.fire( 'saveSnapshot' );
2302 // 1. Fixes a range which is a result of deleteContents() and is placed in an intermediate element (see dtd.$intermediate),
2303 // inside a table. A goal is to find a closest <td> or <th> element and when this fails, recreate the structure of the table.
2304 // 2. Fixes empty cells by appending bogus <br>s or deleting empty text nodes in IE<=8 case.
2305 fixTableAfterContentsDeletion
= ( function() {
2306 // Creates an element walker which can only "go deeper". It won't
2307 // move out from any element. Therefore it can be used to find <td>x</td> in cases like:
2308 // <table><tbody><tr><td>x</td></tr></tbody>^<tfoot>...
2309 function getFixTableSelectionWalker( testRange
) {
2310 var walker
= new CKEDITOR
.dom
.walker( testRange
);
2311 walker
.guard = function( node
, isMovingOut
) {
2314 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
)
2315 return node
.is( CKEDITOR
.dtd
.$tableContent
);
2317 walker
.evaluator = function( node
) {
2318 return node
.type
== CKEDITOR
.NODE_ELEMENT
;
2324 function fixTableStructure( element
, newElementName
, appendToStart
) {
2325 var temp
= element
.getDocument().createElement( newElementName
);
2326 element
.append( temp
, appendToStart
);
2330 // Fix empty cells. This means:
2331 // * add bogus <br> if browser needs it
2332 // * remove empty text nodes on IE8, because it will crash (http://dev.ckeditor.com/ticket/11183#comment:8).
2333 function fixEmptyCells( cells
) {
2334 var i
= cells
.count(),
2337 for ( i
; i
-- > 0; ) {
2338 cell
= cells
.getItem( i
);
2340 if ( !CKEDITOR
.tools
.trim( cell
.getHtml() ) ) {
2342 if ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 9 && cell
.getChildCount() )
2343 cell
.getFirst().remove();
2348 return function( range
) {
2349 var container
= range
.startContainer
,
2350 table
= container
.getAscendant( 'table', 1 ),
2353 appendToStart
= false;
2355 fixEmptyCells( table
.getElementsByTag( 'td' ) );
2356 fixEmptyCells( table
.getElementsByTag( 'th' ) );
2359 testRange
= range
.clone();
2360 testRange
.setStart( container
, 0 );
2361 deeperSibling
= getFixTableSelectionWalker( testRange
).lastBackward();
2363 // If left is empty, look right.
2364 if ( !deeperSibling
) {
2365 testRange
= range
.clone();
2366 testRange
.setEndAt( container
, CKEDITOR
.POSITION_BEFORE_END
);
2367 deeperSibling
= getFixTableSelectionWalker( testRange
).lastForward();
2368 appendToStart
= true;
2371 // If there's no deeper nested element in both direction - container is empty - we'll use it then.
2372 if ( !deeperSibling
)
2373 deeperSibling
= container
;
2377 // We found a table what means that it's empty - remove it completely.
2378 if ( deeperSibling
.is( 'table' ) ) {
2379 range
.setStartAt( deeperSibling
, CKEDITOR
.POSITION_BEFORE_START
);
2380 range
.collapse( true );
2381 deeperSibling
.remove();
2385 // Found an empty txxx element - append tr.
2386 if ( deeperSibling
.is( { tbody: 1, thead: 1, tfoot: 1 } ) )
2387 deeperSibling
= fixTableStructure( deeperSibling
, 'tr', appendToStart
);
2389 // Found an empty tr element - append td/th.
2390 if ( deeperSibling
.is( 'tr' ) )
2391 deeperSibling
= fixTableStructure( deeperSibling
, deeperSibling
.getParent().is( 'thead' ) ? 'th' : 'td', appendToStart
);
2393 // To avoid setting selection after bogus, remove it from the current cell.
2394 // We can safely do that, because we'll insert element into that cell.
2395 var bogus
= deeperSibling
.getBogus();
2399 range
.moveToPosition( deeperSibling
, appendToStart
? CKEDITOR
.POSITION_AFTER_START : CKEDITOR
.POSITION_BEFORE_END
);
2403 fixListAfterContentsDelete
= ( function() {
2404 // Creates an element walker which operates only within lists.
2405 function getFixListSelectionWalker( testRange
) {
2406 var walker
= new CKEDITOR
.dom
.walker( testRange
);
2407 walker
.guard = function( node
, isMovingOut
) {
2410 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
)
2411 return node
.is( CKEDITOR
.dtd
.$list
) || node
.is( CKEDITOR
.dtd
.$listItem
);
2413 walker
.evaluator = function( node
) {
2414 return node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( CKEDITOR
.dtd
.$listItem
);
2420 return function( range
) {
2421 var container
= range
.startContainer
,
2422 appendToStart
= false,
2427 testRange
= range
.clone();
2428 testRange
.setStart( container
, 0 );
2429 deeperSibling
= getFixListSelectionWalker( testRange
).lastBackward();
2431 // If left is empty, look right.
2432 if ( !deeperSibling
) {
2433 testRange
= range
.clone();
2434 testRange
.setEndAt( container
, CKEDITOR
.POSITION_BEFORE_END
);
2435 deeperSibling
= getFixListSelectionWalker( testRange
).lastForward();
2436 appendToStart
= true;
2439 // If there's no deeper nested element in both direction - container is empty - we'll use it then.
2440 if ( !deeperSibling
)
2441 deeperSibling
= container
;
2443 // We found a list what means that it's empty - remove it completely.
2444 if ( deeperSibling
.is( CKEDITOR
.dtd
.$list
) ) {
2445 range
.setStartAt( deeperSibling
, CKEDITOR
.POSITION_BEFORE_START
);
2446 range
.collapse( true );
2447 deeperSibling
.remove();
2451 // To avoid setting selection after bogus, remove it from the target list item.
2452 // We can safely do that, because we'll insert element into that cell.
2453 var bogus
= deeperSibling
.getBogus();
2457 range
.moveToPosition( deeperSibling
, appendToStart
? CKEDITOR
.POSITION_AFTER_START : CKEDITOR
.POSITION_BEFORE_END
);
2462 function mergeBlocksCollapsedSelection( editor
, range
, backspace
, startPath
) {
2463 var startBlock
= startPath
.block
;
2465 // Selection must be collapsed and to be anchored in a block.
2469 // Exclude cases where, i.e. if pressed arrow key, selection
2470 // would move within the same block (merge inside a block).
2471 if ( !range
[ backspace
? 'checkStartOfBlock' : 'checkEndOfBlock' ]() )
2474 // Make sure, there's an editable position to put selection,
2475 // which i.e. would be used if pressed arrow key, but abort
2476 // if such position exists but means a selected non-editable element.
2477 if ( !range
.moveToClosestEditablePosition( startBlock
, !backspace
) || !range
.collapsed
)
2480 // Handle special case, when block's sibling is a <hr>. Delete it and keep selection
2481 // in the same place (http://dev.ckeditor.com/ticket/11861#comment:9).
2482 if ( range
.startContainer
.type
== CKEDITOR
.NODE_ELEMENT
) {
2483 var touched
= range
.startContainer
.getChild( range
.startOffset
- ( backspace
? 1 : 0 ) );
2484 if ( touched
&& touched
.type
== CKEDITOR
.NODE_ELEMENT
&& touched
.is( 'hr' ) ) {
2485 editor
.fire( 'saveSnapshot' );
2491 var siblingBlock
= range
.startPath().block
;
2493 // Abort if an editable position exists, but either it's not
2494 // in a block or that block is the parent of the start block
2495 // (merging child into parent).
2496 if ( !siblingBlock
|| ( siblingBlock
&& siblingBlock
.contains( startBlock
) ) )
2499 editor
.fire( 'saveSnapshot' );
2501 // Remove bogus to avoid duplicated boguses.
2503 if ( ( bogus
= ( backspace
? siblingBlock : startBlock
).getBogus() ) )
2506 // Save selection. It will be restored.
2507 var selection
= editor
.getSelection(),
2508 bookmarks
= selection
.createBookmarks();
2511 ( backspace
? startBlock : siblingBlock
).moveChildren( backspace
? siblingBlock : startBlock
, false );
2513 // Also merge children along with parents.
2514 startPath
.lastElement
.mergeSiblings();
2516 // Cut off removable branch of the DOM tree.
2517 pruneEmptyDisjointAncestors( startBlock
, siblingBlock
, !backspace
);
2519 // Restore selection.
2520 selection
.selectBookmarks( bookmarks
);
2525 function mergeBlocksNonCollapsedSelection( editor
, range
, startPath
) {
2526 var startBlock
= startPath
.block
,
2527 endPath
= range
.endPath(),
2528 endBlock
= endPath
.block
;
2530 // Selection must be anchored in two different blocks.
2531 if ( !startBlock
|| !endBlock
|| startBlock
.equals( endBlock
) )
2534 editor
.fire( 'saveSnapshot' );
2536 // Remove bogus to avoid duplicated boguses.
2538 if ( ( bogus
= startBlock
.getBogus() ) )
2541 // Changing end container to element from text node (http://dev.ckeditor.com/ticket/12503).
2542 range
.enlarge( CKEDITOR
.ENLARGE_INLINE
);
2544 // Delete range contents. Do NOT merge. Merging is weird.
2545 range
.deleteContents();
2547 // If something has left of the block to be merged, clean it up.
2548 // It may happen when merging with list items.
2549 if ( endBlock
.getParent() ) {
2550 // Move children to the first block.
2551 endBlock
.moveChildren( startBlock
, false );
2553 // ...and merge them if that's possible.
2554 startPath
.lastElement
.mergeSiblings();
2556 // If expanded selection, things are always merged like with BACKSPACE.
2557 pruneEmptyDisjointAncestors( startBlock
, endBlock
, true );
2560 // Make sure the result selection is collapsed.
2561 range
= editor
.getSelection().getRanges()[ 0 ];
2562 range
.collapse( 1 );
2564 // Optimizing range containers from text nodes to elements (http://dev.ckeditor.com/ticket/12503).
2566 if ( range
.startContainer
.getHtml() === '' ) {
2567 range
.startContainer
.appendBogus();
2575 // Finds the innermost child of common parent, which,
2576 // if removed, removes nothing but the contents of the element.
2578 // before: <div><p><strong>first</strong></p><p>second</p></div>
2579 // after: <div><p>second</p></div>
2581 // before: <div><p>x<strong>first</strong></p><p>second</p></div>
2582 // after: <div><p>x</p><p>second</p></div>
2584 // isPruneToEnd=true
2585 // before: <div><p><strong>first</strong></p><p>second</p></div>
2586 // after: <div><p><strong>first</strong></p></div>
2588 // @param {CKEDITOR.dom.element} first
2589 // @param {CKEDITOR.dom.element} second
2590 // @param {Boolean} isPruneToEnd
2591 function pruneEmptyDisjointAncestors( first
, second
, isPruneToEnd
) {
2592 var commonParent
= first
.getCommonAncestor( second
),
2593 node
= isPruneToEnd
? second : first
,
2594 removableParent
= node
;
2596 while ( ( node
= node
.getParent() ) && !commonParent
.equals( node
) && node
.getChildCount() == 1 )
2597 removableParent
= node
;
2599 removableParent
.remove();
2603 // Helpers for editable.getHtmlFromRange.
2605 getHtmlFromRangeHelpers
= {
2607 detect: function( that
, editable
) {
2608 var range
= that
.range
,
2609 rangeStart
= range
.clone(),
2610 rangeEnd
= range
.clone(),
2612 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, editable
),
2613 endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
, editable
);
2615 // Note: checkBoundaryOfElement will not work on original range as CKEDITOR.START|END
2616 // means that range start|end must be literally anchored at block start|end, e.g.
2618 // <p>a{</p><p>}b</p>
2620 // will return false for both paragraphs but two similar ranges
2622 // <p>a{}</p><p>{}b</p>
2624 // will return true if checked separately.
2625 rangeStart
.collapse( 1 );
2626 rangeEnd
.collapse();
2628 if ( startPath
.block
&& rangeStart
.checkBoundaryOfElement( startPath
.block
, CKEDITOR
.END
) ) {
2629 range
.setStartAfter( startPath
.block
);
2630 that
.prependEolBr
= 1;
2633 if ( endPath
.block
&& rangeEnd
.checkBoundaryOfElement( endPath
.block
, CKEDITOR
.START
) ) {
2634 range
.setEndBefore( endPath
.block
);
2635 that
.appendEolBr
= 1;
2639 fix: function( that
, editable
) {
2640 var doc
= editable
.getDocument(),
2643 // Append <br data-cke-eol="1"> to the fragment.
2644 if ( that
.appendEolBr
) {
2645 appended
= this.createEolBr( doc
);
2646 that
.fragment
.append( appended
);
2649 // Prepend <br data-cke-eol="1"> to the fragment but avoid duplicates. Such
2650 // elements should never follow each other in DOM.
2651 if ( that
.prependEolBr
&& ( !appended
|| appended
.getPrevious() ) ) {
2652 that
.fragment
.append( this.createEolBr( doc
), 1 );
2656 createEolBr: function( doc
) {
2657 return doc
.createElement( 'br', {
2666 exclude: function( that
) {
2667 var boundaryNodes
= that
.range
.getBoundaryNodes(),
2668 startNode
= boundaryNodes
.startNode
,
2669 endNode
= boundaryNodes
.endNode
;
2671 // If bogus is the last node in range but not the only node, exclude it.
2672 if ( endNode
&& isBogus( endNode
) && ( !startNode
|| !startNode
.equals( endNode
) ) )
2673 that
.range
.setEndBefore( endNode
);
2678 rebuild: function( that
, editable
) {
2679 var range
= that
.range
,
2680 node
= range
.getCommonAncestor(),
2682 // A path relative to the common ancestor.
2683 commonPath
= new CKEDITOR
.dom
.elementPath( node
, editable
),
2684 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, editable
),
2685 endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
, editable
),
2688 if ( node
.type
== CKEDITOR
.NODE_TEXT
)
2689 node
= node
.getParent();
2691 // Fix DOM of partially enclosed tables
2692 // <table><tbody><tr><td>a{b</td><td>c}d</td></tr></tbody></table>
2693 // Full table is returned
2694 // <table><tbody><tr><td>b</td><td>c</td></tr></tbody></table>
2696 // <td>b</td><td>c</td>
2697 if ( commonPath
.blockLimit
.is( { tr: 1, table: 1 } ) ) {
2698 var tableParent
= commonPath
.contains( 'table' ).getParent();
2700 limit = function( node
) {
2701 return !node
.equals( tableParent
);
2705 // Fix DOM in the following case
2706 // <ol><li>a{b<ul><li>c}d</li></ul></li></ol>
2707 // Full list is returned
2708 // <ol><li>b<ul><li>c</li></ul></li></ol>
2710 // b<ul><li>c</li></ul>
2711 else if ( commonPath
.block
&& commonPath
.block
.is( CKEDITOR
.dtd
.$listItem
) ) {
2712 var startList
= startPath
.contains( CKEDITOR
.dtd
.$list
),
2713 endList
= endPath
.contains( CKEDITOR
.dtd
.$list
);
2715 if ( !startList
.equals( endList
) ) {
2716 var listParent
= commonPath
.contains( CKEDITOR
.dtd
.$list
).getParent();
2718 limit = function( node
) {
2719 return !node
.equals( listParent
);
2724 // If not defined, use generic limit function.
2726 limit = function( node
) {
2727 return !node
.equals( commonPath
.block
) && !node
.equals( commonPath
.blockLimit
);
2731 this.rebuildFragment( that
, editable
, node
, limit
);
2734 rebuildFragment: function( that
, editable
, node
, checkLimit
) {
2737 while ( node
&& !node
.equals( editable
) && checkLimit( node
) ) {
2738 // Don't clone children. Preserve element ids.
2739 clone
= node
.clone( 0, 1 );
2740 that
.fragment
.appendTo( clone
);
2741 that
.fragment
= clone
;
2743 node
= node
.getParent();
2749 // Handle range anchored in table row with a single cell enclosed:
2750 // <table><tbody><tr>[<td>a</td>]</tr></tbody></table>
2752 // <table><tbody><tr><td>{a}</td></tr></tbody></table>
2753 shrink: function( that
) {
2754 var range
= that
.range
,
2755 startContainer
= range
.startContainer
,
2756 endContainer
= range
.endContainer
,
2757 startOffset
= range
.startOffset
,
2758 endOffset
= range
.endOffset
;
2760 if ( startContainer
.type
== CKEDITOR
.NODE_ELEMENT
&& startContainer
.equals( endContainer
) && startContainer
.is( 'tr' ) && ++startOffset
== endOffset
) {
2761 range
.shrink( CKEDITOR
.SHRINK_TEXT
);
2768 // Helpers for editable.extractHtmlFromRange.
2770 extractHtmlFromRangeHelpers
= ( function() {
2771 function optimizeBookmarkNode( node
, toStart
) {
2772 var parent
= node
.getParent();
2774 if ( parent
.is( CKEDITOR
.dtd
.$inline
) )
2775 node
[ toStart
? 'insertBefore' : 'insertAfter' ]( parent
);
2778 function mergeElements( merged
, startBookmark
, endBookmark
) {
2779 optimizeBookmarkNode( startBookmark
);
2780 optimizeBookmarkNode( endBookmark
, 1 );
2783 while ( ( next
= endBookmark
.getNext() ) ) {
2784 next
.insertAfter( startBookmark
);
2786 // Update startBookmark after insertion to avoid the reversal of nodes (http://dev.ckeditor.com/ticket/13449).
2787 startBookmark
= next
;
2790 if ( isEmpty( merged
) )
2794 function getPath( startElement
, root
) {
2795 return new CKEDITOR
.dom
.elementPath( startElement
, root
);
2798 // Creates a range from a bookmark without removing the bookmark.
2799 function createRangeFromBookmark( root
, bookmark
) {
2800 var range
= new CKEDITOR
.dom
.range( root
);
2801 range
.setStartAfter( bookmark
.startNode
);
2802 range
.setEndBefore( bookmark
.endNode
);
2807 detectMerge: function( that
, editable
) {
2808 var range
= createRangeFromBookmark( editable
, that
.bookmark
),
2809 startPath
= range
.startPath(),
2810 endPath
= range
.endPath(),
2812 startList
= startPath
.contains( CKEDITOR
.dtd
.$list
),
2813 endList
= endPath
.contains( CKEDITOR
.dtd
.$list
);
2816 // Both lists must exist
2817 startList
&& endList
&&
2818 // ...and be of the same type
2819 // startList.getName() == endList.getName() &&
2820 // ...and share the same parent (same level in the tree)
2821 startList
.getParent().equals( endList
.getParent() ) &&
2822 // ...and must be different.
2823 !startList
.equals( endList
);
2825 that
.mergeListItems
=
2826 startPath
.block
&& endPath
.block
&&
2827 // Both containers must be list items
2828 startPath
.block
.is( CKEDITOR
.dtd
.$listItem
) && endPath
.block
.is( CKEDITOR
.dtd
.$listItem
);
2830 // Create merge bookmark.
2831 if ( that
.mergeList
|| that
.mergeListItems
) {
2832 var rangeClone
= range
.clone();
2834 rangeClone
.setStartBefore( that
.bookmark
.startNode
);
2835 rangeClone
.setEndAfter( that
.bookmark
.endNode
);
2837 that
.mergeListBookmark
= rangeClone
.createBookmark();
2841 merge: function( that
, editable
) {
2842 if ( !that
.mergeListBookmark
)
2845 var startNode
= that
.mergeListBookmark
.startNode
,
2846 endNode
= that
.mergeListBookmark
.endNode
,
2848 startPath
= getPath( startNode
, editable
),
2849 endPath
= getPath( endNode
, editable
);
2851 if ( that
.mergeList
) {
2852 var firstList
= startPath
.contains( CKEDITOR
.dtd
.$list
),
2853 secondList
= endPath
.contains( CKEDITOR
.dtd
.$list
);
2855 if ( !firstList
.equals( secondList
) ) {
2856 secondList
.moveChildren( firstList
);
2857 secondList
.remove();
2861 if ( that
.mergeListItems
) {
2862 var firstListItem
= startPath
.contains( CKEDITOR
.dtd
.$listItem
),
2863 secondListItem
= endPath
.contains( CKEDITOR
.dtd
.$listItem
);
2865 if ( !firstListItem
.equals( secondListItem
) ) {
2866 mergeElements( secondListItem
, startNode
, endNode
);
2870 // Remove bookmark nodes.
2877 // Detects whether blocks should be merged once contents are extracted.
2878 detectMerge: function( that
, editable
) {
2879 // Don't merge blocks if lists or tables are already involved.
2880 if ( that
.tableContentsRanges
|| that
.mergeListBookmark
)
2883 var rangeClone
= new CKEDITOR
.dom
.range( editable
);
2885 rangeClone
.setStartBefore( that
.bookmark
.startNode
);
2886 rangeClone
.setEndAfter( that
.bookmark
.endNode
);
2888 that
.mergeBlockBookmark
= rangeClone
.createBookmark();
2891 merge: function( that
, editable
) {
2892 if ( !that
.mergeBlockBookmark
|| that
.purgeTableBookmark
)
2895 var startNode
= that
.mergeBlockBookmark
.startNode
,
2896 endNode
= that
.mergeBlockBookmark
.endNode
,
2898 startPath
= getPath( startNode
, editable
),
2899 endPath
= getPath( endNode
, editable
),
2901 firstBlock
= startPath
.block
,
2902 secondBlock
= endPath
.block
;
2904 if ( firstBlock
&& secondBlock
&& !firstBlock
.equals( secondBlock
) ) {
2905 mergeElements( secondBlock
, startNode
, endNode
);
2908 // Remove bookmark nodes.
2914 var table
= ( function() {
2915 var tableEditable
= { td: 1, th: 1, caption: 1 };
2917 // Returns an array of ranges which should be entirely extracted.
2919 // <table><tr>[<td>xx</td><td>y}y</td></tr></table>
2921 // <table><tr><td>[xx]</td><td>[y}y</td></tr></table>
2922 function findTableContentsRanges( range
) {
2923 // Leaving the below for debugging purposes.
2925 // console.log( 'findTableContentsRanges' );
2926 // console.log( bender.tools.range.getWithHtml( range.root, range ) );
2928 var contentsRanges
= [],
2930 walker
= new CKEDITOR
.dom
.walker( range
),
2931 startCell
= range
.startPath().contains( tableEditable
),
2932 endCell
= range
.endPath().contains( tableEditable
),
2935 walker
.guard = function( node
, leaving
) {
2936 // Guard may be executed on some node boundaries multiple times,
2937 // what results in creating more than one range for each selected cell. (http://dev.ckeditor.com/ticket/12964)
2938 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
) {
2939 var key
= 'visited_' + ( leaving
? 'out' : 'in' );
2940 if ( node
.getCustomData( key
) ) {
2944 CKEDITOR
.dom
.element
.setMarker( database
, node
, key
, 1 );
2947 // Handle partial selection in a cell in which the range starts:
2948 // <td><p>x{xx</p></td>...
2950 // <td><p>x{xx</p>]</td>
2951 if ( leaving
&& startCell
&& node
.equals( startCell
) ) {
2952 editableRange
= range
.clone();
2953 editableRange
.setEndAt( startCell
, CKEDITOR
.POSITION_BEFORE_END
);
2954 contentsRanges
.push( editableRange
);
2958 // Handle partial selection in a cell in which the range ends.
2959 if ( !leaving
&& endCell
&& node
.equals( endCell
) ) {
2960 editableRange
= range
.clone();
2961 editableRange
.setStartAt( endCell
, CKEDITOR
.POSITION_AFTER_START
);
2962 contentsRanges
.push( editableRange
);
2966 // Handle all other cells visited by the walker.
2967 // We need to check whether the cell is disjoint with
2968 // the start and end cells to correctly handle case like:
2969 // <td>x{x</td><td><table>..<td>y}y</td>..</table></td>
2970 // without the check the second cell's content would be entirely removed.
2971 if ( !leaving
&& checkRemoveCellContents( node
) ) {
2972 editableRange
= range
.clone();
2973 editableRange
.selectNodeContents( node
);
2974 contentsRanges
.push( editableRange
);
2978 walker
.lastForward();
2980 // Clear all markers so next extraction will not be affected by this one.
2981 CKEDITOR
.dom
.element
.clearAllMarkers( database
);
2983 return contentsRanges
;
2985 function checkRemoveCellContents( node
) {
2988 node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( tableEditable
) &&
2989 // Must be disjoint with the range's startCell if exists.
2990 ( !startCell
|| checkDisjointNodes( node
, startCell
) ) &&
2991 // Must be disjoint with the range's endCell if exists.
2992 ( !endCell
|| checkDisjointNodes( node
, endCell
) )
2997 // Returns a normalized common ancestor of a range.
2998 // If the real common ancestor is located somewhere in between a table and a td/th/caption,
2999 // then the table will be returned.
3000 function getNormalizedAncestor( range
) {
3001 var common
= range
.getCommonAncestor();
3003 if ( common
.is( CKEDITOR
.dtd
.$tableContent
) && !common
.is( tableEditable
) ) {
3004 common
= common
.getAscendant( 'table', true );
3010 // Check whether node1 and node2 are disjoint, so are:
3012 // * not contained in each other.
3013 function checkDisjointNodes( node1
, node2
) {
3014 var disallowedPositions
= CKEDITOR
.POSITION_CONTAINS
+ CKEDITOR
.POSITION_IS_CONTAINED
,
3015 pos
= node1
.getPosition( node2
);
3017 // Baaah... IDENTICAL is 0, so we can't simplify this ;/.
3018 return pos
=== CKEDITOR
.POSITION_IDENTICAL
?
3020 ( ( pos
& disallowedPositions
) === 0 );
3024 // Detects whether to purge entire list.
3025 detectPurge: function( that
) {
3026 var range
= that
.range
,
3027 walkerRange
= range
.clone();
3029 walkerRange
.enlarge( CKEDITOR
.ENLARGE_ELEMENT
);
3031 var walker
= new CKEDITOR
.dom
.walker( walkerRange
),
3034 // Count the number of table editables in the range. If there's more than one,
3035 // table MAY be removed completely (it's a cross-cell range). Otherwise, only
3036 // the contents of the cell are usually removed.
3037 walker
.evaluator = function( node
) {
3038 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( tableEditable
) ) {
3043 walker
.checkForward();
3045 if ( editablesCount
> 1 ) {
3046 var startTable
= range
.startPath().contains( 'table' ),
3047 endTable
= range
.endPath().contains( 'table' );
3049 if ( startTable
&& endTable
&& range
.checkBoundaryOfElement( startTable
, CKEDITOR
.START
) && range
.checkBoundaryOfElement( endTable
, CKEDITOR
.END
) ) {
3050 var rangeClone
= that
.range
.clone();
3052 rangeClone
.setStartBefore( startTable
);
3053 rangeClone
.setEndAfter( endTable
);
3055 that
.purgeTableBookmark
= rangeClone
.createBookmark();
3062 // This method tries to discover whether the range starts or ends somewhere in a table
3063 // (it is not interested whether the range contains a table, because in such case
3064 // the extractContents() methods does the job correctly).
3065 // If the range meets these criteria, then the method tries to discover and store the following:
3067 // * that.tableSurroundingRange - a part of the range which is located outside of any table which
3068 // will be touched (note: when range is located in a single cell it does not touch the table).
3069 // This range can be placed at:
3070 // * at the beginning: <p>he{re</p><table>..]..</table>
3071 // * in the middle: <table>..[..</table><p>here</p><table>..]..</table>
3072 // * at the end: <table>..[..</table><p>he}re</p>
3073 // * that.tableContentsRanges - an array of ranges with contents of td/th/caption that should be removed.
3074 // This assures that calling extractContents() does not change the structure of the table(s).
3075 detectRanges: function( that
, editable
) {
3076 var range
= createRangeFromBookmark( editable
, that
.bookmark
),
3077 surroundingRange
= range
.clone(),
3081 // Find a common ancestor and normalize it (so the following paths contain tables).
3082 commonAncestor
= getNormalizedAncestor( range
),
3084 // Create paths using the normalized ancestor, so tables beyond the context
3085 // of the input range are not found.
3086 startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, commonAncestor
),
3087 endPath
= new CKEDITOR
.dom
.elementPath( range
.endContainer
, commonAncestor
),
3089 startTable
= startPath
.contains( 'table' ),
3090 endTable
= endPath
.contains( 'table' ),
3092 tableContentsRanges
;
3094 // Nothing to do here - the range doesn't touch any table or
3095 // it contains a table, but that table is fully selected so it will be simply fully removed
3096 // by the normal algorithm.
3097 if ( !startTable
&& !endTable
) {
3101 // Handle two disjoint tables case:
3102 // <table>..[..</table><p>ab</p><table>..]..</table>
3103 // is handled as (respectively: findTableContents( left ), surroundingRange, findTableContents( right )):
3104 // <table>..[..</table>][<p>ab</p>][<table>..]..</table>
3105 // Check that tables are disjoint to exclude a case when start equals end or one is contained
3107 if ( startTable
&& endTable
&& checkDisjointNodes( startTable
, endTable
) ) {
3108 that
.tableSurroundingRange
= surroundingRange
;
3109 surroundingRange
.setStartAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
3110 surroundingRange
.setEndAt( endTable
, CKEDITOR
.POSITION_BEFORE_START
);
3112 leftRange
= range
.clone();
3113 leftRange
.setEndAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
3115 rightRange
= range
.clone();
3116 rightRange
.setStartAt( endTable
, CKEDITOR
.POSITION_BEFORE_START
);
3118 tableContentsRanges
= findTableContentsRanges( leftRange
).concat( findTableContentsRanges( rightRange
) );
3120 // Divide the initial range into two parts:
3121 // * range which contains the part containing the table,
3122 // * surroundingRange which contains the part outside the table.
3124 // The surroundingRange exists only if one of the range ends is
3125 // located outside the table.
3127 // <p>a{b</p><table>..]..</table><p>cd</p>
3128 // becomes (respectively: surroundingRange, range):
3129 // <p>a{b</p>][<table>..]..</table><p>cd</p>
3130 else if ( !startTable
) {
3131 that
.tableSurroundingRange
= surroundingRange
;
3132 surroundingRange
.setEndAt( endTable
, CKEDITOR
.POSITION_BEFORE_START
);
3134 range
.setStartAt( endTable
, CKEDITOR
.POSITION_AFTER_START
);
3136 // <p>ab</p><table>..[..</table><p>c}d</p>
3137 // becomes (respectively range, surroundingRange):
3138 // <p>ab</p><table>..[..</table>][<p>c}d</p>
3139 else if ( !endTable
) {
3140 that
.tableSurroundingRange
= surroundingRange
;
3141 surroundingRange
.setStartAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
3143 range
.setEndAt( startTable
, CKEDITOR
.POSITION_AFTER_END
);
3146 // Use already calculated or calculate for the remaining range.
3147 that
.tableContentsRanges
= tableContentsRanges
? tableContentsRanges : findTableContentsRanges( range
);
3149 // Leaving the below for debugging purposes.
3151 // if ( that.tableSurroundingRange ) {
3152 // console.log( 'tableSurroundingRange' );
3153 // console.log( bender.tools.range.getWithHtml( that.tableSurroundingRange.root, that.tableSurroundingRange ) );
3156 // console.log( 'tableContentsRanges' );
3157 // that.tableContentsRanges.forEach( function( range ) {
3158 // console.log( bender.tools.range.getWithHtml( range.root, range ) );
3162 deleteRanges: function( that
) {
3165 // Delete table cell contents.
3166 while ( ( range
= that
.tableContentsRanges
.pop() ) ) {
3167 range
.extractContents();
3169 if ( isEmpty( range
.startContainer
) )
3170 range
.startContainer
.appendBogus();
3173 // Finally delete surroundings of the table.
3174 if ( that
.tableSurroundingRange
) {
3175 that
.tableSurroundingRange
.extractContents();
3179 purge: function( that
) {
3180 if ( !that
.purgeTableBookmark
)
3185 rangeClone
= range
.clone(),
3186 // How about different enter modes?
3187 block
= doc
.createElement( 'p' );
3189 block
.insertBefore( that
.purgeTableBookmark
.startNode
);
3191 rangeClone
.moveToBookmark( that
.purgeTableBookmark
);
3192 rangeClone
.deleteContents();
3194 that
.range
.moveToPosition( block
, CKEDITOR
.POSITION_AFTER_START
);
3204 // Detects whether use "mergeThen" argument in range.extractContents().
3205 detectExtractMerge: function( that
) {
3206 // Don't merge if playing with lists.
3208 that
.range
.startPath().contains( CKEDITOR
.dtd
.$listItem
) &&
3209 that
.range
.endPath().contains( CKEDITOR
.dtd
.$listItem
)
3213 fixUneditableRangePosition: function( range
) {
3214 if ( !range
.startContainer
.getDtd()[ '#' ] ) {
3215 range
.moveToClosestEditablePosition( null, true );
3219 // Perform auto paragraphing if needed.
3220 autoParagraph: function( editor
, range
) {
3221 var path
= range
.startPath(),
3224 if ( shouldAutoParagraph( editor
, path
.block
, path
.blockLimit
) && ( fixBlock
= autoParagraphTag( editor
) ) ) {
3225 fixBlock
= range
.document
.createElement( fixBlock
);
3226 fixBlock
.appendBogus();
3227 range
.insertNode( fixBlock
);
3228 range
.moveToPosition( fixBlock
, CKEDITOR
.POSITION_AFTER_START
);
3237 * Whether the editor must output an empty value (`''`) if its content only consists
3238 * of an empty paragraph.
3240 * config.ignoreEmptyParagraph = false;
3242 * @cfg {Boolean} [ignoreEmptyParagraph=true]
3243 * @member CKEDITOR.config
3247 * Event fired by the editor in order to get accessibility help label.
3248 * The event is responded to by a component which provides accessibility
3249 * help (i.e. the `a11yhelp` plugin) hence the editor is notified whether
3250 * accessibility help is available.
3254 * editor.on( 'ariaEditorHelpLabel', function( evt ) {
3255 * evt.data.label = editor.lang.common.editorHelp;
3260 * var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
3263 * @event ariaEditorHelpLabel
3264 * @param {String} label The label to be used.
3265 * @member CKEDITOR.editor
3269 * Event fired when the user double-clicks in the editable area.
3270 * The event allows to open a dialog window for a clicked element in a convenient way:
3272 * editor.on( 'doubleclick', function( evt ) {
3273 * var element = evt.data.element;
3275 * if ( element.is( 'table' ) )
3276 * evt.data.dialog = 'tableProperties';
3279 * **Note:** To handle double-click on a widget use {@link CKEDITOR.plugins.widget#doubleclick}.
3281 * @event doubleclick
3283 * @param {CKEDITOR.dom.element} data.element The double-clicked element.
3284 * @param {String} data.dialog The dialog window to be opened. If set by the listener,
3285 * the specified dialog window will be opened.
3286 * @member CKEDITOR.editor