aboutsummaryrefslogtreecommitdiff
path: root/sources/core/dom
diff options
context:
space:
mode:
Diffstat (limited to 'sources/core/dom')
-rw-r--r--sources/core/dom/comment.js53
-rw-r--r--sources/core/dom/document.js326
-rw-r--r--sources/core/dom/documentfragment.js62
-rw-r--r--sources/core/dom/domobject.js266
-rw-r--r--sources/core/dom/element.js2107
-rw-r--r--sources/core/dom/elementpath.js251
-rw-r--r--sources/core/dom/event.js208
-rw-r--r--sources/core/dom/iterator.js565
-rw-r--r--sources/core/dom/node.js897
-rw-r--r--sources/core/dom/nodelist.js43
-rw-r--r--sources/core/dom/range.js2860
-rw-r--r--sources/core/dom/rangelist.js199
-rw-r--r--sources/core/dom/text.js135
-rw-r--r--sources/core/dom/walker.js652
-rw-r--r--sources/core/dom/window.js95
15 files changed, 8719 insertions, 0 deletions
diff --git a/sources/core/dom/comment.js b/sources/core/dom/comment.js
new file mode 100644
index 00000000..b4e67833
--- /dev/null
+++ b/sources/core/dom/comment.js
@@ -0,0 +1,53 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.comment} class, which represents
8 * a DOM comment node.
9 */
10
11/**
12 * Represents a DOM comment node.
13 *
14 * var nativeNode = document.createComment( 'Example' );
15 * var comment = new CKEDITOR.dom.comment( nativeNode );
16 *
17 * var comment = new CKEDITOR.dom.comment( 'Example' );
18 *
19 * @class
20 * @extends CKEDITOR.dom.node
21 * @constructor Creates a comment class instance.
22 * @param {Object/String} comment A native DOM comment node or a string containing
23 * the text to use to create a new comment node.
24 * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
25 * the node in case of new node creation. Defaults to the current document.
26 */
27CKEDITOR.dom.comment = function( comment, ownerDocument ) {
28 if ( typeof comment == 'string' )
29 comment = ( ownerDocument ? ownerDocument.$ : document ).createComment( comment );
30
31 CKEDITOR.dom.domObject.call( this, comment );
32};
33
34CKEDITOR.dom.comment.prototype = new CKEDITOR.dom.node();
35
36CKEDITOR.tools.extend( CKEDITOR.dom.comment.prototype, {
37 /**
38 * The node type. This is a constant value set to {@link CKEDITOR#NODE_COMMENT}.
39 *
40 * @readonly
41 * @property {Number} [=CKEDITOR.NODE_COMMENT]
42 */
43 type: CKEDITOR.NODE_COMMENT,
44
45 /**
46 * Gets the outer HTML of this comment.
47 *
48 * @returns {String} The HTML `<!-- comment value -->`.
49 */
50 getOuterHtml: function() {
51 return '<!--' + this.$.nodeValue + '-->';
52 }
53} );
diff --git a/sources/core/dom/document.js b/sources/core/dom/document.js
new file mode 100644
index 00000000..ea0290f0
--- /dev/null
+++ b/sources/core/dom/document.js
@@ -0,0 +1,326 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.document} class which
8 * represents a DOM document.
9 */
10
11/**
12 * Represents a DOM document.
13 *
14 * var document = new CKEDITOR.dom.document( document );
15 *
16 * @class
17 * @extends CKEDITOR.dom.domObject
18 * @constructor Creates a document class instance.
19 * @param {Object} domDocument A native DOM document.
20 */
21CKEDITOR.dom.document = function( domDocument ) {
22 CKEDITOR.dom.domObject.call( this, domDocument );
23};
24
25// PACKAGER_RENAME( CKEDITOR.dom.document )
26
27CKEDITOR.dom.document.prototype = new CKEDITOR.dom.domObject();
28
29CKEDITOR.tools.extend( CKEDITOR.dom.document.prototype, {
30 /**
31 * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT}.
32 *
33 * @readonly
34 * @property {Number} [=CKEDITOR.NODE_DOCUMENT]
35 */
36 type: CKEDITOR.NODE_DOCUMENT,
37
38 /**
39 * Appends a CSS file to the document.
40 *
41 * CKEDITOR.document.appendStyleSheet( '/mystyles.css' );
42 *
43 * @param {String} cssFileUrl The CSS file URL.
44 */
45 appendStyleSheet: function( cssFileUrl ) {
46 if ( this.$.createStyleSheet )
47 this.$.createStyleSheet( cssFileUrl );
48 else {
49 var link = new CKEDITOR.dom.element( 'link' );
50 link.setAttributes( {
51 rel: 'stylesheet',
52 type: 'text/css',
53 href: cssFileUrl
54 } );
55
56 this.getHead().append( link );
57 }
58 },
59
60 /**
61 * Creates a CSS stylesheet and inserts it into the document.
62 *
63 * @param cssStyleText {String} CSS style text.
64 * @returns {Object} The created DOM native stylesheet object.
65 */
66 appendStyleText: function( cssStyleText ) {
67 if ( this.$.createStyleSheet ) {
68 var styleSheet = this.$.createStyleSheet( '' );
69 styleSheet.cssText = cssStyleText;
70 } else {
71 var style = new CKEDITOR.dom.element( 'style', this );
72 style.append( new CKEDITOR.dom.text( cssStyleText, this ) );
73 this.getHead().append( style );
74 }
75
76 return styleSheet || style.$.sheet;
77 },
78
79 /**
80 * Creates a {@link CKEDITOR.dom.element} instance in this document.
81 *
82 * @param {String} name The name of the element.
83 * @param {Object} [attributesAndStyles]
84 * @param {Object} [attributesAndStyles.attributes] Attributes that will be set.
85 * @param {Object} [attributesAndStyles.styles] Styles that will be set.
86 * @returns {CKEDITOR.dom.element}
87 */
88 createElement: function( name, attribsAndStyles ) {
89 var element = new CKEDITOR.dom.element( name, this );
90
91 if ( attribsAndStyles ) {
92 if ( attribsAndStyles.attributes )
93 element.setAttributes( attribsAndStyles.attributes );
94
95 if ( attribsAndStyles.styles )
96 element.setStyles( attribsAndStyles.styles );
97 }
98
99 return element;
100 },
101
102 /**
103 * Creates a {@link CKEDITOR.dom.text} instance in this document.
104 *
105 * @param {String} text Value of the text node.
106 * @returns {CKEDITOR.dom.element}
107 */
108 createText: function( text ) {
109 return new CKEDITOR.dom.text( text, this );
110 },
111
112 /**
113 * Moves the selection focus to this document's window.
114 */
115 focus: function() {
116 this.getWindow().focus();
117 },
118
119 /**
120 * Returns the element that is currently designated as the active element in the document.
121 *
122 * **Note:** Only one element can be active at a time in a document.
123 * An active element does not necessarily have focus,
124 * but an element with focus is always the active element in a document.
125 *
126 * @returns {CKEDITOR.dom.element} Active element or `null` if an IE8-9 bug is encountered.
127 * See [#10030](http://dev.ckeditor.com/ticket/10030).
128 */
129 getActive: function() {
130 var $active;
131 try {
132 $active = this.$.activeElement;
133 } catch ( e ) {
134 return null;
135 }
136 return new CKEDITOR.dom.element( $active );
137 },
138
139 /**
140 * Gets an element based on its ID.
141 *
142 * var element = CKEDITOR.document.getById( 'myElement' );
143 * alert( element.getId() ); // 'myElement'
144 *
145 * @param {String} elementId The element ID.
146 * @returns {CKEDITOR.dom.element} The element instance, or `null` if not found.
147 */
148 getById: function( elementId ) {
149 var $ = this.$.getElementById( elementId );
150 return $ ? new CKEDITOR.dom.element( $ ) : null;
151 },
152
153 /**
154 * Gets a node based on its address. See {@link CKEDITOR.dom.node#getAddress}.
155 *
156 * @param {Array} address
157 * @param {Boolean} [normalized=false]
158 */
159 getByAddress: function( address, normalized ) {
160 var $ = this.$.documentElement;
161
162 for ( var i = 0; $ && i < address.length; i++ ) {
163 var target = address[ i ];
164
165 if ( !normalized ) {
166 $ = $.childNodes[ target ];
167 continue;
168 }
169
170 var currentIndex = -1;
171
172 for ( var j = 0; j < $.childNodes.length; j++ ) {
173 var candidate = $.childNodes[ j ];
174
175 if ( normalized === true && candidate.nodeType == 3 && candidate.previousSibling && candidate.previousSibling.nodeType == 3 )
176 continue;
177
178 currentIndex++;
179
180 if ( currentIndex == target ) {
181 $ = candidate;
182 break;
183 }
184 }
185 }
186
187 return $ ? new CKEDITOR.dom.node( $ ) : null;
188 },
189
190 /**
191 * Gets elements list based on a given tag name.
192 *
193 * @param {String} tagName The element tag name.
194 * @returns {CKEDITOR.dom.nodeList} The nodes list.
195 */
196 getElementsByTag: function( tagName, namespace ) {
197 if ( !( CKEDITOR.env.ie && ( document.documentMode <= 8 ) ) && namespace )
198 tagName = namespace + ':' + tagName;
199 return new CKEDITOR.dom.nodeList( this.$.getElementsByTagName( tagName ) );
200 },
201
202 /**
203 * Gets the `<head>` element for this document.
204 *
205 * var element = CKEDITOR.document.getHead();
206 * alert( element.getName() ); // 'head'
207 *
208 * @returns {CKEDITOR.dom.element} The `<head>` element.
209 */
210 getHead: function() {
211 var head = this.$.getElementsByTagName( 'head' )[ 0 ];
212 if ( !head )
213 head = this.getDocumentElement().append( new CKEDITOR.dom.element( 'head' ), true );
214 else
215 head = new CKEDITOR.dom.element( head );
216
217 return head;
218 },
219
220 /**
221 * Gets the `<body>` element for this document.
222 *
223 * var element = CKEDITOR.document.getBody();
224 * alert( element.getName() ); // 'body'
225 *
226 * @returns {CKEDITOR.dom.element} The `<body>` element.
227 */
228 getBody: function() {
229 return new CKEDITOR.dom.element( this.$.body );
230 },
231
232 /**
233 * Gets the DOM document element for this document.
234 *
235 * @returns {CKEDITOR.dom.element} The DOM document element.
236 */
237 getDocumentElement: function() {
238 return new CKEDITOR.dom.element( this.$.documentElement );
239 },
240
241 /**
242 * Gets the window object that stores this document.
243 *
244 * @returns {CKEDITOR.dom.window} The window object.
245 */
246 getWindow: function() {
247 return new CKEDITOR.dom.window( this.$.parentWindow || this.$.defaultView );
248 },
249
250 /**
251 * Defines the document content through `document.write`. Note that the
252 * previous document content will be lost (cleaned).
253 *
254 * document.write(
255 * '<html>' +
256 * '<head><title>Sample Document</title></head>' +
257 * '<body>Document content created by code.</body>' +
258 * '</html>'
259 * );
260 *
261 * @since 3.5
262 * @param {String} html The HTML defining the document content.
263 */
264 write: function( html ) {
265 // Don't leave any history log in IE. (#5657)
266 this.$.open( 'text/html', 'replace' );
267
268 // Support for custom document.domain in IE.
269 //
270 // The script must be appended because if placed before the
271 // doctype, IE will go into quirks mode and mess with
272 // the editable, e.g. by changing its default height.
273 if ( CKEDITOR.env.ie )
274 html = html.replace( /(?:^\s*<!DOCTYPE[^>]*?>)|^/i, '$&\n<script data-cke-temp="1">(' + CKEDITOR.tools.fixDomain + ')();</script>' );
275
276 this.$.write( html );
277 this.$.close();
278 },
279
280 /**
281 * Wrapper for `querySelectorAll`. Returns a list of elements within this document that match
282 * the specified `selector`.
283 *
284 * **Note:** The returned list is not a live collection (like the result of native `querySelectorAll`).
285 *
286 * @since 4.3
287 * @param {String} selector
288 * @returns {CKEDITOR.dom.nodeList}
289 */
290 find: function( selector ) {
291 return new CKEDITOR.dom.nodeList( this.$.querySelectorAll( selector ) );
292 },
293
294 /**
295 * Wrapper for `querySelector`. Returns the first element within this document that matches
296 * the specified `selector`.
297 *
298 * @since 4.3
299 * @param {String} selector
300 * @returns {CKEDITOR.dom.element}
301 */
302 findOne: function( selector ) {
303 var el = this.$.querySelector( selector );
304
305 return el ? new CKEDITOR.dom.element( el ) : null;
306 },
307
308 /**
309 * Internet Explorer 8 only method. It returns a document fragment which has all HTML5 elements enabled.
310 *
311 * @since 4.3
312 * @private
313 * @returns DocumentFragment
314 */
315 _getHtml5ShivFrag: function() {
316 var $frag = this.getCustomData( 'html5ShivFrag' );
317
318 if ( !$frag ) {
319 $frag = this.$.createDocumentFragment();
320 CKEDITOR.tools.enableHtml5Elements( $frag, true );
321 this.setCustomData( 'html5ShivFrag', $frag );
322 }
323
324 return $frag;
325 }
326} );
diff --git a/sources/core/dom/documentfragment.js b/sources/core/dom/documentfragment.js
new file mode 100644
index 00000000..5015144d
--- /dev/null
+++ b/sources/core/dom/documentfragment.js
@@ -0,0 +1,62 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * DocumentFragment is a "lightweight" or "minimal" Document object. It is
8 * commonly used to extract a portion of a document's tree or to create a new
9 * fragment of a document. Various operations may take document fragment objects
10 * as arguments and result in all the child nodes of the document fragment being
11 * moved to the child list of this node.
12 *
13 * @class
14 * @constructor Creates a document fragment class instance.
15 * @param {CKEDITOR.dom.document/DocumentFragment} [nodeOrDoc=CKEDITOR.document]
16 */
17CKEDITOR.dom.documentFragment = function( nodeOrDoc ) {
18 nodeOrDoc = nodeOrDoc || CKEDITOR.document;
19
20 if ( nodeOrDoc.type == CKEDITOR.NODE_DOCUMENT )
21 this.$ = nodeOrDoc.$.createDocumentFragment();
22 else
23 this.$ = nodeOrDoc;
24};
25
26CKEDITOR.tools.extend( CKEDITOR.dom.documentFragment.prototype, CKEDITOR.dom.element.prototype, {
27 /**
28 * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
29 *
30 * @readonly
31 * @property {Number} [=CKEDITOR.NODE_DOCUMENT_FRAGMENT]
32 */
33 type: CKEDITOR.NODE_DOCUMENT_FRAGMENT,
34
35 /**
36 * Inserts the document fragment content after the specified node.
37 *
38 * @param {CKEDITOR.dom.node} node
39 */
40 insertAfterNode: function( node ) {
41 node = node.$;
42 node.parentNode.insertBefore( this.$, node.nextSibling );
43 },
44
45 /**
46 * Gets HTML of this document fragment's children.
47 *
48 * @since 4.5
49 * @returns {String} The HTML of this document fragment's children.
50 */
51 getHtml: function() {
52 var container = new CKEDITOR.dom.element( 'div' );
53
54 this.clone( 1, 1 ).appendTo( container );
55
56 return container.getHtml().replace( /\s*data-cke-expando=".*?"/g, '' );
57 }
58}, true, {
59 'append': 1, 'appendBogus': 1, 'clone': 1, 'getFirst': 1, 'getHtml': 1, 'getLast': 1, 'getParent': 1, 'getNext': 1, 'getPrevious': 1,
60 'appendTo': 1, 'moveChildren': 1, 'insertBefore': 1, 'insertAfterNode': 1, 'replace': 1, 'trim': 1, 'type': 1,
61 'ltrim': 1, 'rtrim': 1, 'getDocument': 1, 'getChildCount': 1, 'getChild': 1, 'getChildren': 1
62} );
diff --git a/sources/core/dom/domobject.js b/sources/core/dom/domobject.js
new file mode 100644
index 00000000..21a351d7
--- /dev/null
+++ b/sources/core/dom/domobject.js
@@ -0,0 +1,266 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.editor} class, which is the base
8 * for other classes representing DOM objects.
9 */
10
11/**
12 * Represents a DOM object. This class is not intended to be used directly. It
13 * serves as the base class for other classes representing specific DOM
14 * objects.
15 *
16 * @class
17 * @mixins CKEDITOR.event
18 * @constructor Creates a domObject class instance.
19 * @param {Object} nativeDomObject A native DOM object.
20 */
21CKEDITOR.dom.domObject = function( nativeDomObject ) {
22 if ( nativeDomObject ) {
23 /**
24 * The native DOM object represented by this class instance.
25 *
26 * var element = new CKEDITOR.dom.element( 'span' );
27 * alert( element.$.nodeType ); // '1'
28 *
29 * @readonly
30 * @property {Object}
31 */
32 this.$ = nativeDomObject;
33 }
34};
35
36CKEDITOR.dom.domObject.prototype = ( function() {
37 // Do not define other local variables here. We want to keep the native
38 // listener closures as clean as possible.
39
40 var getNativeListener = function( domObject, eventName ) {
41 return function( domEvent ) {
42 // In FF, when reloading the page with the editor focused, it may
43 // throw an error because the CKEDITOR global is not anymore
44 // available. So, we check it here first. (#2923)
45 if ( typeof CKEDITOR != 'undefined' )
46 domObject.fire( eventName, new CKEDITOR.dom.event( domEvent ) );
47 };
48 };
49
50 return {
51
52 /**
53 * Gets the private `_` object which is bound to the native
54 * DOM object using {@link #getCustomData}.
55 *
56 * var elementA = new CKEDITOR.dom.element( nativeElement );
57 * elementA.getPrivate().value = 1;
58 * ...
59 * var elementB = new CKEDITOR.dom.element( nativeElement );
60 * elementB.getPrivate().value; // 1
61 *
62 * @returns {Object} The private object.
63 */
64 getPrivate: function() {
65 var priv;
66
67 // Get the main private object from the custom data. Create it if not defined.
68 if ( !( priv = this.getCustomData( '_' ) ) )
69 this.setCustomData( '_', ( priv = {} ) );
70
71 return priv;
72 },
73
74 // Docs inherited from event.
75 on: function( eventName ) {
76 // We customize the "on" function here. The basic idea is that we'll have
77 // only one listener for a native event, which will then call all listeners
78 // set to the event.
79
80 // Get the listeners holder object.
81 var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
82
83 if ( !nativeListeners ) {
84 nativeListeners = {};
85 this.setCustomData( '_cke_nativeListeners', nativeListeners );
86 }
87
88 // Check if we have a listener for that event.
89 if ( !nativeListeners[ eventName ] ) {
90 var listener = nativeListeners[ eventName ] = getNativeListener( this, eventName );
91
92 if ( this.$.addEventListener )
93 this.$.addEventListener( eventName, listener, !!CKEDITOR.event.useCapture );
94 else if ( this.$.attachEvent )
95 this.$.attachEvent( 'on' + eventName, listener );
96 }
97
98 // Call the original implementation.
99 return CKEDITOR.event.prototype.on.apply( this, arguments );
100 },
101
102 // Docs inherited from event.
103 removeListener: function( eventName ) {
104 // Call the original implementation.
105 CKEDITOR.event.prototype.removeListener.apply( this, arguments );
106
107 // If we don't have listeners for this event, clean the DOM up.
108 if ( !this.hasListeners( eventName ) ) {
109 var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
110 var listener = nativeListeners && nativeListeners[ eventName ];
111 if ( listener ) {
112 if ( this.$.removeEventListener )
113 this.$.removeEventListener( eventName, listener, false );
114 else if ( this.$.detachEvent )
115 this.$.detachEvent( 'on' + eventName, listener );
116
117 delete nativeListeners[ eventName ];
118 }
119 }
120 },
121
122 /**
123 * Removes any listener set on this object.
124 *
125 * To avoid memory leaks we must assure that there are no
126 * references left after the object is no longer needed.
127 */
128 removeAllListeners: function() {
129 var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
130 for ( var eventName in nativeListeners ) {
131 var listener = nativeListeners[ eventName ];
132 if ( this.$.detachEvent )
133 this.$.detachEvent( 'on' + eventName, listener );
134 else if ( this.$.removeEventListener )
135 this.$.removeEventListener( eventName, listener, false );
136
137 delete nativeListeners[ eventName ];
138 }
139
140 // Remove events from events object so fire() method will not call
141 // listeners (#11400).
142 CKEDITOR.event.prototype.removeAllListeners.call( this );
143 }
144 };
145} )();
146
147( function( domObjectProto ) {
148 var customData = {};
149
150 CKEDITOR.on( 'reset', function() {
151 customData = {};
152 } );
153
154 /**
155 * Determines whether the specified object is equal to the current object.
156 *
157 * var doc = new CKEDITOR.dom.document( document );
158 * alert( doc.equals( CKEDITOR.document ) ); // true
159 * alert( doc == CKEDITOR.document ); // false
160 *
161 * @param {Object} object The object to compare with the current object.
162 * @returns {Boolean} `true` if the object is equal.
163 */
164 domObjectProto.equals = function( object ) {
165 // Try/Catch to avoid IE permission error when object is from different document.
166 try {
167 return ( object && object.$ === this.$ );
168 } catch ( er ) {
169 return false;
170 }
171 };
172
173 /**
174 * Sets a data slot value for this object. These values are shared by all
175 * instances pointing to that same DOM object.
176 *
177 * **Note:** The created data slot is only guaranteed to be available on this unique DOM node,
178 * thus any wish to continue access to it from other element clones (either created by
179 * clone node or from `innerHtml`) will fail. For such usage please use
180 * {@link CKEDITOR.dom.element#setAttribute} instead.
181 *
182 * **Note**: This method does not work on text nodes prior to Internet Explorer 9.
183 *
184 * var element = new CKEDITOR.dom.element( 'span' );
185 * element.setCustomData( 'hasCustomData', true );
186 *
187 * @param {String} key A key used to identify the data slot.
188 * @param {Object} value The value to set to the data slot.
189 * @returns {CKEDITOR.dom.domObject} This DOM object instance.
190 * @chainable
191 */
192 domObjectProto.setCustomData = function( key, value ) {
193 var expandoNumber = this.getUniqueId(),
194 dataSlot = customData[ expandoNumber ] || ( customData[ expandoNumber ] = {} );
195
196 dataSlot[ key ] = value;
197
198 return this;
199 };
200
201 /**
202 * Gets the value set to a data slot in this object.
203 *
204 * var element = new CKEDITOR.dom.element( 'span' );
205 * alert( element.getCustomData( 'hasCustomData' ) ); // e.g. 'true'
206 * alert( element.getCustomData( 'nonExistingKey' ) ); // null
207 *
208 * @param {String} key The key used to identify the data slot.
209 * @returns {Object} This value set to the data slot.
210 */
211 domObjectProto.getCustomData = function( key ) {
212 var expandoNumber = this.$[ 'data-cke-expando' ],
213 dataSlot = expandoNumber && customData[ expandoNumber ];
214
215 return ( dataSlot && key in dataSlot ) ? dataSlot[ key ] : null;
216 };
217
218 /**
219 * Removes the value in the data slot under the given `key`.
220 *
221 * @param {String} key
222 * @returns {Object} Removed value or `null` if not found.
223 */
224 domObjectProto.removeCustomData = function( key ) {
225 var expandoNumber = this.$[ 'data-cke-expando' ],
226 dataSlot = expandoNumber && customData[ expandoNumber ],
227 retval, hadKey;
228
229 if ( dataSlot ) {
230 retval = dataSlot[ key ];
231 hadKey = key in dataSlot;
232 delete dataSlot[ key ];
233 }
234
235 return hadKey ? retval : null;
236 };
237
238 /**
239 * Removes any data stored in this object.
240 * To avoid memory leaks we must assure that there are no
241 * references left after the object is no longer needed.
242 */
243 domObjectProto.clearCustomData = function() {
244 // Clear all event listeners
245 this.removeAllListeners();
246
247 var expandoNumber = this.$[ 'data-cke-expando' ];
248 expandoNumber && delete customData[ expandoNumber ];
249 };
250
251 /**
252 * Gets an ID that can be used to identify this DOM object in
253 * the running session.
254 *
255 * **Note**: This method does not work on text nodes prior to Internet Explorer 9.
256 *
257 * @returns {Number} A unique ID.
258 */
259 domObjectProto.getUniqueId = function() {
260 return this.$[ 'data-cke-expando' ] || ( this.$[ 'data-cke-expando' ] = CKEDITOR.tools.getNextNumber() );
261 };
262
263 // Implement CKEDITOR.event.
264 CKEDITOR.event.implementOn( domObjectProto );
265
266} )( CKEDITOR.dom.domObject.prototype );
diff --git a/sources/core/dom/element.js b/sources/core/dom/element.js
new file mode 100644
index 00000000..d5181282
--- /dev/null
+++ b/sources/core/dom/element.js
@@ -0,0 +1,2107 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.element} class, which
8 * represents a DOM element.
9 */
10
11/**
12 * Represents a DOM element.
13 *
14 * // Create a new <span> element.
15 * var element = new CKEDITOR.dom.element( 'span' );
16 *
17 * // Create an element based on a native DOM element.
18 * var element = new CKEDITOR.dom.element( document.getElementById( 'myId' ) );
19 *
20 * @class
21 * @extends CKEDITOR.dom.node
22 * @constructor Creates an element class instance.
23 * @param {Object/String} element A native DOM element or the element name for
24 * new elements.
25 * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
26 * the element in case of element creation.
27 */
28CKEDITOR.dom.element = function( element, ownerDocument ) {
29 if ( typeof element == 'string' )
30 element = ( ownerDocument ? ownerDocument.$ : document ).createElement( element );
31
32 // Call the base constructor (we must not call CKEDITOR.dom.node).
33 CKEDITOR.dom.domObject.call( this, element );
34};
35
36// PACKAGER_RENAME( CKEDITOR.dom.element )
37/**
38 * The the {@link CKEDITOR.dom.element} representing and element. If the
39 * element is a native DOM element, it will be transformed into a valid
40 * CKEDITOR.dom.element object.
41 *
42 * var element = new CKEDITOR.dom.element( 'span' );
43 * alert( element == CKEDITOR.dom.element.get( element ) ); // true
44 *
45 * var element = document.getElementById( 'myElement' );
46 * alert( CKEDITOR.dom.element.get( element ).getName() ); // (e.g.) 'p'
47 *
48 * @static
49 * @param {String/Object} element Element's id or name or native DOM element.
50 * @returns {CKEDITOR.dom.element} The transformed element.
51 */
52CKEDITOR.dom.element.get = function( element ) {
53 var el = typeof element == 'string' ? document.getElementById( element ) || document.getElementsByName( element )[ 0 ] : element;
54
55 return el && ( el.$ ? el : new CKEDITOR.dom.element( el ) );
56};
57
58CKEDITOR.dom.element.prototype = new CKEDITOR.dom.node();
59
60/**
61 * Creates an instance of the {@link CKEDITOR.dom.element} class based on the
62 * HTML representation of an element.
63 *
64 * var element = CKEDITOR.dom.element.createFromHtml( '<strong class="anyclass">My element</strong>' );
65 * alert( element.getName() ); // 'strong'
66 *
67 * @static
68 * @param {String} html The element HTML. It should define only one element in
69 * the "root" level. The "root" element can have child nodes, but not siblings.
70 * @returns {CKEDITOR.dom.element} The element instance.
71 */
72CKEDITOR.dom.element.createFromHtml = function( html, ownerDocument ) {
73 var temp = new CKEDITOR.dom.element( 'div', ownerDocument );
74 temp.setHtml( html );
75
76 // When returning the node, remove it from its parent to detach it.
77 return temp.getFirst().remove();
78};
79
80/**
81 * Sets {@link CKEDITOR.dom.element#setCustomData custom data} on an element in a way that it is later
82 * possible to {@link #clearAllMarkers clear all data} set on all elements sharing the same database.
83 *
84 * This mechanism is very useful when processing some portion of DOM. All markers can later be removed
85 * by calling the {@link #clearAllMarkers} method, hence markers will not leak to second pass of this algorithm.
86 *
87 * var database = {};
88 * CKEDITOR.dom.element.setMarker( database, element1, 'foo', 'bar' );
89 * CKEDITOR.dom.element.setMarker( database, element2, 'oof', [ 1, 2, 3 ] );
90 *
91 * element1.getCustomData( 'foo' ); // 'bar'
92 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
93 *
94 * CKEDITOR.dom.element.clearAllMarkers( database );
95 *
96 * element1.getCustomData( 'foo' ); // null
97 *
98 * @static
99 * @param {Object} database
100 * @param {CKEDITOR.dom.element} element
101 * @param {String} name
102 * @param {Object} value
103 * @returns {CKEDITOR.dom.element} The element.
104 */
105CKEDITOR.dom.element.setMarker = function( database, element, name, value ) {
106 var id = element.getCustomData( 'list_marker_id' ) || ( element.setCustomData( 'list_marker_id', CKEDITOR.tools.getNextNumber() ).getCustomData( 'list_marker_id' ) ),
107 markerNames = element.getCustomData( 'list_marker_names' ) || ( element.setCustomData( 'list_marker_names', {} ).getCustomData( 'list_marker_names' ) );
108 database[ id ] = element;
109 markerNames[ name ] = 1;
110
111 return element.setCustomData( name, value );
112};
113
114/**
115 * Removes all markers added using this database. See the {@link #setMarker} method for more information.
116 *
117 * @param {Object} database
118 * @static
119 */
120CKEDITOR.dom.element.clearAllMarkers = function( database ) {
121 for ( var i in database )
122 CKEDITOR.dom.element.clearMarkers( database, database[ i ], 1 );
123};
124
125/**
126 * Removes all markers added to this element and removes it from the database if
127 * `removeFromDatabase` was passed. See the {@link #setMarker} method for more information.
128 *
129 * var database = {};
130 * CKEDITOR.dom.element.setMarker( database, element1, 'foo', 'bar' );
131 * CKEDITOR.dom.element.setMarker( database, element2, 'oof', [ 1, 2, 3 ] );
132 *
133 * element1.getCustomData( 'foo' ); // 'bar'
134 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
135 *
136 * CKEDITOR.dom.element.clearMarkers( database, element1, true );
137 *
138 * element1.getCustomData( 'foo' ); // null
139 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
140 *
141 * @param {Object} database
142 * @static
143 */
144CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatabase ) {
145 var names = element.getCustomData( 'list_marker_names' ),
146 id = element.getCustomData( 'list_marker_id' );
147 for ( var i in names )
148 element.removeCustomData( i );
149 element.removeCustomData( 'list_marker_names' );
150 if ( removeFromDatabase ) {
151 element.removeCustomData( 'list_marker_id' );
152 delete database[ id ];
153 }
154};
155
156( function() {
157 var elementsClassList = document.createElement( '_' ).classList,
158 supportsClassLists = typeof elementsClassList !== 'undefined' && String( elementsClassList.add ).match( /\[Native code\]/gi ) !== null,
159 rclass = /[\n\t\r]/g;
160
161 function hasClass( classNames, className ) {
162 // Source: jQuery.
163 return ( ' ' + classNames + ' ' ).replace( rclass, ' ' ).indexOf( ' ' + className + ' ' ) > -1;
164 }
165
166 CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, {
167 /**
168 * The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}.
169 *
170 * @readonly
171 * @property {Number} [=CKEDITOR.NODE_ELEMENT]
172 */
173 type: CKEDITOR.NODE_ELEMENT,
174
175 /**
176 * Adds a CSS class to the element. It appends the class to the
177 * already existing names.
178 *
179 * var element = new CKEDITOR.dom.element( 'div' );
180 * element.addClass( 'classA' ); // <div class="classA">
181 * element.addClass( 'classB' ); // <div class="classA classB">
182 * element.addClass( 'classA' ); // <div class="classA classB">
183 *
184 * **Note:** Since CKEditor 4.5 this method cannot be used with multiple classes (`'classA classB'`).
185 *
186 * @chainable
187 * @method addClass
188 * @param {String} className The name of the class to be added.
189 */
190 addClass: supportsClassLists ?
191 function( className ) {
192 this.$.classList.add( className );
193
194 return this;
195 } : function( className ) {
196 var c = this.$.className;
197 if ( c ) {
198 if ( !hasClass( c, className ) )
199 c += ' ' + className;
200 }
201 this.$.className = c || className;
202
203 return this;
204 },
205
206 /**
207 * Removes a CSS class name from the elements classes. Other classes
208 * remain untouched.
209 *
210 * var element = new CKEDITOR.dom.element( 'div' );
211 * element.addClass( 'classA' ); // <div class="classA">
212 * element.addClass( 'classB' ); // <div class="classA classB">
213 * element.removeClass( 'classA' ); // <div class="classB">
214 * element.removeClass( 'classB' ); // <div>
215 *
216 * @chainable
217 * @method removeClass
218 * @param {String} className The name of the class to remove.
219 */
220 removeClass: supportsClassLists ?
221 function( className ) {
222 var $ = this.$;
223 $.classList.remove( className );
224
225 if ( !$.className )
226 $.removeAttribute( 'class' );
227
228 return this;
229 } : function( className ) {
230 var c = this.getAttribute( 'class' );
231 if ( c && hasClass( c, className ) ) {
232 c = c
233 .replace( new RegExp( '(?:^|\\s+)' + className + '(?=\\s|$)' ), '' )
234 .replace( /^\s+/, '' );
235
236 if ( c )
237 this.setAttribute( 'class', c );
238 else
239 this.removeAttribute( 'class' );
240 }
241
242 return this;
243 },
244
245 /**
246 * Checks if element has class name.
247 *
248 * @param {String} className
249 * @returns {Boolean}
250 */
251 hasClass: function( className ) {
252 return hasClass( this.$.className, className );
253 },
254
255 /**
256 * Append a node as a child of this element.
257 *
258 * var p = new CKEDITOR.dom.element( 'p' );
259 *
260 * var strong = new CKEDITOR.dom.element( 'strong' );
261 * p.append( strong );
262 *
263 * var em = p.append( 'em' );
264 *
265 * // Result: '<p><strong></strong><em></em></p>'
266 *
267 * @param {CKEDITOR.dom.node/String} node The node or element name to be appended.
268 * @param {Boolean} [toStart=false] Indicates that the element is to be appended at the start.
269 * @returns {CKEDITOR.dom.node} The appended node.
270 */
271 append: function( node, toStart ) {
272 if ( typeof node == 'string' )
273 node = this.getDocument().createElement( node );
274
275 if ( toStart )
276 this.$.insertBefore( node.$, this.$.firstChild );
277 else
278 this.$.appendChild( node.$ );
279
280 return node;
281 },
282
283 /**
284 * Append HTML as a child(ren) of this element.
285 *
286 * @param {String} html
287 */
288 appendHtml: function( html ) {
289 if ( !this.$.childNodes.length )
290 this.setHtml( html );
291 else {
292 var temp = new CKEDITOR.dom.element( 'div', this.getDocument() );
293 temp.setHtml( html );
294 temp.moveChildren( this );
295 }
296 },
297
298 /**
299 * Append text to this element.
300 *
301 * var p = new CKEDITOR.dom.element( 'p' );
302 * p.appendText( 'This is' );
303 * p.appendText( ' some text' );
304 *
305 * // Result: '<p>This is some text</p>'
306 *
307 * @param {String} text The text to be appended.
308 */
309 appendText: function( text ) {
310 // On IE8 it is impossible to append node to script tag, so we use its text.
311 // On the contrary, on Safari the text property is unpredictable in links. (#13232)
312 if ( this.$.text != null && CKEDITOR.env.ie && CKEDITOR.env.version < 9 )
313 this.$.text += text;
314 else
315 this.append( new CKEDITOR.dom.text( text ) );
316 },
317
318 /**
319 * Appends a `<br>` filler element to this element if the filler is not present already.
320 * By default filler is appended only if {@link CKEDITOR.env#needsBrFiller} is `true`,
321 * however when `force` is set to `true` filler will be appended regardless of the environment.
322 *
323 * @param {Boolean} [force] Append filler regardless of the environment.
324 */
325 appendBogus: function( force ) {
326 if ( !force && !CKEDITOR.env.needsBrFiller )
327 return;
328
329 var lastChild = this.getLast();
330
331 // Ignore empty/spaces text.
332 while ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.rtrim( lastChild.getText() ) )
333 lastChild = lastChild.getPrevious();
334 if ( !lastChild || !lastChild.is || !lastChild.is( 'br' ) ) {
335 var bogus = this.getDocument().createElement( 'br' );
336
337 CKEDITOR.env.gecko && bogus.setAttribute( 'type', '_moz' );
338
339 this.append( bogus );
340 }
341 },
342
343 /**
344 * Breaks one of the ancestor element in the element position, moving
345 * this element between the broken parts.
346 *
347 * // Before breaking:
348 * // <b>This <i>is some<span /> sample</i> test text</b>
349 * // If "element" is <span /> and "parent" is <i>:
350 * // <b>This <i>is some</i><span /><i> sample</i> test text</b>
351 * element.breakParent( parent );
352 *
353 * // Before breaking:
354 * // <b>This <i>is some<span /> sample</i> test text</b>
355 * // If "element" is <span /> and "parent" is <b>:
356 * // <b>This <i>is some</i></b><span /><b><i> sample</i> test text</b>
357 * element.breakParent( parent );
358 *
359 * @param {CKEDITOR.dom.element} parent The anscestor element to get broken.
360 * @param {Boolean} [cloneId=false] Whether to preserve ancestor ID attributes while breaking.
361 */
362 breakParent: function( parent, cloneId ) {
363 var range = new CKEDITOR.dom.range( this.getDocument() );
364
365 // We'll be extracting part of this element, so let's use our
366 // range to get the correct piece.
367 range.setStartAfter( this );
368 range.setEndAfter( parent );
369
370 // Extract it.
371 var docFrag = range.extractContents( false, cloneId || false );
372
373 // Move the element outside the broken element.
374 range.insertNode( this.remove() );
375
376 // Re-insert the extracted piece after the element.
377 docFrag.insertAfterNode( this );
378 },
379
380 /**
381 * Checks if this element contains given node.
382 *
383 * @method
384 * @param {CKEDITOR.dom.node} node
385 * @returns {Boolean}
386 */
387 contains: !document.compareDocumentPosition ?
388 function( node ) {
389 var $ = this.$;
390
391 return node.type != CKEDITOR.NODE_ELEMENT ? $.contains( node.getParent().$ ) : $ != node.$ && $.contains( node.$ );
392 } : function( node ) {
393 return !!( this.$.compareDocumentPosition( node.$ ) & 16 );
394 },
395
396 /**
397 * Moves the selection focus to this element.
398 *
399 * var element = CKEDITOR.document.getById( 'myTextarea' );
400 * element.focus();
401 *
402 * @method
403 * @param {Boolean} defer Whether to asynchronously defer the
404 * execution by 100 ms.
405 */
406 focus: ( function() {
407 function exec() {
408 // IE throws error if the element is not visible.
409 try {
410 this.$.focus();
411 } catch ( e ) {}
412 }
413
414 return function( defer ) {
415 if ( defer )
416 CKEDITOR.tools.setTimeout( exec, 100, this );
417 else
418 exec.call( this );
419 };
420 } )(),
421
422 /**
423 * Gets the inner HTML of this element.
424 *
425 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
426 * alert( element.getHtml() ); // '<b>Example</b>'
427 *
428 * @returns {String} The inner HTML of this element.
429 */
430 getHtml: function() {
431 var retval = this.$.innerHTML;
432 // Strip <?xml:namespace> tags in IE. (#3341).
433 return CKEDITOR.env.ie ? retval.replace( /<\?[^>]*>/g, '' ) : retval;
434 },
435
436 /**
437 * Gets the outer (inner plus tags) HTML of this element.
438 *
439 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="bold"><b>Example</b></div>' );
440 * alert( element.getOuterHtml() ); // '<div class="bold"><b>Example</b></div>'
441 *
442 * @returns {String} The outer HTML of this element.
443 */
444 getOuterHtml: function() {
445 if ( this.$.outerHTML ) {
446 // IE includes the <?xml:namespace> tag in the outerHTML of
447 // namespaced element. So, we must strip it here. (#3341)
448 return this.$.outerHTML.replace( /<\?[^>]*>/, '' );
449 }
450
451 var tmpDiv = this.$.ownerDocument.createElement( 'div' );
452 tmpDiv.appendChild( this.$.cloneNode( true ) );
453 return tmpDiv.innerHTML;
454 },
455
456 /**
457 * Retrieve the bounding rectangle of the current element, in pixels,
458 * relative to the upper-left corner of the browser's client area.
459 *
460 * @returns {Object} The dimensions of the DOM element including
461 * `left`, `top`, `right`, `bottom`, `width` and `height`.
462 */
463 getClientRect: function() {
464 // http://help.dottoro.com/ljvmcrrn.php
465 var rect = CKEDITOR.tools.extend( {}, this.$.getBoundingClientRect() );
466
467 !rect.width && ( rect.width = rect.right - rect.left );
468 !rect.height && ( rect.height = rect.bottom - rect.top );
469
470 return rect;
471 },
472
473 /**
474 * Sets the inner HTML of this element.
475 *
476 * var p = new CKEDITOR.dom.element( 'p' );
477 * p.setHtml( '<b>Inner</b> HTML' );
478 *
479 * // Result: '<p><b>Inner</b> HTML</p>'
480 *
481 * @method
482 * @param {String} html The HTML to be set for this element.
483 * @returns {String} The inserted HTML.
484 */
485 setHtml: ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) ?
486 // old IEs throws error on HTML manipulation (through the "innerHTML" property)
487 // on the element which resides in an DTD invalid position, e.g. <span><div></div></span>
488 // fortunately it can be worked around with DOM manipulation.
489 function( html ) {
490 try {
491 var $ = this.$;
492
493 // Fix the case when setHtml is called on detached element.
494 // HTML5 shiv used for document in which this element was created
495 // won't affect that detached element. So get document fragment with
496 // all HTML5 elements enabled and set innerHTML while this element is appended to it.
497 if ( this.getParent() )
498 return ( $.innerHTML = html );
499 else {
500 var $frag = this.getDocument()._getHtml5ShivFrag();
501 $frag.appendChild( $ );
502 $.innerHTML = html;
503 $frag.removeChild( $ );
504
505 return html;
506 }
507 }
508 catch ( e ) {
509 this.$.innerHTML = '';
510
511 var temp = new CKEDITOR.dom.element( 'body', this.getDocument() );
512 temp.$.innerHTML = html;
513
514 var children = temp.getChildren();
515 while ( children.count() )
516 this.append( children.getItem( 0 ) );
517
518 return html;
519 }
520 } : function( html ) {
521 return ( this.$.innerHTML = html );
522 },
523
524 /**
525 * Sets the element contents as plain text.
526 *
527 * var element = new CKEDITOR.dom.element( 'div' );
528 * element.setText( 'A > B & C < D' );
529 * alert( element.innerHTML ); // 'A &gt; B &amp; C &lt; D'
530 *
531 * @param {String} text The text to be set.
532 * @returns {String} The inserted text.
533 */
534 setText: ( function() {
535 var supportsTextContent = document.createElement( 'p' );
536 supportsTextContent.innerHTML = 'x';
537 supportsTextContent = supportsTextContent.textContent;
538
539 return function( text ) {
540 this.$[ supportsTextContent ? 'textContent' : 'innerText' ] = text;
541 };
542 } )(),
543
544 /**
545 * Gets the value of an element attribute.
546 *
547 * var element = CKEDITOR.dom.element.createFromHtml( '<input type="text" />' );
548 * alert( element.getAttribute( 'type' ) ); // 'text'
549 *
550 * @method
551 * @param {String} name The attribute name.
552 * @returns {String} The attribute value or null if not defined.
553 */
554 getAttribute: ( function() {
555 var standard = function( name ) {
556 return this.$.getAttribute( name, 2 );
557 };
558
559 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
560 return function( name ) {
561 switch ( name ) {
562 case 'class':
563 name = 'className';
564 break;
565
566 case 'http-equiv':
567 name = 'httpEquiv';
568 break;
569
570 case 'name':
571 return this.$.name;
572
573 case 'tabindex':
574 var tabIndex = standard.call( this, name );
575
576 // IE returns tabIndex=0 by default for all
577 // elements. For those elements,
578 // getAtrribute( 'tabindex', 2 ) returns 32768
579 // instead. So, we must make this check to give a
580 // uniform result among all browsers.
581 if ( tabIndex !== 0 && this.$.tabIndex === 0 )
582 tabIndex = null;
583
584 return tabIndex;
585
586 case 'checked':
587 var attr = this.$.attributes.getNamedItem( name ),
588 attrValue = attr.specified ? attr.nodeValue // For value given by parser.
589 : this.$.checked; // For value created via DOM interface.
590
591 return attrValue ? 'checked' : null;
592
593 case 'hspace':
594 case 'value':
595 return this.$[ name ];
596
597 case 'style':
598 // IE does not return inline styles via getAttribute(). See #2947.
599 return this.$.style.cssText;
600
601 case 'contenteditable':
602 case 'contentEditable':
603 return this.$.attributes.getNamedItem( 'contentEditable' ).specified ? this.$.getAttribute( 'contentEditable' ) : null;
604 }
605
606 return standard.call( this, name );
607 };
608 } else {
609 return standard;
610 }
611 } )(),
612
613 /**
614 * Gets the nodes list containing all children of this element.
615 *
616 * @returns {CKEDITOR.dom.nodeList}
617 */
618 getChildren: function() {
619 return new CKEDITOR.dom.nodeList( this.$.childNodes );
620 },
621
622 /**
623 * Gets the current computed value of one of the element CSS style
624 * properties.
625 *
626 * var element = new CKEDITOR.dom.element( 'span' );
627 * alert( element.getComputedStyle( 'display' ) ); // 'inline'
628 *
629 * @method
630 * @param {String} propertyName The style property name.
631 * @returns {String} The property value.
632 */
633 getComputedStyle: ( document.defaultView && document.defaultView.getComputedStyle ) ?
634 function( propertyName ) {
635 var style = this.getWindow().$.getComputedStyle( this.$, null );
636
637 // Firefox may return null if we call the above on a hidden iframe. (#9117)
638 return style ? style.getPropertyValue( propertyName ) : '';
639 } : function( propertyName ) {
640 return this.$.currentStyle[ CKEDITOR.tools.cssStyleToDomStyle( propertyName ) ];
641 },
642
643 /**
644 * Gets the DTD entries for this element.
645 *
646 * @returns {Object} An object containing the list of elements accepted
647 * by this element.
648 */
649 getDtd: function() {
650 var dtd = CKEDITOR.dtd[ this.getName() ];
651
652 this.getDtd = function() {
653 return dtd;
654 };
655
656 return dtd;
657 },
658
659 /**
660 * Gets all this element's descendants having given tag name.
661 *
662 * @method
663 * @param {String} tagName
664 */
665 getElementsByTag: CKEDITOR.dom.document.prototype.getElementsByTag,
666
667 /**
668 * Gets the computed tabindex for this element.
669 *
670 * var element = CKEDITOR.document.getById( 'myDiv' );
671 * alert( element.getTabIndex() ); // (e.g.) '-1'
672 *
673 * @method
674 * @returns {Number} The tabindex value.
675 */
676 getTabIndex: function() {
677 var tabIndex = this.$.tabIndex;
678
679 // IE returns tabIndex=0 by default for all elements. In
680 // those cases we must check that the element really has
681 // the tabindex attribute set to zero, or it is one of
682 // those element that should have zero by default.
683 if ( tabIndex === 0 && !CKEDITOR.dtd.$tabIndex[ this.getName() ] && parseInt( this.getAttribute( 'tabindex' ), 10 ) !== 0 )
684 return -1;
685
686 return tabIndex;
687 },
688
689 /**
690 * Gets the text value of this element.
691 *
692 * Only in IE (which uses innerText), `<br>` will cause linebreaks,
693 * and sucessive whitespaces (including line breaks) will be reduced to
694 * a single space. This behavior is ok for us, for now. It may change
695 * in the future.
696 *
697 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Sample <i>text</i>.</div>' );
698 * alert( <b>element.getText()</b> ); // 'Sample text.'
699 *
700 * @returns {String} The text value.
701 */
702 getText: function() {
703 return this.$.textContent || this.$.innerText || '';
704 },
705
706 /**
707 * Gets the window object that contains this element.
708 *
709 * @returns {CKEDITOR.dom.window} The window object.
710 */
711 getWindow: function() {
712 return this.getDocument().getWindow();
713 },
714
715 /**
716 * Gets the value of the `id` attribute of this element.
717 *
718 * var element = CKEDITOR.dom.element.createFromHtml( '<p id="myId"></p>' );
719 * alert( element.getId() ); // 'myId'
720 *
721 * @returns {String} The element id, or null if not available.
722 */
723 getId: function() {
724 return this.$.id || null;
725 },
726
727 /**
728 * Gets the value of the `name` attribute of this element.
729 *
730 * var element = CKEDITOR.dom.element.createFromHtml( '<input name="myName"></input>' );
731 * alert( <b>element.getNameAtt()</b> ); // 'myName'
732 *
733 * @returns {String} The element name, or null if not available.
734 */
735 getNameAtt: function() {
736 return this.$.name || null;
737 },
738
739 /**
740 * Gets the element name (tag name). The returned name is guaranteed to
741 * be always full lowercased.
742 *
743 * var element = new CKEDITOR.dom.element( 'span' );
744 * alert( element.getName() ); // 'span'
745 *
746 * @returns {String} The element name.
747 */
748 getName: function() {
749 // Cache the lowercased name inside a closure.
750 var nodeName = this.$.nodeName.toLowerCase();
751
752 if ( CKEDITOR.env.ie && ( document.documentMode <= 8 ) ) {
753 var scopeName = this.$.scopeName;
754 if ( scopeName != 'HTML' )
755 nodeName = scopeName.toLowerCase() + ':' + nodeName;
756 }
757
758 this.getName = function() {
759 return nodeName;
760 };
761
762 return this.getName();
763 },
764
765 /**
766 * Gets the value set to this element. This value is usually available
767 * for form field elements.
768 *
769 * @returns {String} The element value.
770 */
771 getValue: function() {
772 return this.$.value;
773 },
774
775 /**
776 * Gets the first child node of this element.
777 *
778 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
779 * var first = element.getFirst();
780 * alert( first.getName() ); // 'b'
781 *
782 * @param {Function} evaluator Filtering the result node.
783 * @returns {CKEDITOR.dom.node} The first child node or null if not available.
784 */
785 getFirst: function( evaluator ) {
786 var first = this.$.firstChild,
787 retval = first && new CKEDITOR.dom.node( first );
788 if ( retval && evaluator && !evaluator( retval ) )
789 retval = retval.getNext( evaluator );
790
791 return retval;
792 },
793
794 /**
795 * See {@link #getFirst}.
796 *
797 * @param {Function} evaluator Filtering the result node.
798 * @returns {CKEDITOR.dom.node}
799 */
800 getLast: function( evaluator ) {
801 var last = this.$.lastChild,
802 retval = last && new CKEDITOR.dom.node( last );
803 if ( retval && evaluator && !evaluator( retval ) )
804 retval = retval.getPrevious( evaluator );
805
806 return retval;
807 },
808
809 /**
810 * Gets CSS style value.
811 *
812 * @param {String} name The CSS property name.
813 * @returns {String} Style value.
814 */
815 getStyle: function( name ) {
816 return this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ];
817 },
818
819 /**
820 * Checks if the element name matches the specified criteria.
821 *
822 * var element = new CKEDITOR.element( 'span' );
823 * alert( element.is( 'span' ) ); // true
824 * alert( element.is( 'p', 'span' ) ); // true
825 * alert( element.is( 'p' ) ); // false
826 * alert( element.is( 'p', 'div' ) ); // false
827 * alert( element.is( { p:1,span:1 } ) ); // true
828 *
829 * @param {String.../Object} name One or more names to be checked, or a {@link CKEDITOR.dtd} object.
830 * @returns {Boolean} `true` if the element name matches any of the names.
831 */
832 is: function() {
833 var name = this.getName();
834
835 // Check against the specified DTD liternal.
836 if ( typeof arguments[ 0 ] == 'object' )
837 return !!arguments[ 0 ][ name ];
838
839 // Check for tag names
840 for ( var i = 0; i < arguments.length; i++ ) {
841 if ( arguments[ i ] == name )
842 return true;
843 }
844 return false;
845 },
846
847 /**
848 * Decide whether one element is able to receive cursor.
849 *
850 * @param {Boolean} [textCursor=true] Only consider element that could receive text child.
851 */
852 isEditable: function( textCursor ) {
853 var name = this.getName();
854
855 if ( this.isReadOnly() || this.getComputedStyle( 'display' ) == 'none' ||
856 this.getComputedStyle( 'visibility' ) == 'hidden' ||
857 CKEDITOR.dtd.$nonEditable[ name ] ||
858 CKEDITOR.dtd.$empty[ name ] ||
859 ( this.is( 'a' ) &&
860 ( this.data( 'cke-saved-name' ) || this.hasAttribute( 'name' ) ) &&
861 !this.getChildCount()
862 ) ) {
863 return false;
864 }
865
866 if ( textCursor !== false ) {
867 // Get the element DTD (defaults to span for unknown elements).
868 var dtd = CKEDITOR.dtd[ name ] || CKEDITOR.dtd.span;
869 // In the DTD # == text node.
870 return !!( dtd && dtd[ '#' ] );
871 }
872
873 return true;
874 },
875
876 /**
877 * Compare this element's inner html, tag name, attributes, etc. with other one.
878 *
879 * See [W3C's DOM Level 3 spec - node#isEqualNode](http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-isEqualNode)
880 * for more details.
881 *
882 * @param {CKEDITOR.dom.element} otherElement Element to compare.
883 * @returns {Boolean}
884 */
885 isIdentical: function( otherElement ) {
886 // do shallow clones, but with IDs
887 var thisEl = this.clone( 0, 1 ),
888 otherEl = otherElement.clone( 0, 1 );
889
890 // Remove distractions.
891 thisEl.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] );
892 otherEl.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] );
893
894 // Native comparison available.
895 if ( thisEl.$.isEqualNode ) {
896 // Styles order matters.
897 thisEl.$.style.cssText = CKEDITOR.tools.normalizeCssText( thisEl.$.style.cssText );
898 otherEl.$.style.cssText = CKEDITOR.tools.normalizeCssText( otherEl.$.style.cssText );
899 return thisEl.$.isEqualNode( otherEl.$ );
900 } else {
901 thisEl = thisEl.getOuterHtml();
902 otherEl = otherEl.getOuterHtml();
903
904 // Fix tiny difference between link href in older IEs.
905 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && this.is( 'a' ) ) {
906 var parent = this.getParent();
907 if ( parent.type == CKEDITOR.NODE_ELEMENT ) {
908 var el = parent.clone();
909 el.setHtml( thisEl ), thisEl = el.getHtml();
910 el.setHtml( otherEl ), otherEl = el.getHtml();
911 }
912 }
913
914 return thisEl == otherEl;
915 }
916 },
917
918 /**
919 * Checks if this element is visible. May not work if the element is
920 * child of an element with visibility set to `hidden`, but works well
921 * on the great majority of cases.
922 *
923 * @returns {Boolean} True if the element is visible.
924 */
925 isVisible: function() {
926 var isVisible = ( this.$.offsetHeight || this.$.offsetWidth ) && this.getComputedStyle( 'visibility' ) != 'hidden',
927 elementWindow, elementWindowFrame;
928
929 // Webkit and Opera report non-zero offsetHeight despite that
930 // element is inside an invisible iframe. (#4542)
931 if ( isVisible && CKEDITOR.env.webkit ) {
932 elementWindow = this.getWindow();
933
934 if ( !elementWindow.equals( CKEDITOR.document.getWindow() ) && ( elementWindowFrame = elementWindow.$.frameElement ) )
935 isVisible = new CKEDITOR.dom.element( elementWindowFrame ).isVisible();
936
937 }
938
939 return !!isVisible;
940 },
941
942 /**
943 * Whether it's an empty inline elements which has no visual impact when removed.
944 *
945 * @returns {Boolean}
946 */
947 isEmptyInlineRemoveable: function() {
948 if ( !CKEDITOR.dtd.$removeEmpty[ this.getName() ] )
949 return false;
950
951 var children = this.getChildren();
952 for ( var i = 0, count = children.count(); i < count; i++ ) {
953 var child = children.getItem( i );
954
955 if ( child.type == CKEDITOR.NODE_ELEMENT && child.data( 'cke-bookmark' ) )
956 continue;
957
958 if ( child.type == CKEDITOR.NODE_ELEMENT && !child.isEmptyInlineRemoveable() || child.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( child.getText() ) )
959 return false;
960
961 }
962 return true;
963 },
964
965 /**
966 * Checks if the element has any defined attributes.
967 *
968 * var element = CKEDITOR.dom.element.createFromHtml( '<div title="Test">Example</div>' );
969 * alert( element.hasAttributes() ); // true
970 *
971 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Example</div>' );
972 * alert( element.hasAttributes() ); // false
973 *
974 * @method
975 * @returns {Boolean} True if the element has attributes.
976 */
977 hasAttributes: CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ?
978 function() {
979 var attributes = this.$.attributes;
980
981 for ( var i = 0; i < attributes.length; i++ ) {
982 var attribute = attributes[ i ];
983
984 switch ( attribute.nodeName ) {
985 case 'class':
986 // IE has a strange bug. If calling removeAttribute('className'),
987 // the attributes collection will still contain the "class"
988 // attribute, which will be marked as "specified", even if the
989 // outerHTML of the element is not displaying the class attribute.
990 // Note : I was not able to reproduce it outside the editor,
991 // but I've faced it while working on the TC of #1391.
992 if ( this.getAttribute( 'class' ) ) {
993 return true;
994 }
995
996 // Attributes to be ignored.
997 /* falls through */
998 case 'data-cke-expando':
999 continue;
1000
1001
1002 /* falls through */
1003 default:
1004 if ( attribute.specified ) {
1005 return true;
1006 }
1007 }
1008 }
1009
1010 return false;
1011 } : function() {
1012 var attrs = this.$.attributes,
1013 attrsNum = attrs.length;
1014
1015 // The _moz_dirty attribute might get into the element after pasting (#5455)
1016 var execludeAttrs = { 'data-cke-expando': 1, _moz_dirty: 1 };
1017
1018 return attrsNum > 0 && ( attrsNum > 2 || !execludeAttrs[ attrs[ 0 ].nodeName ] || ( attrsNum == 2 && !execludeAttrs[ attrs[ 1 ].nodeName ] ) );
1019 },
1020
1021 /**
1022 * Checks if the specified attribute is defined for this element.
1023 *
1024 * @method
1025 * @param {String} name The attribute name.
1026 * @returns {Boolean} `true` if the specified attribute is defined.
1027 */
1028 hasAttribute: ( function() {
1029 function ieHasAttribute( name ) {
1030 var $attr = this.$.attributes.getNamedItem( name );
1031
1032 if ( this.getName() == 'input' ) {
1033 switch ( name ) {
1034 case 'class':
1035 return this.$.className.length > 0;
1036 case 'checked':
1037 return !!this.$.checked;
1038 case 'value':
1039 var type = this.getAttribute( 'type' );
1040 return type == 'checkbox' || type == 'radio' ? this.$.value != 'on' : !!this.$.value;
1041 }
1042 }
1043
1044 if ( !$attr )
1045 return false;
1046
1047 return $attr.specified;
1048 }
1049
1050 if ( CKEDITOR.env.ie ) {
1051 if ( CKEDITOR.env.version < 8 ) {
1052 return function( name ) {
1053 // On IE < 8 the name attribute cannot be retrieved
1054 // right after the element creation and setting the
1055 // name with setAttribute.
1056 if ( name == 'name' )
1057 return !!this.$.name;
1058
1059 return ieHasAttribute.call( this, name );
1060 };
1061 } else {
1062 return ieHasAttribute;
1063 }
1064 } else {
1065 return function( name ) {
1066 // On other browsers specified property is deprecated and return always true,
1067 // but fortunately $.attributes contains only specified attributes.
1068 return !!this.$.attributes.getNamedItem( name );
1069 };
1070 }
1071 } )(),
1072
1073 /**
1074 * Hides this element (sets `display: none`).
1075 *
1076 * var element = CKEDITOR.document.getById( 'myElement' );
1077 * element.hide();
1078 */
1079 hide: function() {
1080 this.setStyle( 'display', 'none' );
1081 },
1082
1083 /**
1084 * Moves this element's children to the target element.
1085 *
1086 * @param {CKEDITOR.dom.element} target
1087 * @param {Boolean} [toStart=false] Insert moved children at the
1088 * beginning of the target element.
1089 */
1090 moveChildren: function( target, toStart ) {
1091 var $ = this.$;
1092 target = target.$;
1093
1094 if ( $ == target )
1095 return;
1096
1097 var child;
1098
1099 if ( toStart ) {
1100 while ( ( child = $.lastChild ) )
1101 target.insertBefore( $.removeChild( child ), target.firstChild );
1102 } else {
1103 while ( ( child = $.firstChild ) )
1104 target.appendChild( $.removeChild( child ) );
1105 }
1106 },
1107
1108 /**
1109 * Merges sibling elements that are identical to this one.
1110 *
1111 * Identical child elements are also merged. For example:
1112 *
1113 * <b><i></i></b><b><i></i></b> => <b><i></i></b>
1114 *
1115 * @method
1116 * @param {Boolean} [inlineOnly=true] Allow only inline elements to be merged.
1117 */
1118 mergeSiblings: ( function() {
1119 function mergeElements( element, sibling, isNext ) {
1120 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) {
1121 // Jumping over bookmark nodes and empty inline elements, e.g. <b><i></i></b>,
1122 // queuing them to be moved later. (#5567)
1123 var pendingNodes = [];
1124
1125 while ( sibling.data( 'cke-bookmark' ) || sibling.isEmptyInlineRemoveable() ) {
1126 pendingNodes.push( sibling );
1127 sibling = isNext ? sibling.getNext() : sibling.getPrevious();
1128 if ( !sibling || sibling.type != CKEDITOR.NODE_ELEMENT )
1129 return;
1130 }
1131
1132 if ( element.isIdentical( sibling ) ) {
1133 // Save the last child to be checked too, to merge things like
1134 // <b><i></i></b><b><i></i></b> => <b><i></i></b>
1135 var innerSibling = isNext ? element.getLast() : element.getFirst();
1136
1137 // Move pending nodes first into the target element.
1138 while ( pendingNodes.length )
1139 pendingNodes.shift().move( element, !isNext );
1140
1141 sibling.moveChildren( element, !isNext );
1142 sibling.remove();
1143
1144 // Now check the last inner child (see two comments above).
1145 if ( innerSibling && innerSibling.type == CKEDITOR.NODE_ELEMENT )
1146 innerSibling.mergeSiblings();
1147 }
1148 }
1149 }
1150
1151 return function( inlineOnly ) {
1152 // Merge empty links and anchors also. (#5567)
1153 if ( !( inlineOnly === false || CKEDITOR.dtd.$removeEmpty[ this.getName() ] || this.is( 'a' ) ) ) {
1154 return;
1155 }
1156
1157 mergeElements( this, this.getNext(), true );
1158 mergeElements( this, this.getPrevious() );
1159 };
1160 } )(),
1161
1162 /**
1163 * Shows this element (displays it).
1164 *
1165 * var element = CKEDITOR.document.getById( 'myElement' );
1166 * element.show();
1167 */
1168 show: function() {
1169 this.setStyles( {
1170 display: '',
1171 visibility: ''
1172 } );
1173 },
1174
1175 /**
1176 * Sets the value of an element attribute.
1177 *
1178 * var element = CKEDITOR.document.getById( 'myElement' );
1179 * element.setAttribute( 'class', 'myClass' );
1180 * element.setAttribute( 'title', 'This is an example' );
1181 *
1182 * @method
1183 * @param {String} name The name of the attribute.
1184 * @param {String} value The value to be set to the attribute.
1185 * @returns {CKEDITOR.dom.element} This element instance.
1186 */
1187 setAttribute: ( function() {
1188 var standard = function( name, value ) {
1189 this.$.setAttribute( name, value );
1190 return this;
1191 };
1192
1193 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
1194 return function( name, value ) {
1195 if ( name == 'class' )
1196 this.$.className = value;
1197 else if ( name == 'style' )
1198 this.$.style.cssText = value;
1199 else if ( name == 'tabindex' ) // Case sensitive.
1200 this.$.tabIndex = value;
1201 else if ( name == 'checked' )
1202 this.$.checked = value;
1203 else if ( name == 'contenteditable' )
1204 standard.call( this, 'contentEditable', value );
1205 else
1206 standard.apply( this, arguments );
1207 return this;
1208 };
1209 } else if ( CKEDITOR.env.ie8Compat && CKEDITOR.env.secure ) {
1210 return function( name, value ) {
1211 // IE8 throws error when setting src attribute to non-ssl value. (#7847)
1212 if ( name == 'src' && value.match( /^http:\/\// ) ) {
1213 try {
1214 standard.apply( this, arguments );
1215 } catch ( e ) {}
1216 } else {
1217 standard.apply( this, arguments );
1218 }
1219 return this;
1220 };
1221 } else {
1222 return standard;
1223 }
1224 } )(),
1225
1226 /**
1227 * Sets the value of several element attributes.
1228 *
1229 * var element = CKEDITOR.document.getById( 'myElement' );
1230 * element.setAttributes( {
1231 * 'class': 'myClass',
1232 * title: 'This is an example'
1233 * } );
1234 *
1235 * @chainable
1236 * @param {Object} attributesPairs An object containing the names and
1237 * values of the attributes.
1238 * @returns {CKEDITOR.dom.element} This element instance.
1239 */
1240 setAttributes: function( attributesPairs ) {
1241 for ( var name in attributesPairs )
1242 this.setAttribute( name, attributesPairs[ name ] );
1243 return this;
1244 },
1245
1246 /**
1247 * Sets the element value. This function is usually used with form
1248 * field element.
1249 *
1250 * @chainable
1251 * @param {String} value The element value.
1252 * @returns {CKEDITOR.dom.element} This element instance.
1253 */
1254 setValue: function( value ) {
1255 this.$.value = value;
1256 return this;
1257 },
1258
1259 /**
1260 * Removes an attribute from the element.
1261 *
1262 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="classA"></div>' );
1263 * element.removeAttribute( 'class' );
1264 *
1265 * @method
1266 * @param {String} name The attribute name.
1267 */
1268 removeAttribute: ( function() {
1269 var standard = function( name ) {
1270 this.$.removeAttribute( name );
1271 };
1272
1273 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
1274 return function( name ) {
1275 if ( name == 'class' )
1276 name = 'className';
1277 else if ( name == 'tabindex' )
1278 name = 'tabIndex';
1279 else if ( name == 'contenteditable' )
1280 name = 'contentEditable';
1281 standard.call( this, name );
1282 };
1283 } else {
1284 return standard;
1285 }
1286 } )(),
1287
1288 /**
1289 * Removes all element's attributes or just given ones.
1290 *
1291 * @param {Array} [attributes] The array with attributes names.
1292 */
1293 removeAttributes: function( attributes ) {
1294 if ( CKEDITOR.tools.isArray( attributes ) ) {
1295 for ( var i = 0; i < attributes.length; i++ )
1296 this.removeAttribute( attributes[ i ] );
1297 } else {
1298 for ( var attr in attributes )
1299 attributes.hasOwnProperty( attr ) && this.removeAttribute( attr );
1300 }
1301 },
1302
1303 /**
1304 * Removes a style from the element.
1305 *
1306 * var element = CKEDITOR.dom.element.createFromHtml( '<div style="display:none"></div>' );
1307 * element.removeStyle( 'display' );
1308 *
1309 * @method
1310 * @param {String} name The style name.
1311 */
1312 removeStyle: function( name ) {
1313 // Removes the specified property from the current style object.
1314 var $ = this.$.style;
1315
1316 // "removeProperty" need to be specific on the following styles.
1317 if ( !$.removeProperty && ( name == 'border' || name == 'margin' || name == 'padding' ) ) {
1318 var names = expandedRules( name );
1319 for ( var i = 0 ; i < names.length ; i++ )
1320 this.removeStyle( names[ i ] );
1321 return;
1322 }
1323
1324 $.removeProperty ? $.removeProperty( name ) : $.removeAttribute( CKEDITOR.tools.cssStyleToDomStyle( name ) );
1325
1326 // Eventually remove empty style attribute.
1327 if ( !this.$.style.cssText )
1328 this.removeAttribute( 'style' );
1329 },
1330
1331 /**
1332 * Sets the value of an element style.
1333 *
1334 * var element = CKEDITOR.document.getById( 'myElement' );
1335 * element.setStyle( 'background-color', '#ff0000' );
1336 * element.setStyle( 'margin-top', '10px' );
1337 * element.setStyle( 'float', 'right' );
1338 *
1339 * @param {String} name The name of the style. The CSS naming notation
1340 * must be used (e.g. `background-color`).
1341 * @param {String} value The value to be set to the style.
1342 * @returns {CKEDITOR.dom.element} This element instance.
1343 */
1344 setStyle: function( name, value ) {
1345 this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ] = value;
1346 return this;
1347 },
1348
1349 /**
1350 * Sets the value of several element styles.
1351 *
1352 * var element = CKEDITOR.document.getById( 'myElement' );
1353 * element.setStyles( {
1354 * position: 'absolute',
1355 * float: 'right'
1356 * } );
1357 *
1358 * @param {Object} stylesPairs An object containing the names and
1359 * values of the styles.
1360 * @returns {CKEDITOR.dom.element} This element instance.
1361 */
1362 setStyles: function( stylesPairs ) {
1363 for ( var name in stylesPairs )
1364 this.setStyle( name, stylesPairs[ name ] );
1365 return this;
1366 },
1367
1368 /**
1369 * Sets the opacity of an element.
1370 *
1371 * var element = CKEDITOR.document.getById( 'myElement' );
1372 * element.setOpacity( 0.75 );
1373 *
1374 * @param {Number} opacity A number within the range `[0.0, 1.0]`.
1375 */
1376 setOpacity: function( opacity ) {
1377 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
1378 opacity = Math.round( opacity * 100 );
1379 this.setStyle( 'filter', opacity >= 100 ? '' : 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + opacity + ')' );
1380 } else {
1381 this.setStyle( 'opacity', opacity );
1382 }
1383 },
1384
1385 /**
1386 * Makes the element and its children unselectable.
1387 *
1388 * var element = CKEDITOR.document.getById( 'myElement' );
1389 * element.unselectable();
1390 *
1391 * @method
1392 */
1393 unselectable: function() {
1394 // CSS unselectable.
1395 this.setStyles( CKEDITOR.tools.cssVendorPrefix( 'user-select', 'none' ) );
1396
1397 // For IE/Opera which doesn't support for the above CSS style,
1398 // the unselectable="on" attribute only specifies the selection
1399 // process cannot start in the element itself, and it doesn't inherit.
1400 if ( CKEDITOR.env.ie ) {
1401 this.setAttribute( 'unselectable', 'on' );
1402
1403 var element,
1404 elements = this.getElementsByTag( '*' );
1405
1406 for ( var i = 0, count = elements.count() ; i < count ; i++ ) {
1407 element = elements.getItem( i );
1408 element.setAttribute( 'unselectable', 'on' );
1409 }
1410 }
1411 },
1412
1413 /**
1414 * Gets closest positioned (`position != static`) ancestor.
1415 *
1416 * @returns {CKEDITOR.dom.element} Positioned ancestor or `null`.
1417 */
1418 getPositionedAncestor: function() {
1419 var current = this;
1420 while ( current.getName() != 'html' ) {
1421 if ( current.getComputedStyle( 'position' ) != 'static' )
1422 return current;
1423
1424 current = current.getParent();
1425 }
1426 return null;
1427 },
1428
1429 /**
1430 * Gets this element's position in document.
1431 *
1432 * @param {CKEDITOR.dom.document} [refDocument]
1433 * @returns {Object} Element's position.
1434 * @returns {Number} return.x
1435 * @returns {Number} return.y
1436 * @todo refDocument
1437 */
1438 getDocumentPosition: function( refDocument ) {
1439 var x = 0,
1440 y = 0,
1441 doc = this.getDocument(),
1442 body = doc.getBody(),
1443 quirks = doc.$.compatMode == 'BackCompat';
1444
1445 if ( document.documentElement.getBoundingClientRect ) {
1446 var box = this.$.getBoundingClientRect(),
1447 $doc = doc.$,
1448 $docElem = $doc.documentElement;
1449
1450 var clientTop = $docElem.clientTop || body.$.clientTop || 0,
1451 clientLeft = $docElem.clientLeft || body.$.clientLeft || 0,
1452 needAdjustScrollAndBorders = true;
1453
1454 // #3804: getBoundingClientRect() works differently on IE and non-IE
1455 // browsers, regarding scroll positions.
1456 //
1457 // On IE, the top position of the <html> element is always 0, no matter
1458 // how much you scrolled down.
1459 //
1460 // On other browsers, the top position of the <html> element is negative
1461 // scrollTop.
1462 if ( CKEDITOR.env.ie ) {
1463 var inDocElem = doc.getDocumentElement().contains( this ),
1464 inBody = doc.getBody().contains( this );
1465
1466 needAdjustScrollAndBorders = ( quirks && inBody ) || ( !quirks && inDocElem );
1467 }
1468
1469 // #12747.
1470 if ( needAdjustScrollAndBorders ) {
1471 var scrollRelativeLeft,
1472 scrollRelativeTop;
1473
1474 // See #12758 to know more about document.(documentElement|body).scroll(Left|Top) in Webkit.
1475 if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version >= 12 ) ) {
1476 scrollRelativeLeft = body.$.scrollLeft || $docElem.scrollLeft;
1477 scrollRelativeTop = body.$.scrollTop || $docElem.scrollTop;
1478 } else {
1479 var scrollRelativeElement = quirks ? body.$ : $docElem;
1480
1481 scrollRelativeLeft = scrollRelativeElement.scrollLeft;
1482 scrollRelativeTop = scrollRelativeElement.scrollTop;
1483 }
1484
1485 x = box.left + scrollRelativeLeft - clientLeft;
1486 y = box.top + scrollRelativeTop - clientTop;
1487 }
1488 } else {
1489 var current = this,
1490 previous = null,
1491 offsetParent;
1492 while ( current && !( current.getName() == 'body' || current.getName() == 'html' ) ) {
1493 x += current.$.offsetLeft - current.$.scrollLeft;
1494 y += current.$.offsetTop - current.$.scrollTop;
1495
1496 // Opera includes clientTop|Left into offsetTop|Left.
1497 if ( !current.equals( this ) ) {
1498 x += ( current.$.clientLeft || 0 );
1499 y += ( current.$.clientTop || 0 );
1500 }
1501
1502 var scrollElement = previous;
1503 while ( scrollElement && !scrollElement.equals( current ) ) {
1504 x -= scrollElement.$.scrollLeft;
1505 y -= scrollElement.$.scrollTop;
1506 scrollElement = scrollElement.getParent();
1507 }
1508
1509 previous = current;
1510 current = ( offsetParent = current.$.offsetParent ) ? new CKEDITOR.dom.element( offsetParent ) : null;
1511 }
1512 }
1513
1514 if ( refDocument ) {
1515 var currentWindow = this.getWindow(),
1516 refWindow = refDocument.getWindow();
1517
1518 if ( !currentWindow.equals( refWindow ) && currentWindow.$.frameElement ) {
1519 var iframePosition = ( new CKEDITOR.dom.element( currentWindow.$.frameElement ) ).getDocumentPosition( refDocument );
1520
1521 x += iframePosition.x;
1522 y += iframePosition.y;
1523 }
1524 }
1525
1526 if ( !document.documentElement.getBoundingClientRect ) {
1527 // In Firefox, we'll endup one pixel before the element positions,
1528 // so we must add it here.
1529 if ( CKEDITOR.env.gecko && !quirks ) {
1530 x += this.$.clientLeft ? 1 : 0;
1531 y += this.$.clientTop ? 1 : 0;
1532 }
1533 }
1534
1535 return { x: x, y: y };
1536 },
1537
1538 /**
1539 * Make any page element visible inside the browser viewport.
1540 *
1541 * @param {Boolean} [alignToTop=false]
1542 */
1543 scrollIntoView: function( alignToTop ) {
1544 var parent = this.getParent();
1545 if ( !parent )
1546 return;
1547
1548 // Scroll the element into parent container from the inner out.
1549 do {
1550 // Check ancestors that overflows.
1551 var overflowed =
1552 parent.$.clientWidth && parent.$.clientWidth < parent.$.scrollWidth ||
1553 parent.$.clientHeight && parent.$.clientHeight < parent.$.scrollHeight;
1554
1555 // Skip body element, which will report wrong clientHeight when containing
1556 // floated content. (#9523)
1557 if ( overflowed && !parent.is( 'body' ) )
1558 this.scrollIntoParent( parent, alignToTop, 1 );
1559
1560 // Walk across the frame.
1561 if ( parent.is( 'html' ) ) {
1562 var win = parent.getWindow();
1563
1564 // Avoid security error.
1565 try {
1566 var iframe = win.$.frameElement;
1567 iframe && ( parent = new CKEDITOR.dom.element( iframe ) );
1568 } catch ( er ) {}
1569 }
1570 }
1571 while ( ( parent = parent.getParent() ) );
1572 },
1573
1574 /**
1575 * Make any page element visible inside one of the ancestors by scrolling the parent.
1576 *
1577 * @param {CKEDITOR.dom.element/CKEDITOR.dom.window} parent The container to scroll into.
1578 * @param {Boolean} [alignToTop] Align the element's top side with the container's
1579 * when `true` is specified; align the bottom with viewport bottom when
1580 * `false` is specified. Otherwise scroll on either side with the minimum
1581 * amount to show the element.
1582 * @param {Boolean} [hscroll] Whether horizontal overflow should be considered.
1583 */
1584 scrollIntoParent: function( parent, alignToTop, hscroll ) {
1585 !parent && ( parent = this.getWindow() );
1586
1587 var doc = parent.getDocument();
1588 var isQuirks = doc.$.compatMode == 'BackCompat';
1589
1590 // On window <html> is scrolled while quirks scrolls <body>.
1591 if ( parent instanceof CKEDITOR.dom.window )
1592 parent = isQuirks ? doc.getBody() : doc.getDocumentElement();
1593
1594 // Scroll the parent by the specified amount.
1595 function scrollBy( x, y ) {
1596 // Webkit doesn't support "scrollTop/scrollLeft"
1597 // on documentElement/body element.
1598 if ( /body|html/.test( parent.getName() ) )
1599 parent.getWindow().$.scrollBy( x, y );
1600 else {
1601 parent.$.scrollLeft += x;
1602 parent.$.scrollTop += y;
1603 }
1604 }
1605
1606 // Figure out the element position relative to the specified window.
1607 function screenPos( element, refWin ) {
1608 var pos = { x: 0, y: 0 };
1609
1610 if ( !( element.is( isQuirks ? 'body' : 'html' ) ) ) {
1611 var box = element.$.getBoundingClientRect();
1612 pos.x = box.left, pos.y = box.top;
1613 }
1614
1615 var win = element.getWindow();
1616 if ( !win.equals( refWin ) ) {
1617 var outerPos = screenPos( CKEDITOR.dom.element.get( win.$.frameElement ), refWin );
1618 pos.x += outerPos.x, pos.y += outerPos.y;
1619 }
1620
1621 return pos;
1622 }
1623
1624 // calculated margin size.
1625 function margin( element, side ) {
1626 return parseInt( element.getComputedStyle( 'margin-' + side ) || 0, 10 ) || 0;
1627 }
1628
1629 var win = parent.getWindow();
1630
1631 var thisPos = screenPos( this, win ),
1632 parentPos = screenPos( parent, win ),
1633 eh = this.$.offsetHeight,
1634 ew = this.$.offsetWidth,
1635 ch = parent.$.clientHeight,
1636 cw = parent.$.clientWidth,
1637 lt, br;
1638
1639 // Left-top margins.
1640 lt = {
1641 x: thisPos.x - margin( this, 'left' ) - parentPos.x || 0,
1642 y: thisPos.y - margin( this, 'top' ) - parentPos.y || 0
1643 };
1644
1645 // Bottom-right margins.
1646 br = {
1647 x: thisPos.x + ew + margin( this, 'right' ) - ( ( parentPos.x ) + cw ) || 0,
1648 y: thisPos.y + eh + margin( this, 'bottom' ) - ( ( parentPos.y ) + ch ) || 0
1649 };
1650
1651 // 1. Do the specified alignment as much as possible;
1652 // 2. Otherwise be smart to scroll only the minimum amount;
1653 // 3. Never cut at the top;
1654 // 4. DO NOT scroll when already visible.
1655 if ( lt.y < 0 || br.y > 0 )
1656 scrollBy( 0, alignToTop === true ? lt.y : alignToTop === false ? br.y : lt.y < 0 ? lt.y : br.y );
1657
1658 if ( hscroll && ( lt.x < 0 || br.x > 0 ) )
1659 scrollBy( lt.x < 0 ? lt.x : br.x, 0 );
1660 },
1661
1662 /**
1663 * Switch the `class` attribute to reflect one of the triple states of an
1664 * element in one of {@link CKEDITOR#TRISTATE_ON}, {@link CKEDITOR#TRISTATE_OFF}
1665 * or {@link CKEDITOR#TRISTATE_DISABLED}.
1666 *
1667 * link.setState( CKEDITOR.TRISTATE_ON );
1668 * // <a class="cke_on" aria-pressed="true">...</a>
1669 * link.setState( CKEDITOR.TRISTATE_OFF );
1670 * // <a class="cke_off">...</a>
1671 * link.setState( CKEDITOR.TRISTATE_DISABLED );
1672 * // <a class="cke_disabled" aria-disabled="true">...</a>
1673 *
1674 * span.setState( CKEDITOR.TRISTATE_ON, 'cke_button' );
1675 * // <span class="cke_button_on">...</span>
1676 *
1677 * @param {Number} state Indicate the element state. One of {@link CKEDITOR#TRISTATE_ON},
1678 * {@link CKEDITOR#TRISTATE_OFF}, {@link CKEDITOR#TRISTATE_DISABLED}.
1679 * @param [base='cke'] The prefix apply to each of the state class name.
1680 * @param [useAria=true] Whether toggle the ARIA state attributes besides of class name change.
1681 */
1682 setState: function( state, base, useAria ) {
1683 base = base || 'cke';
1684
1685 switch ( state ) {
1686 case CKEDITOR.TRISTATE_ON:
1687 this.addClass( base + '_on' );
1688 this.removeClass( base + '_off' );
1689 this.removeClass( base + '_disabled' );
1690 useAria && this.setAttribute( 'aria-pressed', true );
1691 useAria && this.removeAttribute( 'aria-disabled' );
1692 break;
1693
1694 case CKEDITOR.TRISTATE_DISABLED:
1695 this.addClass( base + '_disabled' );
1696 this.removeClass( base + '_off' );
1697 this.removeClass( base + '_on' );
1698 useAria && this.setAttribute( 'aria-disabled', true );
1699 useAria && this.removeAttribute( 'aria-pressed' );
1700 break;
1701
1702 default:
1703 this.addClass( base + '_off' );
1704 this.removeClass( base + '_on' );
1705 this.removeClass( base + '_disabled' );
1706 useAria && this.removeAttribute( 'aria-pressed' );
1707 useAria && this.removeAttribute( 'aria-disabled' );
1708 break;
1709 }
1710 },
1711
1712 /**
1713 * Returns the inner document of this `<iframe>` element.
1714 *
1715 * @returns {CKEDITOR.dom.document} The inner document.
1716 */
1717 getFrameDocument: function() {
1718 var $ = this.$;
1719
1720 try {
1721 // In IE, with custom document.domain, it may happen that
1722 // the iframe is not yet available, resulting in "Access
1723 // Denied" for the following property access.
1724 $.contentWindow.document;
1725 } catch ( e ) {
1726 // Trick to solve this issue, forcing the iframe to get ready
1727 // by simply setting its "src" property.
1728 $.src = $.src;
1729 }
1730
1731 return $ && new CKEDITOR.dom.document( $.contentWindow.document );
1732 },
1733
1734 /**
1735 * Copy all the attributes from one node to the other, kinda like a clone
1736 * skipAttributes is an object with the attributes that must **not** be copied.
1737 *
1738 * @param {CKEDITOR.dom.element} dest The destination element.
1739 * @param {Object} skipAttributes A dictionary of attributes to skip.
1740 */
1741 copyAttributes: function( dest, skipAttributes ) {
1742 var attributes = this.$.attributes;
1743 skipAttributes = skipAttributes || {};
1744
1745 for ( var n = 0; n < attributes.length; n++ ) {
1746 var attribute = attributes[ n ];
1747
1748 // Lowercase attribute name hard rule is broken for
1749 // some attribute on IE, e.g. CHECKED.
1750 var attrName = attribute.nodeName.toLowerCase(),
1751 attrValue;
1752
1753 // We can set the type only once, so do it with the proper value, not copying it.
1754 if ( attrName in skipAttributes )
1755 continue;
1756
1757 if ( attrName == 'checked' && ( attrValue = this.getAttribute( attrName ) ) )
1758 dest.setAttribute( attrName, attrValue );
1759 // IE contains not specified attributes in $.attributes so we need to check
1760 // if elements attribute is specified using hasAttribute.
1761 else if ( !CKEDITOR.env.ie || this.hasAttribute( attrName ) ) {
1762 attrValue = this.getAttribute( attrName );
1763 if ( attrValue === null )
1764 attrValue = attribute.nodeValue;
1765
1766 dest.setAttribute( attrName, attrValue );
1767 }
1768 }
1769
1770 // The style:
1771 if ( this.$.style.cssText !== '' )
1772 dest.$.style.cssText = this.$.style.cssText;
1773 },
1774
1775 /**
1776 * Changes the tag name of the current element.
1777 *
1778 * @param {String} newTag The new tag for the element.
1779 */
1780 renameNode: function( newTag ) {
1781 // If it's already correct exit here.
1782 if ( this.getName() == newTag )
1783 return;
1784
1785 var doc = this.getDocument();
1786
1787 // Create the new node.
1788 var newNode = new CKEDITOR.dom.element( newTag, doc );
1789
1790 // Copy all attributes.
1791 this.copyAttributes( newNode );
1792
1793 // Move children to the new node.
1794 this.moveChildren( newNode );
1795
1796 // Replace the node.
1797 this.getParent( true ) && this.$.parentNode.replaceChild( newNode.$, this.$ );
1798 newNode.$[ 'data-cke-expando' ] = this.$[ 'data-cke-expando' ];
1799 this.$ = newNode.$;
1800 // Bust getName's cache. (#8663)
1801 delete this.getName;
1802 },
1803
1804 /**
1805 * Gets a DOM tree descendant under the current node.
1806 *
1807 * var strong = p.getChild( 0 );
1808 *
1809 * @method
1810 * @param {Array/Number} indices The child index or array of child indices under the node.
1811 * @returns {CKEDITOR.dom.node} The specified DOM child under the current node. Null if child does not exist.
1812 */
1813 getChild: ( function() {
1814 function getChild( rawNode, index ) {
1815 var childNodes = rawNode.childNodes;
1816
1817 if ( index >= 0 && index < childNodes.length )
1818 return childNodes[ index ];
1819 }
1820
1821 return function( indices ) {
1822 var rawNode = this.$;
1823
1824 if ( !indices.slice )
1825 rawNode = getChild( rawNode, indices );
1826 else {
1827 indices = indices.slice();
1828 while ( indices.length > 0 && rawNode )
1829 rawNode = getChild( rawNode, indices.shift() );
1830 }
1831
1832 return rawNode ? new CKEDITOR.dom.node( rawNode ) : null;
1833 };
1834 } )(),
1835
1836 /**
1837 * Gets number of element's children.
1838 *
1839 * @returns {Number}
1840 */
1841 getChildCount: function() {
1842 return this.$.childNodes.length;
1843 },
1844
1845 /**
1846 * Disables browser's context menu in this element.
1847 */
1848 disableContextMenu: function() {
1849 this.on( 'contextmenu', function( evt ) {
1850 // Cancel the browser context menu.
1851 if ( !evt.data.getTarget().getAscendant( enablesContextMenu, true ) )
1852 evt.data.preventDefault();
1853 } );
1854
1855 function enablesContextMenu( node ) {
1856 return node.type == CKEDITOR.NODE_ELEMENT && node.hasClass( 'cke_enable_context_menu' );
1857 }
1858 },
1859
1860 /**
1861 * Gets element's direction. Supports both CSS `direction` prop and `dir` attr.
1862 */
1863 getDirection: function( useComputed ) {
1864 if ( useComputed ) {
1865 return this.getComputedStyle( 'direction' ) ||
1866 this.getDirection() ||
1867 this.getParent() && this.getParent().getDirection( 1 ) ||
1868 this.getDocument().$.dir ||
1869 'ltr';
1870 }
1871 else {
1872 return this.getStyle( 'direction' ) || this.getAttribute( 'dir' );
1873 }
1874 },
1875
1876 /**
1877 * Gets, sets and removes custom data to be stored as HTML5 data-* attributes.
1878 *
1879 * element.data( 'extra-info', 'test' ); // Appended the attribute data-extra-info="test" to the element.
1880 * alert( element.data( 'extra-info' ) ); // 'test'
1881 * element.data( 'extra-info', false ); // Remove the data-extra-info attribute from the element.
1882 *
1883 * @param {String} name The name of the attribute, excluding the `data-` part.
1884 * @param {String} [value] The value to set. If set to false, the attribute will be removed.
1885 */
1886 data: function( name, value ) {
1887 name = 'data-' + name;
1888 if ( value === undefined )
1889 return this.getAttribute( name );
1890 else if ( value === false )
1891 this.removeAttribute( name );
1892 else
1893 this.setAttribute( name, value );
1894
1895 return null;
1896 },
1897
1898 /**
1899 * Retrieves an editor instance which is based on this element (if any).
1900 * It basically loops over {@link CKEDITOR#instances} in search for an instance
1901 * that uses the element.
1902 *
1903 * var element = new CKEDITOR.dom.element( 'div' );
1904 * element.appendTo( CKEDITOR.document.getBody() );
1905 * CKEDITOR.replace( element );
1906 * alert( element.getEditor().name ); // 'editor1'
1907 *
1908 * @returns {CKEDITOR.editor} An editor instance or null if nothing has been found.
1909 */
1910 getEditor: function() {
1911 var instances = CKEDITOR.instances,
1912 name, instance;
1913
1914 for ( name in instances ) {
1915 instance = instances[ name ];
1916
1917 if ( instance.element.equals( this ) && instance.elementMode != CKEDITOR.ELEMENT_MODE_APPENDTO )
1918 return instance;
1919 }
1920
1921 return null;
1922 },
1923
1924 /**
1925 * Returns list of elements within this element that match specified `selector`.
1926 *
1927 * **Notes:**
1928 *
1929 * * Not available in IE7.
1930 * * Returned list is not a live collection (like a result of native `querySelectorAll`).
1931 * * Unlike native `querySelectorAll` this method ensures selector contextualization. This is:
1932 *
1933 * HTML: '<body><div><i>foo</i></div></body>'
1934 * Native: div.querySelectorAll( 'body i' ) // -> [ <i>foo</i> ]
1935 * Method: div.find( 'body i' ) // -> []
1936 * div.find( 'i' ) // -> [ <i>foo</i> ]
1937 *
1938 * @since 4.3
1939 * @param {String} selector
1940 * @returns {CKEDITOR.dom.nodeList}
1941 */
1942 find: function( selector ) {
1943 var removeTmpId = createTmpId( this ),
1944 list = new CKEDITOR.dom.nodeList(
1945 this.$.querySelectorAll( getContextualizedSelector( this, selector ) )
1946 );
1947
1948 removeTmpId();
1949
1950 return list;
1951 },
1952
1953 /**
1954 * Returns first element within this element that matches specified `selector`.
1955 *
1956 * **Notes:**
1957 *
1958 * * Not available in IE7.
1959 * * Unlike native `querySelectorAll` this method ensures selector contextualization. This is:
1960 *
1961 * HTML: '<body><div><i>foo</i></div></body>'
1962 * Native: div.querySelector( 'body i' ) // -> <i>foo</i>
1963 * Method: div.findOne( 'body i' ) // -> null
1964 * div.findOne( 'i' ) // -> <i>foo</i>
1965 *
1966 * @since 4.3
1967 * @param {String} selector
1968 * @returns {CKEDITOR.dom.element}
1969 */
1970 findOne: function( selector ) {
1971 var removeTmpId = createTmpId( this ),
1972 found = this.$.querySelector( getContextualizedSelector( this, selector ) );
1973
1974 removeTmpId();
1975
1976 return found ? new CKEDITOR.dom.element( found ) : null;
1977 },
1978
1979 /**
1980 * Traverse the DOM of this element (inclusive), executing a callback for
1981 * each node.
1982 *
1983 * var element = CKEDITOR.dom.element.createFromHtml( '<div><p>foo<b>bar</b>bom</p></div>' );
1984 * element.forEach( function( node ) {
1985 * console.log( node );
1986 * } );
1987 * // Will log:
1988 * // 1. <div> element,
1989 * // 2. <p> element,
1990 * // 3. "foo" text node,
1991 * // 4. <b> element,
1992 * // 5. "bar" text node,
1993 * // 6. "bom" text node.
1994 *
1995 * @since 4.3
1996 * @param {Function} callback Function to be executed on every node.
1997 * If `callback` returns `false` descendants of the node will be ignored.
1998 * @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument.
1999 * @param {Number} [type] If specified `callback` will be executed only on
2000 * nodes of this type.
2001 * @param {Boolean} [skipRoot] Don't execute `callback` on this element.
2002 */
2003 forEach: function( callback, type, skipRoot ) {
2004 if ( !skipRoot && ( !type || this.type == type ) )
2005 var ret = callback( this );
2006
2007 // Do not filter children if callback returned false.
2008 if ( ret === false )
2009 return;
2010
2011 var children = this.getChildren(),
2012 node,
2013 i = 0;
2014
2015 // We do not cache the size, because the live list of nodes may be changed by the callback.
2016 for ( ; i < children.count(); i++ ) {
2017 node = children.getItem( i );
2018 if ( node.type == CKEDITOR.NODE_ELEMENT )
2019 node.forEach( callback, type );
2020 else if ( !type || node.type == type )
2021 callback( node );
2022 }
2023 }
2024 } );
2025
2026 function createTmpId( element ) {
2027 var hadId = true;
2028
2029 if ( !element.$.id ) {
2030 element.$.id = 'cke_tmp_' + CKEDITOR.tools.getNextNumber();
2031 hadId = false;
2032 }
2033
2034 return function() {
2035 if ( !hadId )
2036 element.removeAttribute( 'id' );
2037 };
2038 }
2039
2040 function getContextualizedSelector( element, selector ) {
2041 return '#' + element.$.id + ' ' + selector.split( /,\s*/ ).join( ', #' + element.$.id + ' ' );
2042 }
2043
2044 var sides = {
2045 width: [ 'border-left-width', 'border-right-width', 'padding-left', 'padding-right' ],
2046 height: [ 'border-top-width', 'border-bottom-width', 'padding-top', 'padding-bottom' ]
2047 };
2048
2049 // Generate list of specific style rules, applicable to margin/padding/border.
2050 function expandedRules( style ) {
2051 var sides = [ 'top', 'left', 'right', 'bottom' ], components;
2052
2053 if ( style == 'border' )
2054 components = [ 'color', 'style', 'width' ];
2055
2056 var styles = [];
2057 for ( var i = 0 ; i < sides.length ; i++ ) {
2058
2059 if ( components ) {
2060 for ( var j = 0 ; j < components.length ; j++ )
2061 styles.push( [ style, sides[ i ], components[ j ] ].join( '-' ) );
2062 } else {
2063 styles.push( [ style, sides[ i ] ].join( '-' ) );
2064 }
2065 }
2066
2067 return styles;
2068 }
2069
2070 function marginAndPaddingSize( type ) {
2071 var adjustment = 0;
2072 for ( var i = 0, len = sides[ type ].length; i < len; i++ )
2073 adjustment += parseInt( this.getComputedStyle( sides[ type ][ i ] ) || 0, 10 ) || 0;
2074 return adjustment;
2075 }
2076
2077 /**
2078 * Sets the element size considering the box model.
2079 *
2080 * @param {'width'/'height'} type The dimension to set.
2081 * @param {Number} size The length unit in px.
2082 * @param {Boolean} isBorderBox Apply the size based on the border box model.
2083 */
2084 CKEDITOR.dom.element.prototype.setSize = function( type, size, isBorderBox ) {
2085 if ( typeof size == 'number' ) {
2086 if ( isBorderBox && !( CKEDITOR.env.ie && CKEDITOR.env.quirks ) )
2087 size -= marginAndPaddingSize.call( this, type );
2088
2089 this.setStyle( type, size + 'px' );
2090 }
2091 };
2092
2093 /**
2094 * Gets the element size, possibly considering the box model.
2095 *
2096 * @param {'width'/'height'} type The dimension to get.
2097 * @param {Boolean} isBorderBox Get the size based on the border box model.
2098 */
2099 CKEDITOR.dom.element.prototype.getSize = function( type, isBorderBox ) {
2100 var size = Math.max( this.$[ 'offset' + CKEDITOR.tools.capitalize( type ) ], this.$[ 'client' + CKEDITOR.tools.capitalize( type ) ] ) || 0;
2101
2102 if ( isBorderBox )
2103 size -= marginAndPaddingSize.call( this, type );
2104
2105 return size;
2106 };
2107} )();
diff --git a/sources/core/dom/elementpath.js b/sources/core/dom/elementpath.js
new file mode 100644
index 00000000..55b776d5
--- /dev/null
+++ b/sources/core/dom/elementpath.js
@@ -0,0 +1,251 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8( function() {
9
10 var pathBlockLimitElements = {},
11 pathBlockElements = {},
12 tag;
13
14 // Elements that are considered the "Block limit" in an element path.
15 for ( tag in CKEDITOR.dtd.$blockLimit ) {
16 // Exclude from list roots.
17 if ( !( tag in CKEDITOR.dtd.$list ) )
18 pathBlockLimitElements[ tag ] = 1;
19 }
20
21 // Elements that are considered the "End level Block" in an element path.
22 for ( tag in CKEDITOR.dtd.$block ) {
23 // Exclude block limits, and empty block element, e.g. hr.
24 if ( !( tag in CKEDITOR.dtd.$blockLimit || tag in CKEDITOR.dtd.$empty ) )
25 pathBlockElements[ tag ] = 1;
26 }
27
28 // Check if an element contains any block element.
29 function checkHasBlock( element ) {
30 var childNodes = element.getChildren();
31
32 for ( var i = 0, count = childNodes.count(); i < count; i++ ) {
33 var child = childNodes.getItem( i );
34
35 if ( child.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$block[ child.getName() ] )
36 return true;
37 }
38
39 return false;
40 }
41
42 /**
43 * Retrieve the list of nodes walked from the start node up to the editable element of the editor.
44 *
45 * @class
46 * @constructor Creates an element path class instance.
47 * @param {CKEDITOR.dom.element} startNode From which the path should start.
48 * @param {CKEDITOR.dom.element} root To which element the path should stop, defaults to the `body` element.
49 */
50 CKEDITOR.dom.elementPath = function( startNode, root ) {
51 var block = null,
52 blockLimit = null,
53 elements = [],
54 e = startNode,
55 elementName;
56
57 // Backward compact.
58 root = root || startNode.getDocument().getBody();
59
60 do {
61 if ( e.type == CKEDITOR.NODE_ELEMENT ) {
62 elements.push( e );
63
64 if ( !this.lastElement ) {
65 this.lastElement = e;
66
67 // If an object or non-editable element is fully selected at the end of the element path,
68 // it must not become the block limit.
69 if ( e.is( CKEDITOR.dtd.$object ) || e.getAttribute( 'contenteditable' ) == 'false' )
70 continue;
71 }
72
73 if ( e.equals( root ) )
74 break;
75
76 if ( !blockLimit ) {
77 elementName = e.getName();
78
79 // First editable element becomes a block limit, because it cannot be split.
80 if ( e.getAttribute( 'contenteditable' ) == 'true' )
81 blockLimit = e;
82 // "Else" because element cannot be both - block and block levelimit.
83 else if ( !block && pathBlockElements[ elementName ] )
84 block = e;
85
86 if ( pathBlockLimitElements[ elementName ] ) {
87 // End level DIV is considered as the block, if no block is available. (#525)
88 // But it must NOT be the root element (checked above).
89 if ( !block && elementName == 'div' && !checkHasBlock( e ) )
90 block = e;
91 else
92 blockLimit = e;
93 }
94 }
95 }
96 }
97 while ( ( e = e.getParent() ) );
98
99 // Block limit defaults to root.
100 if ( !blockLimit )
101 blockLimit = root;
102
103 /**
104 * First non-empty block element which:
105 *
106 * * is not a {@link CKEDITOR.dtd#$blockLimit},
107 * * or is a `div` which does not contain block elements and is not a `root`.
108 *
109 * This means a first, splittable block in elements path.
110 *
111 * @readonly
112 * @property {CKEDITOR.dom.element}
113 */
114 this.block = block;
115
116 /**
117 * See the {@link CKEDITOR.dtd#$blockLimit} description.
118 *
119 * @readonly
120 * @property {CKEDITOR.dom.element}
121 */
122 this.blockLimit = blockLimit;
123
124 /**
125 * The root of the elements path - `root` argument passed to class constructor or a `body` element.
126 *
127 * @readonly
128 * @property {CKEDITOR.dom.element}
129 */
130 this.root = root;
131
132 /**
133 * An array of elements (from `startNode` to `root`) in the path.
134 *
135 * @readonly
136 * @property {CKEDITOR.dom.element[]}
137 */
138 this.elements = elements;
139
140 /**
141 * The last element of the elements path - `startNode` or its parent.
142 *
143 * @readonly
144 * @property {CKEDITOR.dom.element} lastElement
145 */
146 };
147
148} )();
149
150CKEDITOR.dom.elementPath.prototype = {
151 /**
152 * Compares this element path with another one.
153 *
154 * @param {CKEDITOR.dom.elementPath} otherPath The elementPath object to be
155 * compared with this one.
156 * @returns {Boolean} `true` if the paths are equal, containing the same
157 * number of elements and the same elements in the same order.
158 */
159 compare: function( otherPath ) {
160 var thisElements = this.elements;
161 var otherElements = otherPath && otherPath.elements;
162
163 if ( !otherElements || thisElements.length != otherElements.length )
164 return false;
165
166 for ( var i = 0; i < thisElements.length; i++ ) {
167 if ( !thisElements[ i ].equals( otherElements[ i ] ) )
168 return false;
169 }
170
171 return true;
172 },
173
174 /**
175 * Search the path elements that meets the specified criteria.
176 *
177 * @param {String/Array/Function/Object/CKEDITOR.dom.element} query The criteria that can be
178 * either a tag name, list (array and object) of tag names, element or an node evaluator function.
179 * @param {Boolean} [excludeRoot] Not taking path root element into consideration.
180 * @param {Boolean} [fromTop] Search start from the topmost element instead of bottom.
181 * @returns {CKEDITOR.dom.element} The first matched dom element or `null`.
182 */
183 contains: function( query, excludeRoot, fromTop ) {
184 var evaluator;
185 if ( typeof query == 'string' )
186 evaluator = function( node ) {
187 return node.getName() == query;
188 };
189 if ( query instanceof CKEDITOR.dom.element )
190 evaluator = function( node ) {
191 return node.equals( query );
192 };
193 else if ( CKEDITOR.tools.isArray( query ) )
194 evaluator = function( node ) {
195 return CKEDITOR.tools.indexOf( query, node.getName() ) > -1;
196 };
197 else if ( typeof query == 'function' )
198 evaluator = query;
199 else if ( typeof query == 'object' )
200 evaluator = function( node ) {
201 return node.getName() in query;
202 };
203
204 var elements = this.elements,
205 length = elements.length;
206 excludeRoot && length--;
207
208 if ( fromTop ) {
209 elements = Array.prototype.slice.call( elements, 0 );
210 elements.reverse();
211 }
212
213 for ( var i = 0; i < length; i++ ) {
214 if ( evaluator( elements[ i ] ) )
215 return elements[ i ];
216 }
217
218 return null;
219 },
220
221 /**
222 * Check whether the elements path is the proper context for the specified
223 * tag name in the DTD.
224 *
225 * @param {String} tag The tag name.
226 * @returns {Boolean}
227 */
228 isContextFor: function( tag ) {
229 var holder;
230
231 // Check for block context.
232 if ( tag in CKEDITOR.dtd.$block ) {
233 // Indeterminate elements which are not subjected to be splitted or surrounded must be checked first.
234 var inter = this.contains( CKEDITOR.dtd.$intermediate );
235 holder = inter || ( this.root.equals( this.block ) && this.block ) || this.blockLimit;
236 return !!holder.getDtd()[ tag ];
237 }
238
239 return true;
240 },
241
242 /**
243 * Retrieve the text direction for this elements path.
244 *
245 * @returns {'ltr'/'rtl'}
246 */
247 direction: function() {
248 var directionNode = this.block || this.blockLimit || this.root;
249 return directionNode.getDirection( 1 );
250 }
251};
diff --git a/sources/core/dom/event.js b/sources/core/dom/event.js
new file mode 100644
index 00000000..ee88d78c
--- /dev/null
+++ b/sources/core/dom/event.js
@@ -0,0 +1,208 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.event} class, which
8 * represents the a native DOM event object.
9 */
10
11/**
12 * Represents a native DOM event object.
13 *
14 * @class
15 * @constructor Creates an event class instance.
16 * @param {Object} domEvent A native DOM event object.
17 */
18CKEDITOR.dom.event = function( domEvent ) {
19 /**
20 * The native DOM event object represented by this class instance.
21 *
22 * @readonly
23 */
24 this.$ = domEvent;
25};
26
27CKEDITOR.dom.event.prototype = {
28 /**
29 * Gets the key code associated to the event.
30 *
31 * alert( event.getKey() ); // '65' is 'a' has been pressed
32 *
33 * @returns {Number} The key code.
34 */
35 getKey: function() {
36 return this.$.keyCode || this.$.which;
37 },
38
39 /**
40 * Gets a number represeting the combination of the keys pressed during the
41 * event. It is the sum with the current key code and the {@link CKEDITOR#CTRL},
42 * {@link CKEDITOR#SHIFT} and {@link CKEDITOR#ALT} constants.
43 *
44 * alert( event.getKeystroke() == 65 ); // 'a' key
45 * alert( event.getKeystroke() == CKEDITOR.CTRL + 65 ); // CTRL + 'a' key
46 * alert( event.getKeystroke() == CKEDITOR.CTRL + CKEDITOR.SHIFT + 65 ); // CTRL + SHIFT + 'a' key
47 *
48 * @returns {Number} The number representing the keys combination.
49 */
50 getKeystroke: function() {
51 var keystroke = this.getKey();
52
53 if ( this.$.ctrlKey || this.$.metaKey )
54 keystroke += CKEDITOR.CTRL;
55
56 if ( this.$.shiftKey )
57 keystroke += CKEDITOR.SHIFT;
58
59 if ( this.$.altKey )
60 keystroke += CKEDITOR.ALT;
61
62 return keystroke;
63 },
64
65 /**
66 * Prevents the original behavior of the event to happen. It can optionally
67 * stop propagating the event in the event chain.
68 *
69 * var element = CKEDITOR.document.getById( 'myElement' );
70 * element.on( 'click', function( ev ) {
71 * // The DOM event object is passed by the 'data' property.
72 * var domEvent = ev.data;
73 * // Prevent the click to chave any effect in the element.
74 * domEvent.preventDefault();
75 * } );
76 *
77 * @param {Boolean} [stopPropagation=false] Stop propagating this event in the
78 * event chain.
79 */
80 preventDefault: function( stopPropagation ) {
81 var $ = this.$;
82 if ( $.preventDefault )
83 $.preventDefault();
84 else
85 $.returnValue = false;
86
87 if ( stopPropagation )
88 this.stopPropagation();
89 },
90
91 /**
92 * Stops this event propagation in the event chain.
93 */
94 stopPropagation: function() {
95 var $ = this.$;
96 if ( $.stopPropagation )
97 $.stopPropagation();
98 else
99 $.cancelBubble = true;
100 },
101
102 /**
103 * Returns the DOM node where the event was targeted to.
104 *
105 * var element = CKEDITOR.document.getById( 'myElement' );
106 * element.on( 'click', function( ev ) {
107 * // The DOM event object is passed by the 'data' property.
108 * var domEvent = ev.data;
109 * // Add a CSS class to the event target.
110 * domEvent.getTarget().addClass( 'clicked' );
111 * } );
112 *
113 * @returns {CKEDITOR.dom.node} The target DOM node.
114 */
115 getTarget: function() {
116 var rawNode = this.$.target || this.$.srcElement;
117 return rawNode ? new CKEDITOR.dom.node( rawNode ) : null;
118 },
119
120 /**
121 * Returns an integer value that indicates the current processing phase of an event.
122 * For browsers that doesn't support event phase, {@link CKEDITOR#EVENT_PHASE_AT_TARGET} is always returned.
123 *
124 * @returns {Number} One of {@link CKEDITOR#EVENT_PHASE_CAPTURING},
125 * {@link CKEDITOR#EVENT_PHASE_AT_TARGET}, or {@link CKEDITOR#EVENT_PHASE_BUBBLING}.
126 */
127 getPhase: function() {
128 return this.$.eventPhase || 2;
129 },
130
131 /**
132 * Retrieves the coordinates of the mouse pointer relative to the top-left
133 * corner of the document, in mouse related event.
134 *
135 * element.on( 'mousemouse', function( ev ) {
136 * var pageOffset = ev.data.getPageOffset();
137 * alert( pageOffset.x ); // page offset X
138 * alert( pageOffset.y ); // page offset Y
139 * } );
140 *
141 * @returns {Object} The object contains the position.
142 * @returns {Number} return.x
143 * @returns {Number} return.y
144 */
145 getPageOffset: function() {
146 var doc = this.getTarget().getDocument().$;
147 var pageX = this.$.pageX || this.$.clientX + ( doc.documentElement.scrollLeft || doc.body.scrollLeft );
148 var pageY = this.$.pageY || this.$.clientY + ( doc.documentElement.scrollTop || doc.body.scrollTop );
149 return { x: pageX, y: pageY };
150 }
151};
152
153// For the followind constants, we need to go over the Unicode boundaries
154// (0x10FFFF) to avoid collision.
155
156/**
157 * CTRL key (0x110000).
158 *
159 * @readonly
160 * @property {Number} [=0x110000]
161 * @member CKEDITOR
162 */
163CKEDITOR.CTRL = 0x110000;
164
165/**
166 * SHIFT key (0x220000).
167 *
168 * @readonly
169 * @property {Number} [=0x220000]
170 * @member CKEDITOR
171 */
172CKEDITOR.SHIFT = 0x220000;
173
174/**
175 * ALT key (0x440000).
176 *
177 * @readonly
178 * @property {Number} [=0x440000]
179 * @member CKEDITOR
180 */
181CKEDITOR.ALT = 0x440000;
182
183/**
184 * Capturing phase.
185 *
186 * @readonly
187 * @property {Number} [=1]
188 * @member CKEDITOR
189 */
190CKEDITOR.EVENT_PHASE_CAPTURING = 1;
191
192/**
193 * Event at target.
194 *
195 * @readonly
196 * @property {Number} [=2]
197 * @member CKEDITOR
198 */
199CKEDITOR.EVENT_PHASE_AT_TARGET = 2;
200
201/**
202 * Bubbling phase.
203 *
204 * @readonly
205 * @property {Number} [=3]
206 * @member CKEDITOR
207 */
208CKEDITOR.EVENT_PHASE_BUBBLING = 3;
diff --git a/sources/core/dom/iterator.js b/sources/core/dom/iterator.js
new file mode 100644
index 00000000..99491218
--- /dev/null
+++ b/sources/core/dom/iterator.js
@@ -0,0 +1,565 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @ignore
8 * File overview: DOM iterator which iterates over list items, lines and paragraphs.
9 */
10
11'use strict';
12
13( function() {
14 /**
15 * Represents the iterator class. It can be used to iterate
16 * over all elements (or even text nodes in case of {@link #enlargeBr} set to `false`)
17 * which establish "paragraph-like" spaces within the passed range.
18 *
19 * // <h1>[foo</h1><p>bar]</p>
20 * var iterator = range.createIterator();
21 * iterator.getNextParagraph(); // h1 element
22 * iterator.getNextParagraph(); // p element
23 *
24 * // <ul><li>[foo</li><li>bar]</li>
25 * // With enforceRealBlocks set to false the iterator will return two list item elements.
26 * // With enforceRealBlocks set to true the iterator will return two paragraphs and the DOM will be changed to:
27 * // <ul><li><p>foo</p></li><li><p>bar</p></li>
28 *
29 * @class CKEDITOR.dom.iterator
30 * @constructor Creates an iterator class instance.
31 * @param {CKEDITOR.dom.range} range
32 */
33 function iterator( range ) {
34 if ( arguments.length < 1 )
35 return;
36
37 /**
38 * @readonly
39 * @property {CKEDITOR.dom.range}
40 */
41 this.range = range;
42
43 /**
44 * @property {Boolean} [forceBrBreak=false]
45 */
46 this.forceBrBreak = 0;
47
48 // (#3730).
49 /**
50 * Whether to include `<br>` elements in the enlarged range. Should be
51 * set to `false` when using the iterator in the {@link CKEDITOR#ENTER_BR} mode.
52 *
53 * @property {Boolean} [enlargeBr=true]
54 */
55 this.enlargeBr = 1;
56
57 /**
58 * Whether the iterator should create a transformable block
59 * if the current one contains text and cannot be transformed.
60 * For example new blocks will be established in elements like
61 * `<li>` or `<td>`.
62 *
63 * @property {Boolean} [enforceRealBlocks=false]
64 */
65 this.enforceRealBlocks = 0;
66
67 this._ || ( this._ = {} );
68 }
69
70 /**
71 * Default iterator's filter. It is set only for nested iterators.
72 *
73 * @since 4.3
74 * @readonly
75 * @property {CKEDITOR.filter} filter
76 */
77
78 /**
79 * Iterator's active filter. It is set by the {@link #getNextParagraph} method
80 * when it enters a nested editable.
81 *
82 * @since 4.3
83 * @readonly
84 * @property {CKEDITOR.filter} activeFilter
85 */
86
87 var beginWhitespaceRegex = /^[\r\n\t ]+$/,
88 // Ignore bookmark nodes.(#3783)
89 bookmarkGuard = CKEDITOR.dom.walker.bookmark( false, true ),
90 whitespacesGuard = CKEDITOR.dom.walker.whitespaces( true ),
91 skipGuard = function( node ) {
92 return bookmarkGuard( node ) && whitespacesGuard( node );
93 },
94 listItemNames = { dd: 1, dt: 1, li: 1 };
95
96 iterator.prototype = {
97 /**
98 * Returns the next paragraph-like element or `null` if the end of a range is reached.
99 *
100 * @param {String} [blockTag='p'] Name of a block element which will be established by
101 * the iterator in block-less elements (see {@link #enforceRealBlocks}).
102 */
103 getNextParagraph: function( blockTag ) {
104 // The block element to be returned.
105 var block;
106
107 // The range object used to identify the paragraph contents.
108 var range;
109
110 // Indicats that the current element in the loop is the last one.
111 var isLast;
112
113 // Instructs to cleanup remaining BRs.
114 var removePreviousBr, removeLastBr;
115
116 blockTag = blockTag || 'p';
117
118 // We're iterating over nested editable.
119 if ( this._.nestedEditable ) {
120 // Get next block from nested iterator and returns it if was found.
121 block = this._.nestedEditable.iterator.getNextParagraph( blockTag );
122 if ( block ) {
123 // Inherit activeFilter from the nested iterator.
124 this.activeFilter = this._.nestedEditable.iterator.activeFilter;
125 return block;
126 }
127
128 // No block in nested iterator means that we reached the end of the nested editable.
129 // Reset the active filter to the default filter (or undefined if this iterator didn't have it).
130 this.activeFilter = this.filter;
131
132 // Try to find next nested editable or get back to parent (this) iterator.
133 if ( startNestedEditableIterator( this, blockTag, this._.nestedEditable.container, this._.nestedEditable.remaining ) ) {
134 // Inherit activeFilter from the nested iterator.
135 this.activeFilter = this._.nestedEditable.iterator.activeFilter;
136 return this._.nestedEditable.iterator.getNextParagraph( blockTag );
137 } else {
138 this._.nestedEditable = null;
139 }
140 }
141
142 // Block-less range should be checked first.
143 if ( !this.range.root.getDtd()[ blockTag ] )
144 return null;
145
146 // This is the first iteration. Let's initialize it.
147 if ( !this._.started )
148 range = startIterator.call( this );
149
150 var currentNode = this._.nextNode,
151 lastNode = this._.lastNode;
152
153 this._.nextNode = null;
154 while ( currentNode ) {
155 // closeRange indicates that a paragraph boundary has been found,
156 // so the range can be closed.
157 var closeRange = 0,
158 parentPre = currentNode.hasAscendant( 'pre' );
159
160 // includeNode indicates that the current node is good to be part
161 // of the range. By default, any non-element node is ok for it.
162 var includeNode = ( currentNode.type != CKEDITOR.NODE_ELEMENT ),
163 continueFromSibling = 0;
164
165 // If it is an element node, let's check if it can be part of the range.
166 if ( !includeNode ) {
167 var nodeName = currentNode.getName();
168
169 // Non-editable block was found - return it and move to processing
170 // its nested editables if they exist.
171 if ( CKEDITOR.dtd.$block[ nodeName ] && currentNode.getAttribute( 'contenteditable' ) == 'false' ) {
172 block = currentNode;
173
174 // Setup iterator for first of nested editables.
175 // If there's no editable, then algorithm will move to next element after current block.
176 startNestedEditableIterator( this, blockTag, block );
177
178 // Gets us straight to the end of getParagraph() because block variable is set.
179 break;
180 } else if ( currentNode.isBlockBoundary( this.forceBrBreak && !parentPre && { br: 1 } ) ) {
181 // <br> boundaries must be part of the range. It will
182 // happen only if ForceBrBreak.
183 if ( nodeName == 'br' )
184 includeNode = 1;
185 else if ( !range && !currentNode.getChildCount() && nodeName != 'hr' ) {
186 // If we have found an empty block, and haven't started
187 // the range yet, it means we must return this block.
188 block = currentNode;
189 isLast = currentNode.equals( lastNode );
190 break;
191 }
192
193 // The range must finish right before the boundary,
194 // including possibly skipped empty spaces. (#1603)
195 if ( range ) {
196 range.setEndAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
197
198 // The found boundary must be set as the next one at this
199 // point. (#1717)
200 if ( nodeName != 'br' ) {
201 this._.nextNode = currentNode;
202 }
203 }
204
205 closeRange = 1;
206 } else {
207 // If we have child nodes, let's check them.
208 if ( currentNode.getFirst() ) {
209 // If we don't have a range yet, let's start it.
210 if ( !range ) {
211 range = this.range.clone();
212 range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
213 }
214
215 currentNode = currentNode.getFirst();
216 continue;
217 }
218 includeNode = 1;
219 }
220 } else if ( currentNode.type == CKEDITOR.NODE_TEXT ) {
221 // Ignore normal whitespaces (i.e. not including &nbsp; or
222 // other unicode whitespaces) before/after a block node.
223 if ( beginWhitespaceRegex.test( currentNode.getText() ) )
224 includeNode = 0;
225 }
226
227 // The current node is good to be part of the range and we are
228 // starting a new range, initialize it first.
229 if ( includeNode && !range ) {
230 range = this.range.clone();
231 range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
232 }
233
234 // The last node has been found.
235 isLast = ( ( !closeRange || includeNode ) && currentNode.equals( lastNode ) );
236
237 // If we are in an element boundary, let's check if it is time
238 // to close the range, otherwise we include the parent within it.
239 if ( range && !closeRange ) {
240 while ( !currentNode.getNext( skipGuard ) && !isLast ) {
241 var parentNode = currentNode.getParent();
242
243 if ( parentNode.isBlockBoundary( this.forceBrBreak && !parentPre && { br: 1 } ) ) {
244 closeRange = 1;
245 includeNode = 0;
246 isLast = isLast || ( parentNode.equals( lastNode ) );
247 // Make sure range includes bookmarks at the end of the block. (#7359)
248 range.setEndAt( parentNode, CKEDITOR.POSITION_BEFORE_END );
249 break;
250 }
251
252 currentNode = parentNode;
253 includeNode = 1;
254 isLast = ( currentNode.equals( lastNode ) );
255 continueFromSibling = 1;
256 }
257 }
258
259 // Now finally include the node.
260 if ( includeNode )
261 range.setEndAt( currentNode, CKEDITOR.POSITION_AFTER_END );
262
263 currentNode = this._getNextSourceNode( currentNode, continueFromSibling, lastNode );
264 isLast = !currentNode;
265
266 // We have found a block boundary. Let's close the range and move out of the
267 // loop.
268 if ( isLast || ( closeRange && range ) )
269 break;
270 }
271
272 // Now, based on the processed range, look for (or create) the block to be returned.
273 if ( !block ) {
274 // If no range has been found, this is the end.
275 if ( !range ) {
276 this._.docEndMarker && this._.docEndMarker.remove();
277 this._.nextNode = null;
278 return null;
279 }
280
281 var startPath = new CKEDITOR.dom.elementPath( range.startContainer, range.root );
282 var startBlockLimit = startPath.blockLimit,
283 checkLimits = { div: 1, th: 1, td: 1 };
284 block = startPath.block;
285
286 if ( !block && startBlockLimit && !this.enforceRealBlocks && checkLimits[ startBlockLimit.getName() ] &&
287 range.checkStartOfBlock() && range.checkEndOfBlock() && !startBlockLimit.equals( range.root ) ) {
288 block = startBlockLimit;
289 } else if ( !block || ( this.enforceRealBlocks && block.is( listItemNames ) ) ) {
290 // Create the fixed block.
291 block = this.range.document.createElement( blockTag );
292
293 // Move the contents of the temporary range to the fixed block.
294 range.extractContents().appendTo( block );
295 block.trim();
296
297 // Insert the fixed block into the DOM.
298 range.insertNode( block );
299
300 removePreviousBr = removeLastBr = true;
301 } else if ( block.getName() != 'li' ) {
302 // If the range doesn't includes the entire contents of the
303 // block, we must split it, isolating the range in a dedicated
304 // block.
305 if ( !range.checkStartOfBlock() || !range.checkEndOfBlock() ) {
306 // The resulting block will be a clone of the current one.
307 block = block.clone( false );
308
309 // Extract the range contents, moving it to the new block.
310 range.extractContents().appendTo( block );
311 block.trim();
312
313 // Split the block. At this point, the range will be in the
314 // right position for our intents.
315 var splitInfo = range.splitBlock();
316
317 removePreviousBr = !splitInfo.wasStartOfBlock;
318 removeLastBr = !splitInfo.wasEndOfBlock;
319
320 // Insert the new block into the DOM.
321 range.insertNode( block );
322 }
323 } else if ( !isLast ) {
324 // LIs are returned as is, with all their children (due to the
325 // nested lists). But, the next node is the node right after
326 // the current range, which could be an <li> child (nested
327 // lists) or the next sibling <li>.
328
329 this._.nextNode = ( block.equals( lastNode ) ? null : this._getNextSourceNode( range.getBoundaryNodes().endNode, 1, lastNode ) );
330 }
331 }
332
333 if ( removePreviousBr ) {
334 var previousSibling = block.getPrevious();
335 if ( previousSibling && previousSibling.type == CKEDITOR.NODE_ELEMENT ) {
336 if ( previousSibling.getName() == 'br' )
337 previousSibling.remove();
338 else if ( previousSibling.getLast() && previousSibling.getLast().$.nodeName.toLowerCase() == 'br' )
339 previousSibling.getLast().remove();
340 }
341 }
342
343 if ( removeLastBr ) {
344 var lastChild = block.getLast();
345 if ( lastChild && lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.getName() == 'br' ) {
346 // Remove br filler on browser which do not need it.
347 if ( !CKEDITOR.env.needsBrFiller || lastChild.getPrevious( bookmarkGuard ) || lastChild.getNext( bookmarkGuard ) )
348 lastChild.remove();
349 }
350 }
351
352 // Get a reference for the next element. This is important because the
353 // above block can be removed or changed, so we can rely on it for the
354 // next interation.
355 if ( !this._.nextNode ) {
356 this._.nextNode = ( isLast || block.equals( lastNode ) || !lastNode ) ? null : this._getNextSourceNode( block, 1, lastNode );
357 }
358
359 return block;
360 },
361
362 /**
363 * Gets the next element to check or `null` when the `lastNode` or the
364 * {@link #range}'s {@link CKEDITOR.dom.range#root root} is reached. Bookmarks are skipped.
365 *
366 * @since 4.4.6
367 * @private
368 * @param {CKEDITOR.dom.node} node
369 * @param {Boolean} startFromSibling
370 * @param {CKEDITOR.dom.node} lastNode
371 * @returns {CKEDITOR.dom.node}
372 */
373 _getNextSourceNode: function( node, startFromSibling, lastNode ) {
374 var rootNode = this.range.root,
375 next;
376
377 // Here we are checking in guard function whether current element
378 // reach lastNode(default behaviour) and root node to prevent against
379 // getting out of editor instance root DOM object.
380 // #12484
381 function guardFunction( node ) {
382 return !( node.equals( lastNode ) || node.equals( rootNode ) );
383 }
384
385 next = node.getNextSourceNode( startFromSibling, null, guardFunction );
386 while ( !bookmarkGuard( next ) ) {
387 next = next.getNextSourceNode( startFromSibling, null, guardFunction );
388 }
389 return next;
390 }
391 };
392
393 // @context CKEDITOR.dom.iterator
394 // @returns Collapsed range which will be reused when during furter processing.
395 function startIterator() {
396 var range = this.range.clone(),
397 // Indicate at least one of the range boundaries is inside a preformat block.
398 touchPre,
399
400 // (#12178)
401 // Remember if following situation takes place:
402 // * startAtInnerBoundary: <p>foo[</p>...
403 // * endAtInnerBoundary: ...<p>]bar</p>
404 // Because information about line break will be lost when shrinking range.
405 // Note that we test only if path block exist, because we must properly shrink
406 // range containing table and/or table cells.
407 // Note: When range is collapsed there's no way it can be shrinked.
408 // By checking if range is collapsed we also prevent #12308.
409 startPath = range.startPath(),
410 endPath = range.endPath(),
411 startAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, startPath.block ),
412 endAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, endPath.block, 1 );
413
414 // Shrink the range to exclude harmful "noises" (#4087, #4450, #5435).
415 range.shrink( CKEDITOR.SHRINK_ELEMENT, true );
416
417 if ( startAtInnerBoundary )
418 range.setStartAt( startPath.block, CKEDITOR.POSITION_BEFORE_END );
419 if ( endAtInnerBoundary )
420 range.setEndAt( endPath.block, CKEDITOR.POSITION_AFTER_START );
421
422 touchPre = range.endContainer.hasAscendant( 'pre', true ) || range.startContainer.hasAscendant( 'pre', true );
423
424 range.enlarge( this.forceBrBreak && !touchPre || !this.enlargeBr ? CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR.ENLARGE_BLOCK_CONTENTS );
425
426 if ( !range.collapsed ) {
427 var walker = new CKEDITOR.dom.walker( range.clone() ),
428 ignoreBookmarkTextEvaluator = CKEDITOR.dom.walker.bookmark( true, true );
429 // Avoid anchor inside bookmark inner text.
430 walker.evaluator = ignoreBookmarkTextEvaluator;
431 this._.nextNode = walker.next();
432 // TODO: It's better to have walker.reset() used here.
433 walker = new CKEDITOR.dom.walker( range.clone() );
434 walker.evaluator = ignoreBookmarkTextEvaluator;
435 var lastNode = walker.previous();
436 this._.lastNode = lastNode.getNextSourceNode( true, null, range.root );
437
438 // We may have an empty text node at the end of block due to [3770].
439 // If that node is the lastNode, it would cause our logic to leak to the
440 // next block.(#3887)
441 if ( this._.lastNode && this._.lastNode.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.trim( this._.lastNode.getText() ) && this._.lastNode.getParent().isBlockBoundary() ) {
442 var testRange = this.range.clone();
443 testRange.moveToPosition( this._.lastNode, CKEDITOR.POSITION_AFTER_END );
444 if ( testRange.checkEndOfBlock() ) {
445 var path = new CKEDITOR.dom.elementPath( testRange.endContainer, testRange.root ),
446 lastBlock = path.block || path.blockLimit;
447 this._.lastNode = lastBlock.getNextSourceNode( true );
448 }
449 }
450
451 // The end of document or range.root was reached, so we need a marker node inside.
452 if ( !this._.lastNode || !range.root.contains( this._.lastNode ) ) {
453 this._.lastNode = this._.docEndMarker = range.document.createText( '' );
454 this._.lastNode.insertAfter( lastNode );
455 }
456
457 // Let's reuse this variable.
458 range = null;
459 }
460
461 this._.started = 1;
462
463 return range;
464 }
465
466 // Does a nested editables lookup inside editablesContainer.
467 // If remainingEditables is set will lookup inside this array.
468 // @param {CKEDITOR.dom.element} editablesContainer
469 // @param {CKEDITOR.dom.element[]} [remainingEditables]
470 function getNestedEditableIn( editablesContainer, remainingEditables ) {
471 if ( remainingEditables == null )
472 remainingEditables = findNestedEditables( editablesContainer );
473
474 var editable;
475
476 while ( ( editable = remainingEditables.shift() ) ) {
477 if ( isIterableEditable( editable ) )
478 return { element: editable, remaining: remainingEditables };
479 }
480
481 return null;
482 }
483
484 // Checkes whether we can iterate over this editable.
485 function isIterableEditable( editable ) {
486 // Reject blockless editables.
487 return editable.getDtd().p;
488 }
489
490 // Finds nested editables within container. Does not return
491 // editables nested in another editable (twice).
492 function findNestedEditables( container ) {
493 var editables = [];
494
495 container.forEach( function( element ) {
496 if ( element.getAttribute( 'contenteditable' ) == 'true' ) {
497 editables.push( element );
498 return false; // Skip children.
499 }
500 }, CKEDITOR.NODE_ELEMENT, true );
501
502 return editables;
503 }
504
505 // Looks for a first nested editable after previousEditable (if passed) and creates
506 // nested iterator for it.
507 function startNestedEditableIterator( parentIterator, blockTag, editablesContainer, remainingEditables ) {
508 var editable = getNestedEditableIn( editablesContainer, remainingEditables );
509
510 if ( !editable )
511 return 0;
512
513 var filter = CKEDITOR.filter.instances[ editable.element.data( 'cke-filter' ) ];
514
515 // If current editable has a filter and this filter does not allow for block tag,
516 // search for next nested editable in remaining ones.
517 if ( filter && !filter.check( blockTag ) )
518 return startNestedEditableIterator( parentIterator, blockTag, editablesContainer, editable.remaining );
519
520 var range = new CKEDITOR.dom.range( editable.element );
521 range.selectNodeContents( editable.element );
522
523 var iterator = range.createIterator();
524 // This setting actually does not change anything in this case,
525 // because entire range contents is selected, so there're no <br>s to be included.
526 // But it seems right to copy it too.
527 iterator.enlargeBr = parentIterator.enlargeBr;
528 // Inherit configuration from parent iterator.
529 iterator.enforceRealBlocks = parentIterator.enforceRealBlocks;
530 // Set the activeFilter (which can be overriden when this iteator will start nested iterator)
531 // and the default filter, which will make it possible to reset to
532 // current iterator's activeFilter after leaving nested editable.
533 iterator.activeFilter = iterator.filter = filter;
534
535 parentIterator._.nestedEditable = {
536 element: editable.element,
537 container: editablesContainer,
538 remaining: editable.remaining,
539 iterator: iterator
540 };
541
542 return 1;
543 }
544
545 // Checks whether range starts or ends at inner block boundary.
546 // See usage comments to learn more.
547 function rangeAtInnerBlockBoundary( range, block, checkEnd ) {
548 if ( !block )
549 return false;
550
551 var testRange = range.clone();
552 testRange.collapse( !checkEnd );
553 return testRange.checkBoundaryOfElement( block, checkEnd ? CKEDITOR.START : CKEDITOR.END );
554 }
555
556 /**
557 * Creates a {CKEDITOR.dom.iterator} instance for this range.
558 *
559 * @member CKEDITOR.dom.range
560 * @returns {CKEDITOR.dom.iterator}
561 */
562 CKEDITOR.dom.range.prototype.createIterator = function() {
563 return new iterator( this );
564 };
565} )();
diff --git a/sources/core/dom/node.js b/sources/core/dom/node.js
new file mode 100644
index 00000000..5d791319
--- /dev/null
+++ b/sources/core/dom/node.js
@@ -0,0 +1,897 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.node} class which is the base
8 * class for classes that represent DOM nodes.
9 */
10
11/**
12 * Base class for classes representing DOM nodes. This constructor may return
13 * an instance of a class that inherits from this class, like
14 * {@link CKEDITOR.dom.element} or {@link CKEDITOR.dom.text}.
15 *
16 * @class
17 * @extends CKEDITOR.dom.domObject
18 * @constructor Creates a node class instance.
19 * @param {Object} domNode A native DOM node.
20 * @see CKEDITOR.dom.element
21 * @see CKEDITOR.dom.text
22 */
23CKEDITOR.dom.node = function( domNode ) {
24 if ( domNode ) {
25 var type =
26 domNode.nodeType == CKEDITOR.NODE_DOCUMENT ? 'document' :
27 domNode.nodeType == CKEDITOR.NODE_ELEMENT ? 'element' :
28 domNode.nodeType == CKEDITOR.NODE_TEXT ? 'text' :
29 domNode.nodeType == CKEDITOR.NODE_COMMENT ? 'comment' :
30 domNode.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ? 'documentFragment' :
31 'domObject'; // Call the base constructor otherwise.
32
33 return new CKEDITOR.dom[ type ]( domNode );
34 }
35
36 return this;
37};
38
39CKEDITOR.dom.node.prototype = new CKEDITOR.dom.domObject();
40
41/**
42 * Element node type.
43 *
44 * @readonly
45 * @property {Number} [=1]
46 * @member CKEDITOR
47 */
48CKEDITOR.NODE_ELEMENT = 1;
49
50/**
51 * Document node type.
52 *
53 * @readonly
54 * @property {Number} [=9]
55 * @member CKEDITOR
56 */
57CKEDITOR.NODE_DOCUMENT = 9;
58
59/**
60 * Text node type.
61 *
62 * @readonly
63 * @property {Number} [=3]
64 * @member CKEDITOR
65 */
66CKEDITOR.NODE_TEXT = 3;
67
68/**
69 * Comment node type.
70 *
71 * @readonly
72 * @property {Number} [=8]
73 * @member CKEDITOR
74 */
75CKEDITOR.NODE_COMMENT = 8;
76
77/**
78 * Document fragment node type.
79 *
80 * @readonly
81 * @property {Number} [=11]
82 * @member CKEDITOR
83 */
84CKEDITOR.NODE_DOCUMENT_FRAGMENT = 11;
85
86/**
87 * Indicates that positions of both nodes are identical (this is the same node). See {@link CKEDITOR.dom.node#getPosition}.
88 *
89 * @readonly
90 * @property {Number} [=0]
91 * @member CKEDITOR
92 */
93CKEDITOR.POSITION_IDENTICAL = 0;
94
95/**
96 * Indicates that nodes are in different (detached) trees. See {@link CKEDITOR.dom.node#getPosition}.
97 *
98 * @readonly
99 * @property {Number} [=1]
100 * @member CKEDITOR
101 */
102CKEDITOR.POSITION_DISCONNECTED = 1;
103
104/**
105 * Indicates that the context node follows the other node. See {@link CKEDITOR.dom.node#getPosition}.
106 *
107 * @readonly
108 * @property {Number} [=2]
109 * @member CKEDITOR
110 */
111CKEDITOR.POSITION_FOLLOWING = 2;
112
113/**
114 * Indicates that the context node precedes the other node. See {@link CKEDITOR.dom.node#getPosition}.
115 *
116 * @readonly
117 * @property {Number} [=4]
118 * @member CKEDITOR
119 */
120CKEDITOR.POSITION_PRECEDING = 4;
121
122/**
123 * Indicates that the context node is a descendant of the other node. See {@link CKEDITOR.dom.node#getPosition}.
124 *
125 * @readonly
126 * @property {Number} [=8]
127 * @member CKEDITOR
128 */
129CKEDITOR.POSITION_IS_CONTAINED = 8;
130
131/**
132 * Indicates that the context node contains the other node. See {@link CKEDITOR.dom.node#getPosition}.
133 *
134 * @readonly
135 * @property {Number} [=16]
136 * @member CKEDITOR
137 */
138CKEDITOR.POSITION_CONTAINS = 16;
139
140CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, {
141 /**
142 * Makes this node a child of another element.
143 *
144 * var p = new CKEDITOR.dom.element( 'p' );
145 * var strong = new CKEDITOR.dom.element( 'strong' );
146 * strong.appendTo( p );
147 *
148 * // Result: '<p><strong></strong></p>'.
149 *
150 * @param {CKEDITOR.dom.element} element The target element to which this node will be appended.
151 * @returns {CKEDITOR.dom.element} The target element.
152 */
153 appendTo: function( element, toStart ) {
154 element.append( this, toStart );
155 return element;
156 },
157
158 /**
159 * Clones this node.
160 *
161 * **Note**: Values set by {#setCustomData} will not be available in the clone.
162 *
163 * @param {Boolean} [includeChildren=false] If `true` then all node's
164 * children will be cloned recursively.
165 * @param {Boolean} [cloneId=false] Whether ID attributes should be cloned, too.
166 * @returns {CKEDITOR.dom.node} Clone of this node.
167 */
168 clone: function( includeChildren, cloneId ) {
169 var $clone = this.$.cloneNode( includeChildren );
170
171 // The "id" attribute should never be cloned to avoid duplication.
172 removeIds( $clone );
173
174 var node = new CKEDITOR.dom.node( $clone );
175
176 // On IE8 we need to fixed HTML5 node name, see details below.
177 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 &&
178 ( this.type == CKEDITOR.NODE_ELEMENT || this.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) {
179 renameNodes( node );
180 }
181
182 return node;
183
184 function removeIds( node ) {
185 // Reset data-cke-expando only when has been cloned (IE and only for some types of objects).
186 if ( node[ 'data-cke-expando' ] )
187 node[ 'data-cke-expando' ] = false;
188
189 if ( node.nodeType != CKEDITOR.NODE_ELEMENT && node.nodeType != CKEDITOR.NODE_DOCUMENT_FRAGMENT )
190 return;
191
192 if ( !cloneId && node.nodeType == CKEDITOR.NODE_ELEMENT )
193 node.removeAttribute( 'id', false );
194
195 if ( includeChildren ) {
196 var childs = node.childNodes;
197 for ( var i = 0; i < childs.length; i++ )
198 removeIds( childs[ i ] );
199 }
200 }
201
202 // IE8 rename HTML5 nodes by adding `:` at the begging of the tag name when the node is cloned,
203 // so `<figure>` will be `<:figure>` after 'cloneNode'. We need to fix it (#13101).
204 function renameNodes( node ) {
205 if ( node.type != CKEDITOR.NODE_ELEMENT && node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT )
206 return;
207
208 if ( node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) {
209 var name = node.getName();
210 if ( name[ 0 ] == ':' ) {
211 node.renameNode( name.substring( 1 ) );
212 }
213 }
214
215 if ( includeChildren ) {
216 for ( var i = 0; i < node.getChildCount(); i++ )
217 renameNodes( node.getChild( i ) );
218 }
219 }
220 },
221
222 /**
223 * Checks if the node is preceded by any sibling.
224 *
225 * @returns {Boolean}
226 */
227 hasPrevious: function() {
228 return !!this.$.previousSibling;
229 },
230
231 /**
232 * Checks if the node is succeeded by any sibling.
233 *
234 * @returns {Boolean}
235 */
236 hasNext: function() {
237 return !!this.$.nextSibling;
238 },
239
240 /**
241 * Inserts this element after a node.
242 *
243 * var em = new CKEDITOR.dom.element( 'em' );
244 * var strong = new CKEDITOR.dom.element( 'strong' );
245 * strong.insertAfter( em );
246 *
247 * // Result: '<em></em><strong></strong>'
248 *
249 * @param {CKEDITOR.dom.node} node The node that will precede this element.
250 * @returns {CKEDITOR.dom.node} The node preceding this one after insertion.
251 */
252 insertAfter: function( node ) {
253 node.$.parentNode.insertBefore( this.$, node.$.nextSibling );
254 return node;
255 },
256
257 /**
258 * Inserts this element before a node.
259 *
260 * var em = new CKEDITOR.dom.element( 'em' );
261 * var strong = new CKEDITOR.dom.element( 'strong' );
262 * strong.insertBefore( em );
263 *
264 * // result: '<strong></strong><em></em>'
265 *
266 * @param {CKEDITOR.dom.node} node The node that will succeed this element.
267 * @returns {CKEDITOR.dom.node} The node being inserted.
268 */
269 insertBefore: function( node ) {
270 node.$.parentNode.insertBefore( this.$, node.$ );
271 return node;
272 },
273
274 /**
275 * Inserts a node before this node.
276 *
277 * var em = new CKEDITOR.dom.element( 'em' );
278 * var strong = new CKEDITOR.dom.element( 'strong' );
279 * strong.insertBeforeMe( em );
280 *
281 * // result: '<em></em><strong></strong>'
282 *
283 * @param {CKEDITOR.dom.node} node The node that will preceed this element.
284 * @returns {CKEDITOR.dom.node} The node being inserted.
285 */
286 insertBeforeMe: function( node ) {
287 this.$.parentNode.insertBefore( node.$, this.$ );
288 return node;
289 },
290
291 /**
292 * Retrieves a uniquely identifiable tree address for this node.
293 * The tree address returned is an array of integers, with each integer
294 * indicating a child index of a DOM node, starting from
295 * `document.documentElement`.
296 *
297 * For example, assuming `<body>` is the second child
298 * of `<html>` (`<head>` being the first),
299 * and we would like to address the third child under the
300 * fourth child of `<body>`, the tree address returned would be:
301 * `[1, 3, 2]`.
302 *
303 * The tree address cannot be used for finding back the DOM tree node once
304 * the DOM tree structure has been modified.
305 *
306 * @param {Boolean} [normalized=false] See {@link #getIndex}.
307 * @returns {Array} The address.
308 */
309 getAddress: function( normalized ) {
310 var address = [];
311 var $documentElement = this.getDocument().$.documentElement;
312 var node = this.$;
313
314 while ( node && node != $documentElement ) {
315 var parentNode = node.parentNode;
316
317 if ( parentNode ) {
318 // Get the node index. For performance, call getIndex
319 // directly, instead of creating a new node object.
320 address.unshift( this.getIndex.call( { $: node }, normalized ) );
321 }
322
323 node = parentNode;
324 }
325
326 return address;
327 },
328
329 /**
330 * Gets the document containing this element.
331 *
332 * var element = CKEDITOR.document.getById( 'example' );
333 * alert( element.getDocument().equals( CKEDITOR.document ) ); // true
334 *
335 * @returns {CKEDITOR.dom.document} The document.
336 */
337 getDocument: function() {
338 return new CKEDITOR.dom.document( this.$.ownerDocument || this.$.parentNode.ownerDocument );
339 },
340
341 /**
342 * Gets the index of a node in an array of its `parent.childNodes`.
343 * Returns `-1` if a node does not have a parent or when the `normalized` argument is set to `true`
344 * and the text node is empty and will be removed during the normalization.
345 *
346 * Let us assume having the following `childNodes` array:
347 *
348 * [ emptyText, element1, text, text, element2, emptyText2 ]
349 *
350 * emptyText.getIndex() // 0
351 * emptyText.getIndex( true ) // -1
352 * element1.getIndex(); // 1
353 * element1.getIndex( true ); // 0
354 * element2.getIndex(); // 4
355 * element2.getIndex( true ); // 2
356 * emptyText2.getIndex(); // 5
357 * emptyText2.getIndex( true ); // -1
358 *
359 * @param {Boolean} normalized When `true`, adjacent text nodes are merged and empty text nodes are removed.
360 * @returns {Number} Index of a node or `-1` if a node does not have a parent or is removed during the normalization.
361 */
362 getIndex: function( normalized ) {
363 // Attention: getAddress depends on this.$
364 // getIndex is called on a plain object: { $ : node }
365
366 var current = this.$,
367 index = -1,
368 isNormalizing;
369
370 if ( !this.$.parentNode )
371 return -1;
372
373 // The idea is - all empty text nodes will be virtually merged into their adjacent text nodes.
374 // If an empty text node does not have an adjacent non-empty text node we can return -1 straight away,
375 // because it and all its sibling text nodes will be merged into an empty text node and then totally ignored.
376 if ( normalized && current.nodeType == CKEDITOR.NODE_TEXT && !current.nodeValue ) {
377 var adjacent = getAdjacentNonEmptyTextNode( current ) || getAdjacentNonEmptyTextNode( current, true );
378
379 if ( !adjacent )
380 return -1;
381 }
382
383 do {
384 // Bypass blank node and adjacent text nodes.
385 if ( normalized && current != this.$ && current.nodeType == CKEDITOR.NODE_TEXT && ( isNormalizing || !current.nodeValue ) )
386 continue;
387
388 index++;
389 isNormalizing = current.nodeType == CKEDITOR.NODE_TEXT;
390 }
391 while ( ( current = current.previousSibling ) );
392
393 return index;
394
395 function getAdjacentNonEmptyTextNode( node, lookForward ) {
396 var sibling = lookForward ? node.nextSibling : node.previousSibling;
397
398 if ( !sibling || sibling.nodeType != CKEDITOR.NODE_TEXT ) {
399 return null;
400 }
401
402 // If found a non-empty text node, then return it.
403 // If not, then continue search.
404 return sibling.nodeValue ? sibling : getAdjacentNonEmptyTextNode( sibling, lookForward );
405 }
406 },
407
408 /**
409 * @todo
410 */
411 getNextSourceNode: function( startFromSibling, nodeType, guard ) {
412 // If "guard" is a node, transform it in a function.
413 if ( guard && !guard.call ) {
414 var guardNode = guard;
415 guard = function( node ) {
416 return !node.equals( guardNode );
417 };
418 }
419
420 var node = ( !startFromSibling && this.getFirst && this.getFirst() ),
421 parent;
422
423 // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
424 // send the 'moving out' signal even we don't actually dive into.
425 if ( !node ) {
426 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
427 return null;
428 node = this.getNext();
429 }
430
431 while ( !node && ( parent = ( parent || this ).getParent() ) ) {
432 // The guard check sends the "true" paramenter to indicate that
433 // we are moving "out" of the element.
434 if ( guard && guard( parent, true ) === false )
435 return null;
436
437 node = parent.getNext();
438 }
439
440 if ( !node )
441 return null;
442
443 if ( guard && guard( node ) === false )
444 return null;
445
446 if ( nodeType && nodeType != node.type )
447 return node.getNextSourceNode( false, nodeType, guard );
448
449 return node;
450 },
451
452 /**
453 * @todo
454 */
455 getPreviousSourceNode: function( startFromSibling, nodeType, guard ) {
456 if ( guard && !guard.call ) {
457 var guardNode = guard;
458 guard = function( node ) {
459 return !node.equals( guardNode );
460 };
461 }
462
463 var node = ( !startFromSibling && this.getLast && this.getLast() ),
464 parent;
465
466 // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
467 // send the 'moving out' signal even we don't actually dive into.
468 if ( !node ) {
469 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
470 return null;
471 node = this.getPrevious();
472 }
473
474 while ( !node && ( parent = ( parent || this ).getParent() ) ) {
475 // The guard check sends the "true" paramenter to indicate that
476 // we are moving "out" of the element.
477 if ( guard && guard( parent, true ) === false )
478 return null;
479
480 node = parent.getPrevious();
481 }
482
483 if ( !node )
484 return null;
485
486 if ( guard && guard( node ) === false )
487 return null;
488
489 if ( nodeType && node.type != nodeType )
490 return node.getPreviousSourceNode( false, nodeType, guard );
491
492 return node;
493 },
494
495 /**
496 * Gets the node that preceeds this element in its parent's child list.
497 *
498 * var element = CKEDITOR.dom.element.createFromHtml( '<div><i>prev</i><b>Example</b></div>' );
499 * var first = element.getLast().getPrev();
500 * alert( first.getName() ); // 'i'
501 *
502 * @param {Function} [evaluator] Filtering the result node.
503 * @returns {CKEDITOR.dom.node} The previous node or null if not available.
504 */
505 getPrevious: function( evaluator ) {
506 var previous = this.$,
507 retval;
508 do {
509 previous = previous.previousSibling;
510
511 // Avoid returning the doc type node.
512 // http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927
513 retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous );
514 }
515 while ( retval && evaluator && !evaluator( retval ) );
516 return retval;
517 },
518
519 /**
520 * Gets the node that follows this element in its parent's child list.
521 *
522 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b><i>next</i></div>' );
523 * var last = element.getFirst().getNext();
524 * alert( last.getName() ); // 'i'
525 *
526 * @param {Function} [evaluator] Filtering the result node.
527 * @returns {CKEDITOR.dom.node} The next node or null if not available.
528 */
529 getNext: function( evaluator ) {
530 var next = this.$,
531 retval;
532 do {
533 next = next.nextSibling;
534 retval = next && new CKEDITOR.dom.node( next );
535 }
536 while ( retval && evaluator && !evaluator( retval ) );
537 return retval;
538 },
539
540 /**
541 * Gets the parent element for this node.
542 *
543 * var node = editor.document.getBody().getFirst();
544 * var parent = node.getParent();
545 * alert( parent.getName() ); // 'body'
546 *
547 * @param {Boolean} [allowFragmentParent=false] Consider also parent node that is of
548 * fragment type {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
549 * @returns {CKEDITOR.dom.element} The parent element.
550 */
551 getParent: function( allowFragmentParent ) {
552 var parent = this.$.parentNode;
553 return ( parent && ( parent.nodeType == CKEDITOR.NODE_ELEMENT || allowFragmentParent && parent.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) ? new CKEDITOR.dom.node( parent ) : null;
554 },
555
556 /**
557 * Returns an array containing node parents and the node itself. By default nodes are in _descending_ order.
558 *
559 * // Assuming that body has paragraph as the first child.
560 * var node = editor.document.getBody().getFirst();
561 * var parents = node.getParents();
562 * alert( parents[ 0 ].getName() + ',' + parents[ 2 ].getName() ); // 'html,p'
563 *
564 * @param {Boolean} [closerFirst=false] Determines the order of returned nodes.
565 * @returns {Array} Returns an array of {@link CKEDITOR.dom.node}.
566 */
567 getParents: function( closerFirst ) {
568 var node = this;
569 var parents = [];
570
571 do {
572 parents[ closerFirst ? 'push' : 'unshift' ]( node );
573 }
574 while ( ( node = node.getParent() ) );
575
576 return parents;
577 },
578
579 /**
580 * @todo
581 */
582 getCommonAncestor: function( node ) {
583 if ( node.equals( this ) )
584 return this;
585
586 if ( node.contains && node.contains( this ) )
587 return node;
588
589 var start = this.contains ? this : this.getParent();
590
591 do {
592 if ( start.contains( node ) ) return start;
593 }
594 while ( ( start = start.getParent() ) );
595
596 return null;
597 },
598
599 /**
600 * Determines the position relation between this node and the given {@link CKEDITOR.dom.node} in the document.
601 * This node can be preceding ({@link CKEDITOR#POSITION_PRECEDING}) or following ({@link CKEDITOR#POSITION_FOLLOWING})
602 * the given node. This node can also contain ({@link CKEDITOR#POSITION_CONTAINS}) or be contained by
603 * ({@link CKEDITOR#POSITION_IS_CONTAINED}) the given node. The function returns a bitmask of constants
604 * listed above or {@link CKEDITOR#POSITION_IDENTICAL} if the given node is the same as this node.
605 *
606 * @param {CKEDITOR.dom.node} otherNode A node to check relation with.
607 * @returns {Number} Position relation between this node and given node.
608 */
609 getPosition: function( otherNode ) {
610 var $ = this.$;
611 var $other = otherNode.$;
612
613 if ( $.compareDocumentPosition )
614 return $.compareDocumentPosition( $other );
615
616 // IE and Safari have no support for compareDocumentPosition.
617
618 if ( $ == $other )
619 return CKEDITOR.POSITION_IDENTICAL;
620
621 // Only element nodes support contains and sourceIndex.
622 if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) {
623 if ( $.contains ) {
624 if ( $.contains( $other ) )
625 return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING;
626
627 if ( $other.contains( $ ) )
628 return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
629 }
630
631 if ( 'sourceIndex' in $ )
632 return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
633
634 }
635
636 // For nodes that don't support compareDocumentPosition, contains
637 // or sourceIndex, their "address" is compared.
638
639 var addressOfThis = this.getAddress(),
640 addressOfOther = otherNode.getAddress(),
641 minLevel = Math.min( addressOfThis.length, addressOfOther.length );
642
643 // Determinate preceding/following relationship.
644 for ( var i = 0; i < minLevel; i++ ) {
645 if ( addressOfThis[ i ] != addressOfOther[ i ] ) {
646 return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
647 }
648 }
649
650 // Determinate contains/contained relationship.
651 return ( addressOfThis.length < addressOfOther.length ) ? CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
652 },
653
654 /**
655 * Gets the closest ancestor node of this node, specified by its name or using an evaluator function.
656 *
657 * // Suppose we have the following HTML structure:
658 * // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div>
659 * // If node == <b>
660 * ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner">
661 * ascendant = node.getAscendant( 'b' ); // ascendant == null
662 * ascendant = node.getAscendant( 'b', true ); // ascendant == <b>
663 * ascendant = node.getAscendant( { div:1, p:1 } ); // Searches for the first 'div' or 'p': ascendant == <div id="inner">
664 *
665 * // Using custom evaluator:
666 * ascendant = node.getAscendant( function( el ) {
667 * return el.getId() == 'inner';
668 * } );
669 * // ascendant == <div id="inner">
670 *
671 * @since 3.6.1
672 * @param {String/Function/Object} query The name of the ancestor node to search or
673 * an object with the node names to search for or an evaluator function.
674 * @param {Boolean} [includeSelf] Whether to include the current
675 * node in the search.
676 * @returns {CKEDITOR.dom.node} The located ancestor node or `null` if not found.
677 */
678 getAscendant: function( query, includeSelf ) {
679 var $ = this.$,
680 evaluator,
681 isCustomEvaluator;
682
683 if ( !includeSelf ) {
684 $ = $.parentNode;
685 }
686
687 // Custom checker provided in an argument.
688 if ( typeof query == 'function' ) {
689 isCustomEvaluator = true;
690 evaluator = query;
691 } else {
692 // Predefined tag name checker.
693 isCustomEvaluator = false;
694 evaluator = function( $ ) {
695 var name = ( typeof $.nodeName == 'string' ? $.nodeName.toLowerCase() : '' );
696
697 return ( typeof query == 'string' ? name == query : name in query );
698 };
699 }
700
701 while ( $ ) {
702 // For user provided checker we use CKEDITOR.dom.node.
703 if ( evaluator( isCustomEvaluator ? new CKEDITOR.dom.node( $ ) : $ ) ) {
704 return new CKEDITOR.dom.node( $ );
705 }
706
707 try {
708 $ = $.parentNode;
709 } catch ( e ) {
710 $ = null;
711 }
712 }
713
714 return null;
715 },
716
717 /**
718 * @todo
719 */
720 hasAscendant: function( name, includeSelf ) {
721 var $ = this.$;
722
723 if ( !includeSelf )
724 $ = $.parentNode;
725
726 while ( $ ) {
727 if ( $.nodeName && $.nodeName.toLowerCase() == name )
728 return true;
729
730 $ = $.parentNode;
731 }
732 return false;
733 },
734
735 /**
736 * @todo
737 */
738 move: function( target, toStart ) {
739 target.append( this.remove(), toStart );
740 },
741
742 /**
743 * Removes this node from the document DOM.
744 *
745 * var element = CKEDITOR.document.getById( 'MyElement' );
746 * element.remove();
747 *
748 * @param {Boolean} [preserveChildren=false] Indicates that the children
749 * elements must remain in the document, removing only the outer tags.
750 */
751 remove: function( preserveChildren ) {
752 var $ = this.$;
753 var parent = $.parentNode;
754
755 if ( parent ) {
756 if ( preserveChildren ) {
757 // Move all children before the node.
758 for ( var child;
759 ( child = $.firstChild ); ) {
760 parent.insertBefore( $.removeChild( child ), $ );
761 }
762 }
763
764 parent.removeChild( $ );
765 }
766
767 return this;
768 },
769
770 /**
771 * @todo
772 */
773 replace: function( nodeToReplace ) {
774 this.insertBefore( nodeToReplace );
775 nodeToReplace.remove();
776 },
777
778 /**
779 * @todo
780 */
781 trim: function() {
782 this.ltrim();
783 this.rtrim();
784 },
785
786 /**
787 * @todo
788 */
789 ltrim: function() {
790 var child;
791 while ( this.getFirst && ( child = this.getFirst() ) ) {
792 if ( child.type == CKEDITOR.NODE_TEXT ) {
793 var trimmed = CKEDITOR.tools.ltrim( child.getText() ),
794 originalLength = child.getLength();
795
796 if ( !trimmed ) {
797 child.remove();
798 continue;
799 } else if ( trimmed.length < originalLength ) {
800 child.split( originalLength - trimmed.length );
801
802 // IE BUG: child.remove() may raise JavaScript errors here. (#81)
803 this.$.removeChild( this.$.firstChild );
804 }
805 }
806 break;
807 }
808 },
809
810 /**
811 * @todo
812 */
813 rtrim: function() {
814 var child;
815 while ( this.getLast && ( child = this.getLast() ) ) {
816 if ( child.type == CKEDITOR.NODE_TEXT ) {
817 var trimmed = CKEDITOR.tools.rtrim( child.getText() ),
818 originalLength = child.getLength();
819
820 if ( !trimmed ) {
821 child.remove();
822 continue;
823 } else if ( trimmed.length < originalLength ) {
824 child.split( trimmed.length );
825
826 // IE BUG: child.getNext().remove() may raise JavaScript errors here.
827 // (#81)
828 this.$.lastChild.parentNode.removeChild( this.$.lastChild );
829 }
830 }
831 break;
832 }
833
834 if ( CKEDITOR.env.needsBrFiller ) {
835 child = this.$.lastChild;
836
837 if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) {
838 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324).
839 child.parentNode.removeChild( child );
840 }
841 }
842 },
843
844 /**
845 * Checks if this node is read-only (should not be changed).
846 *
847 * // For the following HTML:
848 * // <b>foo</b><div contenteditable="false"><i>bar</i></div>
849 *
850 * elB.isReadOnly(); // -> false
851 * foo.isReadOnly(); // -> false
852 * elDiv.isReadOnly(); // -> true
853 * elI.isReadOnly(); // -> true
854 *
855 * This method works in two modes depending on browser support for the `element.isContentEditable` property and
856 * the value of the `checkOnlyAttributes` parameter. The `element.isContentEditable` check is faster, but it is known
857 * to malfunction in hidden or detached nodes. Additionally, when processing some detached DOM tree you may want to imitate
858 * that this happens inside an editable container (like it would happen inside the {@link CKEDITOR.editable}). To do so,
859 * you can temporarily attach this tree to an element with the `data-cke-editable` attribute and use the
860 * `checkOnlyAttributes` mode.
861 *
862 * @since 3.5
863 * @param {Boolean} [checkOnlyAttributes=false] If `true`, only attributes will be checked, native methods will not
864 * be used. This parameter needs to be `true` to check hidden or detached elements. Introduced in 4.5.
865 * @returns {Boolean}
866 */
867 isReadOnly: function( checkOnlyAttributes ) {
868 var element = this;
869 if ( this.type != CKEDITOR.NODE_ELEMENT )
870 element = this.getParent();
871
872 // Prevent Edge crash (#13609, #13919).
873 if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) {
874 checkOnlyAttributes = true;
875 }
876
877 if ( !checkOnlyAttributes && element && typeof element.$.isContentEditable != 'undefined' ) {
878 return !( element.$.isContentEditable || element.data( 'cke-editable' ) );
879 }
880 else {
881 // Degrade for old browsers which don't support "isContentEditable", e.g. FF3
882
883 while ( element ) {
884 if ( element.data( 'cke-editable' ) ) {
885 return false;
886 } else if ( element.hasAttribute( 'contenteditable' ) ) {
887 return element.getAttribute( 'contenteditable' ) == 'false';
888 }
889
890 element = element.getParent();
891 }
892
893 // Reached the root of DOM tree, no editable found.
894 return true;
895 }
896 }
897} );
diff --git a/sources/core/dom/nodelist.js b/sources/core/dom/nodelist.js
new file mode 100644
index 00000000..0bbe3ee6
--- /dev/null
+++ b/sources/core/dom/nodelist.js
@@ -0,0 +1,43 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * Represents a list of {@link CKEDITOR.dom.node} objects.
8 * It's a wrapper for native nodes list.
9 *
10 * var nodeList = CKEDITOR.document.getBody().getChildren();
11 * alert( nodeList.count() ); // number [0;N]
12 *
13 * @class
14 * @constructor Creates a document class instance.
15 * @param {Object} nativeList
16 */
17CKEDITOR.dom.nodeList = function( nativeList ) {
18 this.$ = nativeList;
19};
20
21CKEDITOR.dom.nodeList.prototype = {
22 /**
23 * Get count of nodes in this list.
24 *
25 * @returns {Number}
26 */
27 count: function() {
28 return this.$.length;
29 },
30
31 /**
32 * Get node from the list.
33 *
34 * @returns {CKEDITOR.dom.node}
35 */
36 getItem: function( index ) {
37 if ( index < 0 || index >= this.$.length )
38 return null;
39
40 var $node = this.$[ index ];
41 return $node ? new CKEDITOR.dom.node( $node ) : null;
42 }
43};
diff --git a/sources/core/dom/range.js b/sources/core/dom/range.js
new file mode 100644
index 00000000..fe75c550
--- /dev/null
+++ b/sources/core/dom/range.js
@@ -0,0 +1,2860 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * Represents a delimited piece of content in a DOM Document.
8 * It is contiguous in the sense that it can be characterized as selecting all
9 * of the content between a pair of boundary-points.
10 *
11 * This class shares much of the W3C
12 * [Document Object Model Range](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html)
13 * ideas and features, adding several range manipulation tools to it, but it's
14 * not intended to be compatible with it.
15 *
16 * // Create a range for the entire contents of the editor document body.
17 * var range = new CKEDITOR.dom.range( editor.document );
18 * range.selectNodeContents( editor.document.getBody() );
19 * // Delete the contents.
20 * range.deleteContents();
21 *
22 * Usually you will want to work on a ranges rooted in the editor's {@link CKEDITOR.editable editable}
23 * element. Such ranges can be created with a shorthand method &ndash; {@link CKEDITOR.editor#createRange editor.createRange}.
24 *
25 * var range = editor.createRange();
26 * range.root.equals( editor.editable() ); // -> true
27 *
28 * Note that the {@link #root} of a range is an important property, which limits many
29 * algorithms implemented in range's methods. Therefore it is crucial, especially
30 * when using ranges inside inline editors, to specify correct root, so using
31 * the {@link CKEDITOR.editor#createRange} method is highly recommended.
32 *
33 * ### Selection
34 *
35 * Range is only a logical representation of a piece of content in a DOM. It should not
36 * be confused with a {@link CKEDITOR.dom.selection selection} which represents "physically
37 * marked" content. It is possible to create unlimited number of various ranges, when
38 * only one real selection may exist at a time in a document. Ranges are used to read position
39 * of selection in the DOM and to move selection to new positions.
40 *
41 * The editor selection may be retrieved using the {@link CKEDITOR.editor#getSelection} method:
42 *
43 * var sel = editor.getSelection(),
44 * ranges = sel.getRanges(); // CKEDITOR.dom.rangeList instance.
45 *
46 * var range = ranges[ 0 ];
47 * range.root; // -> editor's editable element.
48 *
49 * A range can also be selected:
50 *
51 * var range = editor.createRange();
52 * range.selectNodeContents( editor.editable() );
53 * sel.selectRanges( [ range ] );
54 *
55 * @class
56 * @constructor Creates a {@link CKEDITOR.dom.range} instance that can be used inside a specific DOM Document.
57 * @param {CKEDITOR.dom.document/CKEDITOR.dom.element} root The document or element
58 * within which the range will be scoped.
59 * @todo global "TODO" - precise algorithms descriptions needed for the most complex methods like #enlarge.
60 */
61CKEDITOR.dom.range = function( root ) {
62 /**
63 * Node within which the range begins.
64 *
65 * var range = new CKEDITOR.dom.range( editor.document );
66 * range.selectNodeContents( editor.document.getBody() );
67 * alert( range.startContainer.getName() ); // 'body'
68 *
69 * @readonly
70 * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
71 */
72 this.startContainer = null;
73
74 /**
75 * Offset within the starting node of the range.
76 *
77 * var range = new CKEDITOR.dom.range( editor.document );
78 * range.selectNodeContents( editor.document.getBody() );
79 * alert( range.startOffset ); // 0
80 *
81 * @readonly
82 * @property {Number}
83 */
84 this.startOffset = null;
85
86 /**
87 * Node within which the range ends.
88 *
89 * var range = new CKEDITOR.dom.range( editor.document );
90 * range.selectNodeContents( editor.document.getBody() );
91 * alert( range.endContainer.getName() ); // 'body'
92 *
93 * @readonly
94 * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
95 */
96 this.endContainer = null;
97
98 /**
99 * Offset within the ending node of the range.
100 *
101 * var range = new CKEDITOR.dom.range( editor.document );
102 * range.selectNodeContents( editor.document.getBody() );
103 * alert( range.endOffset ); // == editor.document.getBody().getChildCount()
104 *
105 * @readonly
106 * @property {Number}
107 */
108 this.endOffset = null;
109
110 /**
111 * Indicates that this is a collapsed range. A collapsed range has its
112 * start and end boundaries at the very same point so nothing is contained
113 * in it.
114 *
115 * var range = new CKEDITOR.dom.range( editor.document );
116 * range.selectNodeContents( editor.document.getBody() );
117 * alert( range.collapsed ); // false
118 * range.collapse();
119 * alert( range.collapsed ); // true
120 *
121 * @readonly
122 */
123 this.collapsed = true;
124
125 var isDocRoot = root instanceof CKEDITOR.dom.document;
126 /**
127 * The document within which the range can be used.
128 *
129 * // Selects the body contents of the range document.
130 * range.selectNodeContents( range.document.getBody() );
131 *
132 * @readonly
133 * @property {CKEDITOR.dom.document}
134 */
135 this.document = isDocRoot ? root : root.getDocument();
136
137 /**
138 * The ancestor DOM element within which the range manipulation are limited.
139 *
140 * @readonly
141 * @property {CKEDITOR.dom.element}
142 */
143 this.root = isDocRoot ? root.getBody() : root;
144};
145
146( function() {
147 // Updates the "collapsed" property for the given range object.
148 function updateCollapsed( range ) {
149 range.collapsed = ( range.startContainer && range.endContainer && range.startContainer.equals( range.endContainer ) && range.startOffset == range.endOffset );
150 }
151
152 // This is a shared function used to delete, extract and clone the range content.
153 //
154 // The outline of the algorithm:
155 //
156 // 1. Normalization. We handle special cases, split text nodes if we can, find boundary nodes (startNode and endNode).
157 // 2. Gathering data.
158 // * We start by creating two arrays of boundary nodes parents. You can imagine these arrays as lines limiting
159 // the tree from the left and right and thus marking the part which is selected by the range. The both lines
160 // start in the same node which is the range.root and end in startNode and endNode.
161 // * Then we find min level and max levels. Level represents all nodes which are equally far from the range.root.
162 // Min level is the level at which the left and right boundaries diverged (the first diverged level). And max levels
163 // are how deep the start and end nodes are nested.
164 // 3. Cloning/extraction.
165 // * We start iterating over start node parents (left branch) from min level and clone the parent (usually shallow clone,
166 // because we know that it's not fully selected) and its right siblings (deep clone, because they are fully selected).
167 // We iterate over siblings up to meeting end node parent or end of the siblings chain.
168 // * We clone level after level down to the startNode.
169 // * Then we do the same with end node parents (right branch), because it may contains notes we omit during the previous
170 // step, for example if the right branch is deeper then left branch. Things are more complicated here because we have to
171 // watch out for nodes that were already cloned.
172 // * ***Note:** Setting `cloneId` option to `false` for **extraction** works for partially selected elements only.
173 // See range.extractContents to know more.
174 // 4. Clean up.
175 // * There are two things we need to do - updating the range position and perform the action of the "mergeThen"
176 // param (see range.deleteContents or range.extractContents).
177 // See comments in mergeAndUpdate because this is lots of fun too.
178 function execContentsAction( range, action, docFrag, mergeThen, cloneId ) {
179 'use strict';
180
181 range.optimizeBookmark();
182
183 var isDelete = action === 0;
184 var isExtract = action == 1;
185 var isClone = action == 2;
186 var doClone = isClone || isExtract;
187
188 var startNode = range.startContainer;
189 var endNode = range.endContainer;
190
191 var startOffset = range.startOffset;
192 var endOffset = range.endOffset;
193
194 var cloneStartNode;
195 var cloneEndNode;
196
197 var doNotRemoveStartNode;
198 var doNotRemoveEndNode;
199
200 var cloneStartText;
201 var cloneEndText;
202
203 // Handle here an edge case where we clone a range which is located in one text node.
204 // This allows us to not think about startNode == endNode case later on.
205 // We do that only when cloning, because in other cases we can safely split this text node
206 // and hence we can easily handle this case as many others.
207 if ( isClone && endNode.type == CKEDITOR.NODE_TEXT && startNode.equals( endNode ) ) {
208 startNode = range.document.createText( startNode.substring( startOffset, endOffset ) );
209 docFrag.append( startNode );
210 return;
211 }
212
213 // For text containers, we must simply split the node and point to the
214 // second part. The removal will be handled by the rest of the code.
215 if ( endNode.type == CKEDITOR.NODE_TEXT ) {
216 // If Extract or Delete we can split the text node,
217 // but if Clone (2), then we cannot modify the DOM (#11586) so we mark the text node for cloning.
218 if ( !isClone ) {
219 endNode = endNode.split( endOffset );
220 } else {
221 cloneEndText = true;
222 }
223 } else {
224 // If there's no node after the range boundary we set endNode to the previous node
225 // and mark it to be cloned.
226 if ( endNode.getChildCount() > 0 ) {
227 // If the offset points after the last node.
228 if ( endOffset >= endNode.getChildCount() ) {
229 endNode = endNode.getChild( endOffset - 1 );
230 cloneEndNode = true;
231 } else {
232 endNode = endNode.getChild( endOffset );
233 }
234 }
235 // The end container is empty (<h1>]</h1>), but we want to clone it, although not remove.
236 else {
237 cloneEndNode = true;
238 doNotRemoveEndNode = true;
239 }
240 }
241
242 // For text containers, we must simply split the node. The removal will
243 // be handled by the rest of the code .
244 if ( startNode.type == CKEDITOR.NODE_TEXT ) {
245 // If Extract or Delete we can split the text node,
246 // but if Clone (2), then we cannot modify the DOM (#11586) so we mark
247 // the text node for cloning.
248 if ( !isClone ) {
249 startNode.split( startOffset );
250 } else {
251 cloneStartText = true;
252 }
253 } else {
254 // If there's no node before the range boundary we set startNode to the next node
255 // and mark it to be cloned.
256 if ( startNode.getChildCount() > 0 ) {
257 if ( startOffset === 0 ) {
258 startNode = startNode.getChild( startOffset );
259 cloneStartNode = true;
260 } else {
261 startNode = startNode.getChild( startOffset - 1 );
262 }
263 }
264 // The start container is empty (<h1>[</h1>), but we want to clone it, although not remove.
265 else {
266 cloneStartNode = true;
267 doNotRemoveStartNode = true;
268 }
269 }
270
271 // Get the parent nodes tree for the start and end boundaries.
272 var startParents = startNode.getParents(),
273 endParents = endNode.getParents(),
274 // Level at which start and end boundaries diverged.
275 minLevel = findMinLevel(),
276 maxLevelLeft = startParents.length - 1,
277 maxLevelRight = endParents.length - 1,
278 // Keeps the frag/element which is parent of the level that we are currently cloning.
279 levelParent = docFrag,
280 nextLevelParent,
281 leftNode,
282 rightNode,
283 nextSibling,
284 // Keeps track of the last connected level (on which left and right branches are connected)
285 // Usually this is minLevel, but not always.
286 lastConnectedLevel = -1;
287
288 // THE LEFT BRANCH.
289 for ( var level = minLevel; level <= maxLevelLeft; level++ ) {
290 leftNode = startParents[ level ];
291 nextSibling = leftNode.getNext();
292
293 // 1.
294 // The first step is to handle partial selection of the left branch.
295
296 // Max depth of the left branch. It means that ( leftSibling == endNode ).
297 // We also check if the leftNode isn't only partially selected, because in this case
298 // we want to make a shallow clone of it (the else part).
299 if ( level == maxLevelLeft && !( leftNode.equals( endParents[ level ] ) && maxLevelLeft < maxLevelRight ) ) {
300 if ( cloneStartNode ) {
301 consume( leftNode, levelParent, false, doNotRemoveStartNode );
302 } else if ( cloneStartText ) {
303 levelParent.append( range.document.createText( leftNode.substring( startOffset ) ) );
304 }
305 } else if ( doClone ) {
306 nextLevelParent = levelParent.append( leftNode.clone( 0, cloneId ) );
307 }
308
309 // 2.
310 // The second step is to handle full selection of the content between the left branch and the right branch.
311
312 while ( nextSibling ) {
313 // We can't clone entire endParent just like we can't clone entire startParent -
314 // - they are not fully selected with the range. Partial endParent selection
315 // will be cloned in the next loop.
316 if ( nextSibling.equals( endParents[ level ] ) ) {
317 lastConnectedLevel = level;
318 break;
319 }
320
321 nextSibling = consume( nextSibling, levelParent );
322 }
323
324 levelParent = nextLevelParent;
325 }
326
327 // Reset levelParent, because we reset the level.
328 levelParent = docFrag;
329
330 // THE RIGHT BRANCH.
331 for ( level = minLevel; level <= maxLevelRight; level++ ) {
332 rightNode = endParents[ level ];
333 nextSibling = rightNode.getPrevious();
334
335 // Do not process this node if it is shared with the left branch
336 // because it was already processed.
337 //
338 // Note: Don't worry about text nodes selection - if the entire range was placed in a single text node
339 // it was handled as a special case at the beginning. In other cases when startNode == endNode
340 // or when on this level leftNode == rightNode (so rightNode.equals( startParents[ level ] ))
341 // this node was handled by the previous loop.
342 if ( !rightNode.equals( startParents[ level ] ) ) {
343 // 1.
344 // The first step is to handle partial selection of the right branch.
345
346 // Max depth of the right branch. It means that ( rightNode == endNode ).
347 // We also check if the rightNode isn't only partially selected, because in this case
348 // we want to make a shallow clone of it (the else part).
349 if ( level == maxLevelRight && !( rightNode.equals( startParents[ level ] ) && maxLevelRight < maxLevelLeft ) ) {
350 if ( cloneEndNode ) {
351 consume( rightNode, levelParent, false, doNotRemoveEndNode );
352 } else if ( cloneEndText ) {
353 levelParent.append( range.document.createText( rightNode.substring( 0, endOffset ) ) );
354 }
355 } else if ( doClone ) {
356 nextLevelParent = levelParent.append( rightNode.clone( 0, cloneId ) );
357 }
358
359 // 2.
360 // The second step is to handle all left (selected) siblings of the rightNode which
361 // have not yet been handled. If the level branches were connected, the previous loop
362 // already copied all siblings (except the current rightNode).
363 if ( level > lastConnectedLevel ) {
364 while ( nextSibling ) {
365 nextSibling = consume( nextSibling, levelParent, true );
366 }
367 }
368
369 levelParent = nextLevelParent;
370 } else if ( doClone ) {
371 // If this is "shared" node and we are in cloning mode we have to update levelParent to
372 // reflect that we visited the node (even though we didn't process it).
373 // If we don't do that, in next iterations nodes will be appended to wrong parent.
374 //
375 // We can just take first child because the algorithm guarantees
376 // that this will be the only child on this level. (#13568)
377 levelParent = levelParent.getChild( 0 );
378 }
379 }
380
381 // Delete or Extract.
382 // We need to update the range and if mergeThen was passed do it.
383 if ( !isClone ) {
384 mergeAndUpdate();
385 }
386
387 // Depending on an action:
388 // * clones node and adds to new parent,
389 // * removes node,
390 // * moves node to the new parent.
391 function consume( node, newParent, toStart, forceClone ) {
392 var nextSibling = toStart ? node.getPrevious() : node.getNext();
393
394 // We do not clone if we are only deleting, so do nothing.
395 if ( forceClone && isDelete ) {
396 return nextSibling;
397 }
398
399 // If cloning, just clone it.
400 if ( isClone || forceClone ) {
401 newParent.append( node.clone( true, cloneId ), toStart );
402 } else {
403 // Both Delete and Extract will remove the node.
404 node.remove();
405
406 // When Extracting, move the removed node to the docFrag.
407 if ( isExtract ) {
408 newParent.append( node );
409 }
410 }
411
412 return nextSibling;
413 }
414
415 // Finds a level number on which both branches starts diverging.
416 // If such level does not exist, return the last on which both branches have nodes.
417 function findMinLevel() {
418 // Compare them, to find the top most siblings.
419 var i, topStart, topEnd,
420 maxLevel = Math.min( startParents.length, endParents.length );
421
422 for ( i = 0; i < maxLevel; i++ ) {
423 topStart = startParents[ i ];
424 topEnd = endParents[ i ];
425
426 // The compared nodes will match until we find the top most siblings (different nodes that have the same parent).
427 // "i" will hold the index in the parents array for the top most element.
428 if ( !topStart.equals( topEnd ) ) {
429 return i;
430 }
431 }
432
433 // When startNode == endNode.
434 return i - 1;
435 }
436
437 // Executed only when deleting or extracting to update range position
438 // and perform the merge operation.
439 function mergeAndUpdate() {
440 var commonLevel = minLevel - 1,
441 boundariesInEmptyNode = doNotRemoveStartNode && doNotRemoveEndNode && !startNode.equals( endNode );
442
443 // If a node has been partially selected, collapse the range between
444 // startParents[ minLevel + 1 ] and endParents[ minLevel + 1 ] (the first diverged elements).
445 // Otherwise, simply collapse it to the start. (W3C specs).
446 //
447 // All clear, right?
448 //
449 // It took me few hours to truly understand a previous version of this condition.
450 // Mine seems to be more straightforward (even if it doesn't look so) and I could leave you here
451 // without additional comments, but I'm not that mean so here goes the explanation.
452 //
453 // We want to know if both ends of the range are anchored in the same element. Really. It's this simple.
454 // But why? Because we need to differentiate situations like:
455 //
456 // <p>foo[<b>x</b>bar]y</p> (commonLevel = p, maxLL = "foo", maxLR = "y")
457 // from:
458 // <p>foo<b>x[</b>bar]y</p> (commonLevel = p, maxLL = "x", maxLR = "y")
459 //
460 // In the first case we can collapse the range to the left, because simply everything between range's
461 // boundaries was removed.
462 // In the second case we must place the range after </b>, because <b> was only **partially selected**.
463 //
464 // * <b> is our startParents[ commonLevel + 1 ]
465 // * "y" is our endParents[ commonLevel + 1 ].
466 //
467 // By now "bar" is removed from the DOM so <b> is a direct sibling of "y":
468 // <p>foo<b>x</b>y</p>
469 //
470 // Therefore it's enough to place the range between <b> and "y".
471 //
472 // Now, what does the comparison mean? Why not just taking startNode and endNode and checking
473 // their parents? Because the tree is already changed and they may be gone. Plus, thanks to
474 // cloneStartNode and cloneEndNode, that would be reaaaaly tricky.
475 //
476 // So we play with levels which can give us the same information:
477 // * commonLevel - the level of common ancestor,
478 // * maxLevel - 1 - the level of range boundary parent (range boundary is here like a bookmark span).
479 // * commonLevel < maxLevel - 1 - whether the range boundary is not a child of common ancestor.
480 //
481 // There's also an edge case in which both range boundaries were placed in empty nodes like:
482 // <p>[</p><p>]</p>
483 // Those boundaries were not removed, but in this case start and end nodes are child of the common ancestor.
484 // We handle this edge case separately.
485 if ( commonLevel < ( maxLevelLeft - 1 ) || commonLevel < ( maxLevelRight - 1 ) || boundariesInEmptyNode ) {
486 if ( boundariesInEmptyNode ) {
487 range.moveToPosition( endNode, CKEDITOR.POSITION_BEFORE_START );
488 } else if ( ( maxLevelRight == commonLevel + 1 ) && cloneEndNode ) {
489 // The maxLevelRight + 1 element could be already removed so we use the fact that
490 // we know that it was the last element in its parent.
491 range.moveToPosition( endParents[ commonLevel ], CKEDITOR.POSITION_BEFORE_END );
492 } else {
493 range.moveToPosition( endParents[ commonLevel + 1 ], CKEDITOR.POSITION_BEFORE_START );
494 }
495
496 // Merge split parents.
497 if ( mergeThen ) {
498 // Find the first diverged node in the left branch.
499 var topLeft = startParents[ commonLevel + 1 ];
500
501 // TopLeft may simply not exist if commonLevel == maxLevel or may be a text node.
502 if ( topLeft && topLeft.type == CKEDITOR.NODE_ELEMENT ) {
503 var span = CKEDITOR.dom.element.createFromHtml( '<span ' +
504 'data-cke-bookmark="1" style="display:none">&nbsp;</span>', range.document );
505 span.insertAfter( topLeft );
506 topLeft.mergeSiblings( false );
507 range.moveToBookmark( { startNode: span } );
508 }
509 }
510 } else {
511 // Collapse it to the start.
512 range.collapse( true );
513 }
514 }
515 }
516
517 var inlineChildReqElements = {
518 abbr: 1, acronym: 1, b: 1, bdo: 1, big: 1, cite: 1, code: 1, del: 1,
519 dfn: 1, em: 1, font: 1, i: 1, ins: 1, label: 1, kbd: 1, q: 1, samp: 1, small: 1, span: 1, strike: 1,
520 strong: 1, sub: 1, sup: 1, tt: 1, u: 1, 'var': 1
521 };
522
523 // Creates the appropriate node evaluator for the dom walker used inside
524 // check(Start|End)OfBlock.
525 function getCheckStartEndBlockEvalFunction() {
526 var skipBogus = false,
527 whitespaces = CKEDITOR.dom.walker.whitespaces(),
528 bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),
529 isBogus = CKEDITOR.dom.walker.bogus();
530
531 return function( node ) {
532 // First skip empty nodes
533 if ( bookmarkEvaluator( node ) || whitespaces( node ) )
534 return true;
535
536 // Skip the bogus node at the end of block.
537 if ( isBogus( node ) && !skipBogus ) {
538 skipBogus = true;
539 return true;
540 }
541
542 // If there's any visible text, then we're not at the start.
543 if ( node.type == CKEDITOR.NODE_TEXT &&
544 ( node.hasAscendant( 'pre' ) ||
545 CKEDITOR.tools.trim( node.getText() ).length ) ) {
546 return false;
547 }
548
549 // If there are non-empty inline elements (e.g. <img />), then we're not
550 // at the start.
551 if ( node.type == CKEDITOR.NODE_ELEMENT && !node.is( inlineChildReqElements ) )
552 return false;
553
554 return true;
555 };
556 }
557
558 var isBogus = CKEDITOR.dom.walker.bogus(),
559 nbspRegExp = /^[\t\r\n ]*(?:&nbsp;|\xa0)$/,
560 editableEval = CKEDITOR.dom.walker.editable(),
561 notIgnoredEval = CKEDITOR.dom.walker.ignored( true );
562
563 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
564 // text node and non-empty elements unless it's being bookmark text.
565 function elementBoundaryEval( checkStart ) {
566 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
567 bookmark = CKEDITOR.dom.walker.bookmark( 1 );
568
569 return function( node ) {
570 // First skip empty nodes.
571 if ( bookmark( node ) || whitespaces( node ) )
572 return true;
573
574 // Tolerant bogus br when checking at the end of block.
575 // Reject any text node unless it's being bookmark
576 // OR it's spaces.
577 // Reject any element unless it's being invisible empty. (#3883)
578 return !checkStart && isBogus( node ) ||
579 node.type == CKEDITOR.NODE_ELEMENT &&
580 node.is( CKEDITOR.dtd.$removeEmpty );
581 };
582 }
583
584 function getNextEditableNode( isPrevious ) {
585 return function() {
586 var first;
587
588 return this[ isPrevious ? 'getPreviousNode' : 'getNextNode' ]( function( node ) {
589 // Cache first not ignorable node.
590 if ( !first && notIgnoredEval( node ) )
591 first = node;
592
593 // Return true if found editable node, but not a bogus next to start of our lookup (first != bogus).
594 return editableEval( node ) && !( isBogus( node ) && node.equals( first ) );
595 } );
596 };
597 }
598
599 CKEDITOR.dom.range.prototype = {
600 /**
601 * Clones this range.
602 *
603 * @returns {CKEDITOR.dom.range}
604 */
605 clone: function() {
606 var clone = new CKEDITOR.dom.range( this.root );
607
608 clone._setStartContainer( this.startContainer );
609 clone.startOffset = this.startOffset;
610 clone._setEndContainer( this.endContainer );
611 clone.endOffset = this.endOffset;
612 clone.collapsed = this.collapsed;
613
614 return clone;
615 },
616
617 /**
618 * Makes the range collapsed by moving its start point (or end point if `toStart==true`)
619 * to the second end.
620 *
621 * @param {Boolean} toStart Collapse range "to start".
622 */
623 collapse: function( toStart ) {
624 if ( toStart ) {
625 this._setEndContainer( this.startContainer );
626 this.endOffset = this.startOffset;
627 } else {
628 this._setStartContainer( this.endContainer );
629 this.startOffset = this.endOffset;
630 }
631
632 this.collapsed = true;
633 },
634
635 /**
636 * Clones content nodes of the range and adds them to a document fragment, which is returned.
637 *
638 * @param {Boolean} [cloneId=true] Whether to preserve ID attributes in the clone.
639 * @returns {CKEDITOR.dom.documentFragment} Document fragment containing a clone of range's content.
640 */
641 cloneContents: function( cloneId ) {
642 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
643
644 cloneId = typeof cloneId == 'undefined' ? true : cloneId;
645
646 if ( !this.collapsed )
647 execContentsAction( this, 2, docFrag, false, cloneId );
648
649 return docFrag;
650 },
651
652 /**
653 * Deletes the content nodes of the range permanently from the DOM tree.
654 *
655 * @param {Boolean} [mergeThen] Merge any split elements result in DOM true due to partial selection.
656 */
657 deleteContents: function( mergeThen ) {
658 if ( this.collapsed )
659 return;
660
661 execContentsAction( this, 0, null, mergeThen );
662 },
663
664 /**
665 * The content nodes of the range are cloned and added to a document fragment,
666 * meanwhile they are removed permanently from the DOM tree.
667 *
668 * **Note:** Setting the `cloneId` parameter to `false` works for **partially** selected elements only.
669 * If an element with an ID attribute is **fully enclosed** in a range, it will keep the ID attribute
670 * regardless of the `cloneId` parameter value, because it is not cloned &mdash; it is moved to the returned
671 * document fragment.
672 *
673 * @param {Boolean} [mergeThen] Merge any split elements result in DOM true due to partial selection.
674 * @param {Boolean} [cloneId=true] Whether to preserve ID attributes in the extracted content.
675 * @returns {CKEDITOR.dom.documentFragment} Document fragment containing extracted content.
676 */
677 extractContents: function( mergeThen, cloneId ) {
678 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
679
680 cloneId = typeof cloneId == 'undefined' ? true : cloneId;
681
682 if ( !this.collapsed )
683 execContentsAction( this, 1, docFrag, mergeThen, cloneId );
684
685 return docFrag;
686 },
687
688 /**
689 * Creates a bookmark object, which can be later used to restore the
690 * range by using the {@link #moveToBookmark} function.
691 *
692 * This is an "intrusive" way to create a bookmark. It includes `<span>` tags
693 * in the range boundaries. The advantage of it is that it is possible to
694 * handle DOM mutations when moving back to the bookmark.
695 *
696 * **Note:** The inclusion of nodes in the DOM is a design choice and
697 * should not be changed as there are other points in the code that may be
698 * using those nodes to perform operations.
699 *
700 * @param {Boolean} [serializable] Indicates that the bookmark nodes
701 * must contain IDs, which can be used to restore the range even
702 * when these nodes suffer mutations (like cloning or `innerHTML` change).
703 * @returns {Object} And object representing a bookmark.
704 * @returns {CKEDITOR.dom.node/String} return.startNode Node or element ID.
705 * @returns {CKEDITOR.dom.node/String} return.endNode Node or element ID.
706 * @returns {Boolean} return.serializable
707 * @returns {Boolean} return.collapsed
708 */
709 createBookmark: function( serializable ) {
710 var startNode, endNode;
711 var baseId;
712 var clone;
713 var collapsed = this.collapsed;
714
715 startNode = this.document.createElement( 'span' );
716 startNode.data( 'cke-bookmark', 1 );
717 startNode.setStyle( 'display', 'none' );
718
719 // For IE, it must have something inside, otherwise it may be
720 // removed during DOM operations.
721 startNode.setHtml( '&nbsp;' );
722
723 if ( serializable ) {
724 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
725 startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );
726 }
727
728 // If collapsed, the endNode will not be created.
729 if ( !collapsed ) {
730 endNode = startNode.clone();
731 endNode.setHtml( '&nbsp;' );
732
733 if ( serializable )
734 endNode.setAttribute( 'id', baseId + 'E' );
735
736 clone = this.clone();
737 clone.collapse();
738 clone.insertNode( endNode );
739 }
740
741 clone = this.clone();
742 clone.collapse( true );
743 clone.insertNode( startNode );
744
745 // Update the range position.
746 if ( endNode ) {
747 this.setStartAfter( startNode );
748 this.setEndBefore( endNode );
749 } else {
750 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
751 }
752
753 return {
754 startNode: serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,
755 endNode: serializable ? baseId + 'E' : endNode,
756 serializable: serializable,
757 collapsed: collapsed
758 };
759 },
760
761 /**
762 * Creates a "non intrusive" and "mutation sensible" bookmark. This
763 * kind of bookmark should be used only when the DOM is supposed to
764 * remain stable after its creation.
765 *
766 * @param {Boolean} [normalized] Indicates that the bookmark must
767 * be normalized. When normalized, the successive text nodes are
768 * considered a single node. To successfully load a normalized
769 * bookmark, the DOM tree must also be normalized before calling
770 * {@link #moveToBookmark}.
771 * @returns {Object} An object representing the bookmark.
772 * @returns {Array} return.start Start container's address (see {@link CKEDITOR.dom.node#getAddress}).
773 * @returns {Array} return.end Start container's address.
774 * @returns {Number} return.startOffset
775 * @returns {Number} return.endOffset
776 * @returns {Boolean} return.collapsed
777 * @returns {Boolean} return.normalized
778 * @returns {Boolean} return.is2 This is "bookmark2".
779 */
780 createBookmark2: ( function() {
781 var isNotText = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_TEXT, true );
782
783 // Returns true for limit anchored in element and placed between text nodes.
784 //
785 // v
786 // <p>[text node] [text node]</p> -> true
787 //
788 // v
789 // <p> [text node]</p> -> false
790 //
791 // v
792 // <p>[text node][text node]</p> -> false (limit is anchored in text node)
793 function betweenTextNodes( container, offset ) {
794 // Not anchored in element or limit is on the edge.
795 if ( container.type != CKEDITOR.NODE_ELEMENT || offset === 0 || offset == container.getChildCount() )
796 return 0;
797
798 return container.getChild( offset - 1 ).type == CKEDITOR.NODE_TEXT &&
799 container.getChild( offset ).type == CKEDITOR.NODE_TEXT;
800 }
801
802 // Sums lengths of all preceding text nodes.
803 function getLengthOfPrecedingTextNodes( node ) {
804 var sum = 0;
805
806 while ( ( node = node.getPrevious() ) && node.type == CKEDITOR.NODE_TEXT )
807 sum += node.getLength();
808
809 return sum;
810 }
811
812 function normalize( limit ) {
813 var container = limit.container,
814 offset = limit.offset;
815
816 // If limit is between text nodes move it to the end of preceding one,
817 // because they will be merged.
818 if ( betweenTextNodes( container, offset ) ) {
819 container = container.getChild( offset - 1 );
820 offset = container.getLength();
821 }
822
823 // Now, if limit is anchored in element and has at least two nodes before it,
824 // it may happen that some of them will be merged. Normalize the offset
825 // by setting it to normalized index of its preceding node.
826 if ( container.type == CKEDITOR.NODE_ELEMENT && offset > 1 )
827 offset = container.getChild( offset - 1 ).getIndex( true ) + 1;
828
829 // The last step - fix the offset inside text node by adding
830 // lengths of preceding text nodes which will be merged with container.
831 if ( container.type == CKEDITOR.NODE_TEXT ) {
832 var precedingLength = getLengthOfPrecedingTextNodes( container );
833
834 // Normal case - text node is not empty.
835 if ( container.getText() ) {
836 offset += precedingLength;
837
838 // Awful case - the text node is empty and thus will be totally lost.
839 // In this case we are trying to normalize the limit to the left:
840 // * either to the preceding text node,
841 // * or to the "gap" after the preceding element.
842 } else {
843 // Find the closest non-text sibling.
844 var precedingContainer = container.getPrevious( isNotText );
845
846 // If there are any characters on the left, that means that we can anchor
847 // there, because this text node will not be lost.
848 if ( precedingLength ) {
849 offset = precedingLength;
850
851 if ( precedingContainer ) {
852 // The text node is the first node after the closest non-text sibling.
853 container = precedingContainer.getNext();
854 } else {
855 // But if there was no non-text sibling, then the text node is the first child.
856 container = container.getParent().getFirst();
857 }
858
859 // If there are no characters on the left, then anchor after the previous non-text node.
860 // E.g. (see tests for a legend :D):
861 // <b>x</b>(foo)({}bar) -> <b>x</b>[](foo)(bar)
862 } else {
863 container = container.getParent();
864 offset = precedingContainer ? ( precedingContainer.getIndex( true ) + 1 ) : 0;
865 }
866 }
867 }
868
869 limit.container = container;
870 limit.offset = offset;
871 }
872
873 return function( normalized ) {
874 var collapsed = this.collapsed,
875 bmStart = {
876 container: this.startContainer,
877 offset: this.startOffset
878 },
879 bmEnd = {
880 container: this.endContainer,
881 offset: this.endOffset
882 };
883
884 if ( normalized ) {
885 normalize( bmStart );
886
887 if ( !collapsed )
888 normalize( bmEnd );
889 }
890
891 return {
892 start: bmStart.container.getAddress( normalized ),
893 end: collapsed ? null : bmEnd.container.getAddress( normalized ),
894 startOffset: bmStart.offset,
895 endOffset: bmEnd.offset,
896 normalized: normalized,
897 collapsed: collapsed,
898 is2: true // It's a createBookmark2 bookmark.
899 };
900 };
901 } )(),
902
903 /**
904 * Moves this range to the given bookmark. See {@link #createBookmark} and {@link #createBookmark2}.
905 *
906 * If serializable bookmark passed, then its `<span>` markers will be removed.
907 *
908 * @param {Object} bookmark
909 */
910 moveToBookmark: function( bookmark ) {
911 // Created with createBookmark2().
912 if ( bookmark.is2 ) {
913 // Get the start information.
914 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
915 startOffset = bookmark.startOffset;
916
917 // Get the end information.
918 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
919 endOffset = bookmark.endOffset;
920
921 // Set the start boundary.
922 this.setStart( startContainer, startOffset );
923
924 // Set the end boundary. If not available, collapse it.
925 if ( endContainer )
926 this.setEnd( endContainer, endOffset );
927 else
928 this.collapse( true );
929 }
930 // Created with createBookmark().
931 else {
932 var serializable = bookmark.serializable,
933 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
934 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
935
936 // Set the range start at the bookmark start node position.
937 this.setStartBefore( startNode );
938
939 // Remove it, because it may interfere in the setEndBefore call.
940 startNode.remove();
941
942 // Set the range end at the bookmark end node position, or simply
943 // collapse it if it is not available.
944 if ( endNode ) {
945 this.setEndBefore( endNode );
946 endNode.remove();
947 } else {
948 this.collapse( true );
949 }
950 }
951 },
952
953 /**
954 * Returns two nodes which are on the boundaries of this range.
955 *
956 * @returns {Object}
957 * @returns {CKEDITOR.dom.node} return.startNode
958 * @returns {CKEDITOR.dom.node} return.endNode
959 * @todo precise desc/algorithm
960 */
961 getBoundaryNodes: function() {
962 var startNode = this.startContainer,
963 endNode = this.endContainer,
964 startOffset = this.startOffset,
965 endOffset = this.endOffset,
966 childCount;
967
968 if ( startNode.type == CKEDITOR.NODE_ELEMENT ) {
969 childCount = startNode.getChildCount();
970 if ( childCount > startOffset ) {
971 startNode = startNode.getChild( startOffset );
972 } else if ( childCount < 1 ) {
973 startNode = startNode.getPreviousSourceNode();
974 }
975 // startOffset > childCount but childCount is not 0
976 else {
977 // Try to take the node just after the current position.
978 startNode = startNode.$;
979 while ( startNode.lastChild )
980 startNode = startNode.lastChild;
981 startNode = new CKEDITOR.dom.node( startNode );
982
983 // Normally we should take the next node in DFS order. But it
984 // is also possible that we've already reached the end of
985 // document.
986 startNode = startNode.getNextSourceNode() || startNode;
987 }
988 }
989
990 if ( endNode.type == CKEDITOR.NODE_ELEMENT ) {
991 childCount = endNode.getChildCount();
992 if ( childCount > endOffset ) {
993 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
994 } else if ( childCount < 1 ) {
995 endNode = endNode.getPreviousSourceNode();
996 }
997 // endOffset > childCount but childCount is not 0.
998 else {
999 // Try to take the node just before the current position.
1000 endNode = endNode.$;
1001 while ( endNode.lastChild )
1002 endNode = endNode.lastChild;
1003 endNode = new CKEDITOR.dom.node( endNode );
1004 }
1005 }
1006
1007 // Sometimes the endNode will come right before startNode for collapsed
1008 // ranges. Fix it. (#3780)
1009 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
1010 startNode = endNode;
1011
1012 return { startNode: startNode, endNode: endNode };
1013 },
1014
1015 /**
1016 * Find the node which fully contains the range.
1017 *
1018 * @param {Boolean} [includeSelf=false]
1019 * @param {Boolean} [ignoreTextNode=false] Whether ignore {@link CKEDITOR#NODE_TEXT} type.
1020 * @returns {CKEDITOR.dom.element}
1021 */
1022 getCommonAncestor: function( includeSelf, ignoreTextNode ) {
1023 var start = this.startContainer,
1024 end = this.endContainer,
1025 ancestor;
1026
1027 if ( start.equals( end ) ) {
1028 if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 )
1029 ancestor = start.getChild( this.startOffset );
1030 else
1031 ancestor = start;
1032 } else {
1033 ancestor = start.getCommonAncestor( end );
1034 }
1035
1036 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
1037 },
1038
1039 /**
1040 * Transforms the {@link #startContainer} and {@link #endContainer} properties from text
1041 * nodes to element nodes, whenever possible. This is actually possible
1042 * if either of the boundary containers point to a text node, and its
1043 * offset is set to zero, or after the last char in the node.
1044 */
1045 optimize: function() {
1046 var container = this.startContainer;
1047 var offset = this.startOffset;
1048
1049 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1050 if ( !offset )
1051 this.setStartBefore( container );
1052 else if ( offset >= container.getLength() )
1053 this.setStartAfter( container );
1054 }
1055
1056 container = this.endContainer;
1057 offset = this.endOffset;
1058
1059 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1060 if ( !offset )
1061 this.setEndBefore( container );
1062 else if ( offset >= container.getLength() )
1063 this.setEndAfter( container );
1064 }
1065 },
1066
1067 /**
1068 * Move the range out of bookmark nodes if they'd been the container.
1069 */
1070 optimizeBookmark: function() {
1071 var startNode = this.startContainer,
1072 endNode = this.endContainer;
1073
1074 if ( startNode.is && startNode.is( 'span' ) && startNode.data( 'cke-bookmark' ) )
1075 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
1076 if ( endNode && endNode.is && endNode.is( 'span' ) && endNode.data( 'cke-bookmark' ) )
1077 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
1078 },
1079
1080 /**
1081 * @param {Boolean} [ignoreStart=false]
1082 * @param {Boolean} [ignoreEnd=false]
1083 * @todo precise desc/algorithm
1084 */
1085 trim: function( ignoreStart, ignoreEnd ) {
1086 var startContainer = this.startContainer,
1087 startOffset = this.startOffset,
1088 collapsed = this.collapsed;
1089 if ( ( !ignoreStart || collapsed ) && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1090 // If the offset is zero, we just insert the new node before
1091 // the start.
1092 if ( !startOffset ) {
1093 startOffset = startContainer.getIndex();
1094 startContainer = startContainer.getParent();
1095 }
1096 // If the offset is at the end, we'll insert it after the text
1097 // node.
1098 else if ( startOffset >= startContainer.getLength() ) {
1099 startOffset = startContainer.getIndex() + 1;
1100 startContainer = startContainer.getParent();
1101 }
1102 // In other case, we split the text node and insert the new
1103 // node at the split point.
1104 else {
1105 var nextText = startContainer.split( startOffset );
1106
1107 startOffset = startContainer.getIndex() + 1;
1108 startContainer = startContainer.getParent();
1109
1110 // Check all necessity of updating the end boundary.
1111 if ( this.startContainer.equals( this.endContainer ) )
1112 this.setEnd( nextText, this.endOffset - this.startOffset );
1113 else if ( startContainer.equals( this.endContainer ) )
1114 this.endOffset += 1;
1115 }
1116
1117 this.setStart( startContainer, startOffset );
1118
1119 if ( collapsed ) {
1120 this.collapse( true );
1121 return;
1122 }
1123 }
1124
1125 var endContainer = this.endContainer;
1126 var endOffset = this.endOffset;
1127
1128 if ( !( ignoreEnd || collapsed ) && endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1129 // If the offset is zero, we just insert the new node before
1130 // the start.
1131 if ( !endOffset ) {
1132 endOffset = endContainer.getIndex();
1133 endContainer = endContainer.getParent();
1134 }
1135 // If the offset is at the end, we'll insert it after the text
1136 // node.
1137 else if ( endOffset >= endContainer.getLength() ) {
1138 endOffset = endContainer.getIndex() + 1;
1139 endContainer = endContainer.getParent();
1140 }
1141 // In other case, we split the text node and insert the new
1142 // node at the split point.
1143 else {
1144 endContainer.split( endOffset );
1145
1146 endOffset = endContainer.getIndex() + 1;
1147 endContainer = endContainer.getParent();
1148 }
1149
1150 this.setEnd( endContainer, endOffset );
1151 }
1152 },
1153
1154 /**
1155 * Expands the range so that partial units are completely contained.
1156 *
1157 * @param unit {Number} The unit type to expand with.
1158 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
1159 */
1160 enlarge: function( unit, excludeBrs ) {
1161 var leadingWhitespaceRegex = new RegExp( /[^\s\ufeff]/ );
1162
1163 switch ( unit ) {
1164 case CKEDITOR.ENLARGE_INLINE:
1165 var enlargeInlineOnly = 1;
1166
1167 /* falls through */
1168 case CKEDITOR.ENLARGE_ELEMENT:
1169
1170 if ( this.collapsed )
1171 return;
1172
1173 // Get the common ancestor.
1174 var commonAncestor = this.getCommonAncestor();
1175
1176 var boundary = this.root;
1177
1178 // For each boundary
1179 // a. Depending on its position, find out the first node to be checked (a sibling) or,
1180 // if not available, to be enlarge.
1181 // b. Go ahead checking siblings and enlarging the boundary as much as possible until the
1182 // common ancestor is not reached. After reaching the common ancestor, just save the
1183 // enlargeable node to be used later.
1184
1185 var startTop, endTop;
1186
1187 var enlargeable, sibling, commonReached;
1188
1189 // Indicates that the node can be added only if whitespace
1190 // is available before it.
1191 var needsWhiteSpace = false;
1192 var isWhiteSpace;
1193 var siblingText;
1194
1195 // Process the start boundary.
1196
1197 var container = this.startContainer;
1198 var offset = this.startOffset;
1199
1200 if ( container.type == CKEDITOR.NODE_TEXT ) {
1201 if ( offset ) {
1202 // Check if there is any non-space text before the
1203 // offset. Otherwise, container is null.
1204 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
1205
1206 // If we found only whitespace in the node, it
1207 // means that we'll need more whitespace to be able
1208 // to expand. For example, <i> can be expanded in
1209 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1210 needsWhiteSpace = !!container;
1211 }
1212
1213 if ( container ) {
1214 if ( !( sibling = container.getPrevious() ) )
1215 enlargeable = container.getParent();
1216 }
1217 } else {
1218 // If we have offset, get the node preceeding it as the
1219 // first sibling to be checked.
1220 if ( offset )
1221 sibling = container.getChild( offset - 1 ) || container.getLast();
1222
1223 // If there is no sibling, mark the container to be
1224 // enlarged.
1225 if ( !sibling )
1226 enlargeable = container;
1227 }
1228
1229 // Ensures that enlargeable can be indeed enlarged, if not it will be nulled.
1230 enlargeable = getValidEnlargeable( enlargeable );
1231
1232 while ( enlargeable || sibling ) {
1233 if ( enlargeable && !sibling ) {
1234 // If we reached the common ancestor, mark the flag
1235 // for it.
1236 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1237 commonReached = true;
1238
1239 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1240 break;
1241
1242 // If we don't need space or this element breaks
1243 // the line, then enlarge it.
1244 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1245 needsWhiteSpace = false;
1246
1247 // If the common ancestor has been reached,
1248 // we'll not enlarge it immediately, but just
1249 // mark it to be enlarged later if the end
1250 // boundary also enlarges it.
1251 if ( commonReached )
1252 startTop = enlargeable;
1253 else
1254 this.setStartBefore( enlargeable );
1255 }
1256
1257 sibling = enlargeable.getPrevious();
1258 }
1259
1260 // Check all sibling nodes preceeding the enlargeable
1261 // node. The node wil lbe enlarged only if none of them
1262 // blocks it.
1263 while ( sibling ) {
1264 // This flag indicates that this node has
1265 // whitespaces at the end.
1266 isWhiteSpace = false;
1267
1268 if ( sibling.type == CKEDITOR.NODE_COMMENT ) {
1269 sibling = sibling.getPrevious();
1270 continue;
1271 } else if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1272 siblingText = sibling.getText();
1273
1274 if ( leadingWhitespaceRegex.test( siblingText ) )
1275 sibling = null;
1276
1277 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
1278 } else {
1279 // #12221 (Chrome) plus #11111 (Safari).
1280 var offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
1281
1282 // If this is a visible element.
1283 // We need to check for the bookmark attribute because IE insists on
1284 // rendering the display:none nodes we use for bookmarks. (#3363)
1285 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1286 if ( ( sibling.$.offsetWidth > offsetWidth0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1287 // We'll accept it only if we need
1288 // whitespace, and this is an inline
1289 // element with whitespace only.
1290 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1291 // It must contains spaces and inline elements only.
1292
1293 siblingText = sibling.getText();
1294
1295 if ( leadingWhitespaceRegex.test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
1296 sibling = null;
1297 else {
1298 var allChildren = sibling.$.getElementsByTagName( '*' );
1299 for ( var i = 0, child; child = allChildren[ i++ ]; ) {
1300 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1301 sibling = null;
1302 break;
1303 }
1304 }
1305 }
1306
1307 if ( sibling )
1308 isWhiteSpace = !!siblingText.length;
1309 } else {
1310 sibling = null;
1311 }
1312 }
1313 }
1314
1315 // A node with whitespaces has been found.
1316 if ( isWhiteSpace ) {
1317 // Enlarge the last enlargeable node, if we
1318 // were waiting for spaces.
1319 if ( needsWhiteSpace ) {
1320 if ( commonReached )
1321 startTop = enlargeable;
1322 else if ( enlargeable )
1323 this.setStartBefore( enlargeable );
1324 } else {
1325 needsWhiteSpace = true;
1326 }
1327 }
1328
1329 if ( sibling ) {
1330 var next = sibling.getPrevious();
1331
1332 if ( !enlargeable && !next ) {
1333 // Set the sibling as enlargeable, so it's
1334 // parent will be get later outside this while.
1335 enlargeable = sibling;
1336 sibling = null;
1337 break;
1338 }
1339
1340 sibling = next;
1341 } else {
1342 // If sibling has been set to null, then we
1343 // need to stop enlarging.
1344 enlargeable = null;
1345 }
1346 }
1347
1348 if ( enlargeable )
1349 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1350 }
1351
1352 // Process the end boundary. This is basically the same
1353 // code used for the start boundary, with small changes to
1354 // make it work in the oposite side (to the right). This
1355 // makes it difficult to reuse the code here. So, fixes to
1356 // the above code are likely to be replicated here.
1357
1358 container = this.endContainer;
1359 offset = this.endOffset;
1360
1361 // Reset the common variables.
1362 enlargeable = sibling = null;
1363 commonReached = needsWhiteSpace = false;
1364
1365 // Function check if there are only whitespaces from the given starting point
1366 // (startContainer and startOffset) till the end on block.
1367 // Examples ("[" is the start point):
1368 // - <p>foo[ </p> - will return true,
1369 // - <p><b>foo[ </b> </p> - will return true,
1370 // - <p>foo[ bar</p> - will return false,
1371 // - <p><b>foo[ </b>bar</p> - will return false,
1372 // - <p>foo[ <b></b></p> - will return false.
1373 function onlyWhiteSpaces( startContainer, startOffset ) {
1374 // We need to enlarge range if there is white space at the end of the block,
1375 // because it is not displayed in WYSIWYG mode and user can not select it. So
1376 // "<p>foo[bar] </p>" should be changed to "<p>foo[bar ]</p>". On the other hand
1377 // we should do nothing if we are not at the end of the block, so this should not
1378 // be changed: "<p><i>[foo] </i>bar</p>".
1379 var walkerRange = new CKEDITOR.dom.range( boundary );
1380 walkerRange.setStart( startContainer, startOffset );
1381 // The guard will find the end of range so I put boundary here.
1382 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1383
1384 var walker = new CKEDITOR.dom.walker( walkerRange ),
1385 node;
1386
1387 walker.guard = function( node ) {
1388 // Stop if you exit block.
1389 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() );
1390 };
1391
1392 while ( ( node = walker.next() ) ) {
1393 if ( node.type != CKEDITOR.NODE_TEXT ) {
1394 // Stop if you enter to any node (walker.next() will return node only
1395 // it goes out, not if it is go into node).
1396 return false;
1397 } else {
1398 // Trim the first node to startOffset.
1399 if ( node != startContainer )
1400 siblingText = node.getText();
1401 else
1402 siblingText = node.substring( startOffset );
1403
1404 // Check if it is white space.
1405 if ( leadingWhitespaceRegex.test( siblingText ) )
1406 return false;
1407 }
1408 }
1409
1410 return true;
1411 }
1412
1413 if ( container.type == CKEDITOR.NODE_TEXT ) {
1414 // Check if there is only white space after the offset.
1415 if ( CKEDITOR.tools.trim( container.substring( offset ) ).length ) {
1416 // If we found only whitespace in the node, it
1417 // means that we'll need more whitespace to be able
1418 // to expand. For example, <i> can be expanded in
1419 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1420 needsWhiteSpace = true;
1421 } else {
1422 needsWhiteSpace = !container.getLength();
1423
1424 if ( offset == container.getLength() ) {
1425 // If we are at the end of container and this is the last text node,
1426 // we should enlarge end to the parent.
1427 if ( !( sibling = container.getNext() ) )
1428 enlargeable = container.getParent();
1429 } else {
1430 // If we are in the middle on text node and there are only whitespaces
1431 // till the end of block, we should enlarge element.
1432 if ( onlyWhiteSpaces( container, offset ) )
1433 enlargeable = container.getParent();
1434 }
1435 }
1436 } else {
1437 // Get the node right after the boudary to be checked
1438 // first.
1439 sibling = container.getChild( offset );
1440
1441 if ( !sibling )
1442 enlargeable = container;
1443 }
1444
1445 while ( enlargeable || sibling ) {
1446 if ( enlargeable && !sibling ) {
1447 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1448 commonReached = true;
1449
1450 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1451 break;
1452
1453 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1454 needsWhiteSpace = false;
1455
1456 if ( commonReached )
1457 endTop = enlargeable;
1458 else if ( enlargeable )
1459 this.setEndAfter( enlargeable );
1460 }
1461
1462 sibling = enlargeable.getNext();
1463 }
1464
1465 while ( sibling ) {
1466 isWhiteSpace = false;
1467
1468 if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1469 siblingText = sibling.getText();
1470
1471 // Check if there are not whitespace characters till the end of editable.
1472 // If so stop expanding.
1473 if ( !onlyWhiteSpaces( sibling, 0 ) )
1474 sibling = null;
1475
1476 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
1477 } else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) {
1478 // If this is a visible element.
1479 // We need to check for the bookmark attribute because IE insists on
1480 // rendering the display:none nodes we use for bookmarks. (#3363)
1481 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1482 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1483 // We'll accept it only if we need
1484 // whitespace, and this is an inline
1485 // element with whitespace only.
1486 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1487 // It must contains spaces and inline elements only.
1488
1489 siblingText = sibling.getText();
1490
1491 if ( leadingWhitespaceRegex.test( siblingText ) )
1492 sibling = null;
1493 else {
1494 allChildren = sibling.$.getElementsByTagName( '*' );
1495 for ( i = 0; child = allChildren[ i++ ]; ) {
1496 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1497 sibling = null;
1498 break;
1499 }
1500 }
1501 }
1502
1503 if ( sibling )
1504 isWhiteSpace = !!siblingText.length;
1505 } else {
1506 sibling = null;
1507 }
1508 }
1509 } else {
1510 isWhiteSpace = 1;
1511 }
1512
1513 if ( isWhiteSpace ) {
1514 if ( needsWhiteSpace ) {
1515 if ( commonReached )
1516 endTop = enlargeable;
1517 else
1518 this.setEndAfter( enlargeable );
1519 }
1520 }
1521
1522 if ( sibling ) {
1523 next = sibling.getNext();
1524
1525 if ( !enlargeable && !next ) {
1526 enlargeable = sibling;
1527 sibling = null;
1528 break;
1529 }
1530
1531 sibling = next;
1532 } else {
1533 // If sibling has been set to null, then we
1534 // need to stop enlarging.
1535 enlargeable = null;
1536 }
1537 }
1538
1539 if ( enlargeable )
1540 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1541 }
1542
1543 // If the common ancestor can be enlarged by both boundaries, then include it also.
1544 if ( startTop && endTop ) {
1545 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
1546
1547 this.setStartBefore( commonAncestor );
1548 this.setEndAfter( commonAncestor );
1549 }
1550 break;
1551
1552 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
1553 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
1554
1555 // Enlarging the start boundary.
1556 var walkerRange = new CKEDITOR.dom.range( this.root );
1557
1558 boundary = this.root;
1559
1560 walkerRange.setStartAt( boundary, CKEDITOR.POSITION_AFTER_START );
1561 walkerRange.setEnd( this.startContainer, this.startOffset );
1562
1563 var walker = new CKEDITOR.dom.walker( walkerRange ),
1564 blockBoundary, // The node on which the enlarging should stop.
1565 tailBr, // In case BR as block boundary.
1566 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary( ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br: 1 } : null ),
1567 inNonEditable = null,
1568 // Record the encountered 'blockBoundary' for later use.
1569 boundaryGuard = function( node ) {
1570 // We should not check contents of non-editable elements. It may happen
1571 // that inline widget has display:table child which should not block range#enlarge.
1572 // When encoutered non-editable element...
1573 if ( node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'false' ) {
1574 if ( inNonEditable ) {
1575 // ... in which we already were, reset it (because we're leaving it) and return.
1576 if ( inNonEditable.equals( node ) ) {
1577 inNonEditable = null;
1578 return;
1579 }
1580 // ... which we're entering, remember it but check it (no return).
1581 } else {
1582 inNonEditable = node;
1583 }
1584 // When we are in non-editable element, do not check if current node is a block boundary.
1585 } else if ( inNonEditable ) {
1586 return;
1587 }
1588
1589 var retval = notBlockBoundary( node );
1590 if ( !retval )
1591 blockBoundary = node;
1592 return retval;
1593 },
1594 // Record the encounted 'tailBr' for later use.
1595 tailBrGuard = function( node ) {
1596 var retval = boundaryGuard( node );
1597 if ( !retval && node.is && node.is( 'br' ) )
1598 tailBr = node;
1599 return retval;
1600 };
1601
1602 walker.guard = boundaryGuard;
1603
1604 enlargeable = walker.lastBackward();
1605
1606 // It's the body which stop the enlarging if no block boundary found.
1607 blockBoundary = blockBoundary || boundary;
1608
1609 // Start the range either after the end of found block (<p>...</p>[text)
1610 // or at the start of block (<p>[text...), by comparing the document position
1611 // with 'enlargeable' node.
1612 this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() ||
1613 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END );
1614
1615 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
1616 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) {
1617 var theRange = this.clone();
1618 walker = new CKEDITOR.dom.walker( theRange );
1619
1620 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
1621 bookmark = CKEDITOR.dom.walker.bookmark();
1622
1623 walker.evaluator = function( node ) {
1624 return !whitespaces( node ) && !bookmark( node );
1625 };
1626 var previous = walker.previous();
1627 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
1628 return;
1629 }
1630
1631 // Enlarging the end boundary.
1632 // Set up new range and reset all flags (blockBoundary, inNonEditable, tailBr).
1633
1634 walkerRange = this.clone();
1635 walkerRange.collapse();
1636 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1637 walker = new CKEDITOR.dom.walker( walkerRange );
1638
1639 // tailBrGuard only used for on range end.
1640 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? tailBrGuard : boundaryGuard;
1641 blockBoundary = inNonEditable = tailBr = null;
1642
1643 // End the range right before the block boundary node.
1644 enlargeable = walker.lastForward();
1645
1646 // It's the body which stop the enlarging if no block boundary found.
1647 blockBoundary = blockBoundary || boundary;
1648
1649 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
1650 // by comparing the document position with 'enlargeable' node.
1651 this.setEndAt( blockBoundary, ( !enlargeable && this.checkEndOfBlock() ||
1652 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_BEFORE_START );
1653 // We must include the <br> at the end of range if there's
1654 // one and we're expanding list item contents
1655 if ( tailBr ) {
1656 this.setEndAfter( tailBr );
1657 }
1658 }
1659
1660 // Ensures that returned element can be enlarged by selection, null otherwise.
1661 // @param {CKEDITOR.dom.element} enlargeable
1662 // @returns {CKEDITOR.dom.element/null}
1663 function getValidEnlargeable( enlargeable ) {
1664 return enlargeable && enlargeable.type == CKEDITOR.NODE_ELEMENT && enlargeable.hasAttribute( 'contenteditable' ) ?
1665 null : enlargeable;
1666 }
1667 },
1668
1669 /**
1670 * Descrease the range to make sure that boundaries
1671 * always anchor beside text nodes or innermost element.
1672 *
1673 * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}).
1674 *
1675 * * {@link CKEDITOR#SHRINK_ELEMENT} - Shrink the range boundaries to the edge of the innermost element.
1676 * * {@link CKEDITOR#SHRINK_TEXT} - Shrink the range boudaries to anchor by the side of enclosed text
1677 * node, range remains if there's no text nodes on boundaries at all.
1678 *
1679 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
1680 */
1681 shrink: function( mode, selectContents, shrinkOnBlockBoundary ) {
1682 // Unable to shrink a collapsed range.
1683 if ( !this.collapsed ) {
1684 mode = mode || CKEDITOR.SHRINK_TEXT;
1685
1686 var walkerRange = this.clone();
1687
1688 var startContainer = this.startContainer,
1689 endContainer = this.endContainer,
1690 startOffset = this.startOffset,
1691 endOffset = this.endOffset;
1692
1693 // Whether the start/end boundary is moveable.
1694 var moveStart = 1,
1695 moveEnd = 1;
1696
1697 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1698 if ( !startOffset )
1699 walkerRange.setStartBefore( startContainer );
1700 else if ( startOffset >= startContainer.getLength() )
1701 walkerRange.setStartAfter( startContainer );
1702 else {
1703 // Enlarge the range properly to avoid walker making
1704 // DOM changes caused by triming the text nodes later.
1705 walkerRange.setStartBefore( startContainer );
1706 moveStart = 0;
1707 }
1708 }
1709
1710 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1711 if ( !endOffset )
1712 walkerRange.setEndBefore( endContainer );
1713 else if ( endOffset >= endContainer.getLength() )
1714 walkerRange.setEndAfter( endContainer );
1715 else {
1716 walkerRange.setEndAfter( endContainer );
1717 moveEnd = 0;
1718 }
1719 }
1720
1721 var walker = new CKEDITOR.dom.walker( walkerRange ),
1722 isBookmark = CKEDITOR.dom.walker.bookmark();
1723
1724 walker.evaluator = function( node ) {
1725 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
1726 };
1727
1728 var currentElement;
1729 walker.guard = function( node, movingOut ) {
1730 if ( isBookmark( node ) )
1731 return true;
1732
1733 // Stop when we're shrink in element mode while encountering a text node.
1734 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
1735 return false;
1736
1737 // Stop when we've already walked "through" an element.
1738 if ( movingOut && node.equals( currentElement ) )
1739 return false;
1740
1741 if ( shrinkOnBlockBoundary === false && node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() )
1742 return false;
1743
1744 // Stop shrinking when encountering an editable border.
1745 if ( node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'contenteditable' ) )
1746 return false;
1747
1748 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
1749 currentElement = node;
1750
1751 return true;
1752 };
1753
1754 if ( moveStart ) {
1755 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next' ]();
1756 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
1757 }
1758
1759 if ( moveEnd ) {
1760 walker.reset();
1761 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous' ]();
1762 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
1763 }
1764
1765 return !!( moveStart || moveEnd );
1766 }
1767 },
1768
1769 /**
1770 * Inserts a node at the start of the range. The range will be expanded
1771 * the contain the node.
1772 *
1773 * @param {CKEDITOR.dom.node} node
1774 */
1775 insertNode: function( node ) {
1776 this.optimizeBookmark();
1777 this.trim( false, true );
1778
1779 var startContainer = this.startContainer;
1780 var startOffset = this.startOffset;
1781
1782 var nextNode = startContainer.getChild( startOffset );
1783
1784 if ( nextNode )
1785 node.insertBefore( nextNode );
1786 else
1787 startContainer.append( node );
1788
1789 // Check if we need to update the end boundary.
1790 if ( node.getParent() && node.getParent().equals( this.endContainer ) )
1791 this.endOffset++;
1792
1793 // Expand the range to embrace the new node.
1794 this.setStartBefore( node );
1795 },
1796
1797 /**
1798 * Moves the range to given position according to specified node.
1799 *
1800 * // HTML: <p>Foo <b>bar</b></p>
1801 * range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START );
1802 * // Range will be moved to: <p>Foo ^<b>bar</b></p>
1803 *
1804 * See also {@link #setStartAt} and {@link #setEndAt}.
1805 *
1806 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
1807 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1808 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1809 * {@link CKEDITOR#POSITION_AFTER_END}.
1810 */
1811 moveToPosition: function( node, position ) {
1812 this.setStartAt( node, position );
1813 this.collapse( true );
1814 },
1815
1816 /**
1817 * Moves the range to the exact position of the specified range.
1818 *
1819 * @param {CKEDITOR.dom.range} range
1820 */
1821 moveToRange: function( range ) {
1822 this.setStart( range.startContainer, range.startOffset );
1823 this.setEnd( range.endContainer, range.endOffset );
1824 },
1825
1826 /**
1827 * Select nodes content. Range will start and end inside this node.
1828 *
1829 * @param {CKEDITOR.dom.node} node
1830 */
1831 selectNodeContents: function( node ) {
1832 this.setStart( node, 0 );
1833 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
1834 },
1835
1836 /**
1837 * Sets the start position of a range.
1838 *
1839 * @param {CKEDITOR.dom.node} startNode The node to start the range.
1840 * @param {Number} startOffset An integer greater than or equal to zero
1841 * representing the offset for the start of the range from the start
1842 * of `startNode`.
1843 */
1844 setStart: function( startNode, startOffset ) {
1845 // W3C requires a check for the new position. If it is after the end
1846 // boundary, the range should be collapsed to the new start. It seams
1847 // we will not need this check for our use of this class so we can
1848 // ignore it for now.
1849
1850 // Fixing invalid range start inside dtd empty elements.
1851 if ( startNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ startNode.getName() ] )
1852 startOffset = startNode.getIndex(), startNode = startNode.getParent();
1853
1854 this._setStartContainer( startNode );
1855 this.startOffset = startOffset;
1856
1857 if ( !this.endContainer ) {
1858 this._setEndContainer( startNode );
1859 this.endOffset = startOffset;
1860 }
1861
1862 updateCollapsed( this );
1863 },
1864
1865 /**
1866 * Sets the end position of a Range.
1867 *
1868 * @param {CKEDITOR.dom.node} endNode The node to end the range.
1869 * @param {Number} endOffset An integer greater than or equal to zero
1870 * representing the offset for the end of the range from the start
1871 * of `endNode`.
1872 */
1873 setEnd: function( endNode, endOffset ) {
1874 // W3C requires a check for the new position. If it is before the start
1875 // boundary, the range should be collapsed to the new end. It seams we
1876 // will not need this check for our use of this class so we can ignore
1877 // it for now.
1878
1879 // Fixing invalid range end inside dtd empty elements.
1880 if ( endNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ endNode.getName() ] )
1881 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
1882
1883 this._setEndContainer( endNode );
1884 this.endOffset = endOffset;
1885
1886 if ( !this.startContainer ) {
1887 this._setStartContainer( endNode );
1888 this.startOffset = endOffset;
1889 }
1890
1891 updateCollapsed( this );
1892 },
1893
1894 /**
1895 * Sets start of this range after the specified node.
1896 *
1897 * // Range: <p>foo<b>bar</b>^</p>
1898 * range.setStartAfter( textFoo );
1899 * // The range will be changed to:
1900 * // <p>foo[<b>bar</b>]</p>
1901 *
1902 * @param {CKEDITOR.dom.node} node
1903 */
1904 setStartAfter: function( node ) {
1905 this.setStart( node.getParent(), node.getIndex() + 1 );
1906 },
1907
1908 /**
1909 * Sets start of this range after the specified node.
1910 *
1911 * // Range: <p>foo<b>bar</b>^</p>
1912 * range.setStartBefore( elB );
1913 * // The range will be changed to:
1914 * // <p>foo[<b>bar</b>]</p>
1915 *
1916 * @param {CKEDITOR.dom.node} node
1917 */
1918 setStartBefore: function( node ) {
1919 this.setStart( node.getParent(), node.getIndex() );
1920 },
1921
1922 /**
1923 * Sets end of this range after the specified node.
1924 *
1925 * // Range: <p>foo^<b>bar</b></p>
1926 * range.setEndAfter( elB );
1927 * // The range will be changed to:
1928 * // <p>foo[<b>bar</b>]</p>
1929 *
1930 * @param {CKEDITOR.dom.node} node
1931 */
1932 setEndAfter: function( node ) {
1933 this.setEnd( node.getParent(), node.getIndex() + 1 );
1934 },
1935
1936 /**
1937 * Sets end of this range before the specified node.
1938 *
1939 * // Range: <p>^foo<b>bar</b></p>
1940 * range.setStartAfter( textBar );
1941 * // The range will be changed to:
1942 * // <p>[foo<b>]bar</b></p>
1943 *
1944 * @param {CKEDITOR.dom.node} node
1945 */
1946 setEndBefore: function( node ) {
1947 this.setEnd( node.getParent(), node.getIndex() );
1948 },
1949
1950 /**
1951 * Moves the start of this range to given position according to specified node.
1952 *
1953 * // HTML: <p>Foo <b>bar</b>^</p>
1954 * range.setStartAt( elB, CKEDITOR.POSITION_AFTER_START );
1955 * // The range will be changed to:
1956 * // <p>Foo <b>[bar</b>]</p>
1957 *
1958 * See also {@link #setEndAt} and {@link #moveToPosition}.
1959 *
1960 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
1961 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1962 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1963 * {@link CKEDITOR#POSITION_AFTER_END}.
1964 */
1965 setStartAt: function( node, position ) {
1966 switch ( position ) {
1967 case CKEDITOR.POSITION_AFTER_START:
1968 this.setStart( node, 0 );
1969 break;
1970
1971 case CKEDITOR.POSITION_BEFORE_END:
1972 if ( node.type == CKEDITOR.NODE_TEXT )
1973 this.setStart( node, node.getLength() );
1974 else
1975 this.setStart( node, node.getChildCount() );
1976 break;
1977
1978 case CKEDITOR.POSITION_BEFORE_START:
1979 this.setStartBefore( node );
1980 break;
1981
1982 case CKEDITOR.POSITION_AFTER_END:
1983 this.setStartAfter( node );
1984 }
1985
1986 updateCollapsed( this );
1987 },
1988
1989 /**
1990 * Moves the end of this range to given position according to specified node.
1991 *
1992 * // HTML: <p>^Foo <b>bar</b></p>
1993 * range.setEndAt( textBar, CKEDITOR.POSITION_BEFORE_START );
1994 * // The range will be changed to:
1995 * // <p>[Foo <b>]bar</b></p>
1996 *
1997 * See also {@link #setStartAt} and {@link #moveToPosition}.
1998 *
1999 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2000 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2001 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2002 * {@link CKEDITOR#POSITION_AFTER_END}.
2003 */
2004 setEndAt: function( node, position ) {
2005 switch ( position ) {
2006 case CKEDITOR.POSITION_AFTER_START:
2007 this.setEnd( node, 0 );
2008 break;
2009
2010 case CKEDITOR.POSITION_BEFORE_END:
2011 if ( node.type == CKEDITOR.NODE_TEXT )
2012 this.setEnd( node, node.getLength() );
2013 else
2014 this.setEnd( node, node.getChildCount() );
2015 break;
2016
2017 case CKEDITOR.POSITION_BEFORE_START:
2018 this.setEndBefore( node );
2019 break;
2020
2021 case CKEDITOR.POSITION_AFTER_END:
2022 this.setEndAfter( node );
2023 }
2024
2025 updateCollapsed( this );
2026 },
2027
2028 /**
2029 * Wraps inline content found around the range's start or end boundary
2030 * with a block element.
2031 *
2032 * // Assuming the following range:
2033 * // <h1>foo</h1>ba^r<br />bom<p>foo</p>
2034 * // The result of executing:
2035 * range.fixBlock( true, 'p' );
2036 * // will be:
2037 * // <h1>foo</h1><p>ba^r<br />bom</p><p>foo</p>
2038 *
2039 * Non-collapsed range:
2040 *
2041 * // Assuming the following range:
2042 * // ba[r<p>foo</p>bo]m
2043 * // The result of executing:
2044 * range.fixBlock( false, 'p' );
2045 * // will be:
2046 * // ba[r<p>foo</p><p>bo]m</p>
2047 *
2048 * @param {Boolean} isStart Whether the start or end boundary of a range should be checked.
2049 * @param {String} blockTag The name of a block element in which content will be wrapped.
2050 * For example: `'p'`.
2051 * @returns {CKEDITOR.dom.element} Created block wrapper.
2052 */
2053 fixBlock: function( isStart, blockTag ) {
2054 var bookmark = this.createBookmark(),
2055 fixedBlock = this.document.createElement( blockTag );
2056
2057 this.collapse( isStart );
2058
2059 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
2060
2061 this.extractContents().appendTo( fixedBlock );
2062 fixedBlock.trim();
2063
2064 this.insertNode( fixedBlock );
2065
2066 // Bogus <br> could already exist in the range's container before fixBlock() was called. In such case it was
2067 // extracted and appended to the fixBlock. However, we are not sure that it's at the end of
2068 // the fixedBlock, because of FF's terrible bug. When creating a bookmark in an empty editable
2069 // FF moves the bogus <br> before that bookmark (<editable><br /><bm />[]</editable>).
2070 // So even if the initial range was placed before the bogus <br>, after creating the bookmark it
2071 // is placed before the bookmark.
2072 // Fortunately, getBogus() is able to skip the bookmark so it finds the bogus <br> in this case.
2073 // We remove incorrectly placed one and add a brand new one. (#13001)
2074 var bogus = fixedBlock.getBogus();
2075 if ( bogus ) {
2076 bogus.remove();
2077 }
2078 fixedBlock.appendBogus();
2079
2080 this.moveToBookmark( bookmark );
2081
2082 return fixedBlock;
2083 },
2084
2085 /**
2086 * @todo
2087 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result blocks.
2088 */
2089 splitBlock: function( blockTag, cloneId ) {
2090 var startPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ),
2091 endPath = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2092
2093 var startBlockLimit = startPath.blockLimit,
2094 endBlockLimit = endPath.blockLimit;
2095
2096 var startBlock = startPath.block,
2097 endBlock = endPath.block;
2098
2099 var elementPath = null;
2100 // Do nothing if the boundaries are in different block limits.
2101 if ( !startBlockLimit.equals( endBlockLimit ) )
2102 return null;
2103
2104 // Get or fix current blocks.
2105 if ( blockTag != 'br' ) {
2106 if ( !startBlock ) {
2107 startBlock = this.fixBlock( true, blockTag );
2108 endBlock = new CKEDITOR.dom.elementPath( this.endContainer, this.root ).block;
2109 }
2110
2111 if ( !endBlock )
2112 endBlock = this.fixBlock( false, blockTag );
2113 }
2114
2115 // Get the range position.
2116 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
2117 isEndOfBlock = endBlock && this.checkEndOfBlock();
2118
2119 // Delete the current contents.
2120 // TODO: Why is 2.x doing CheckIsEmpty()?
2121 this.deleteContents();
2122
2123 if ( startBlock && startBlock.equals( endBlock ) ) {
2124 if ( isEndOfBlock ) {
2125 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2126 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
2127 endBlock = null;
2128 } else if ( isStartOfBlock ) {
2129 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2130 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
2131 startBlock = null;
2132 } else {
2133 endBlock = this.splitElement( startBlock, cloneId || false );
2134
2135 // In Gecko, the last child node must be a bogus <br>.
2136 // Note: bogus <br> added under <ul> or <ol> would cause
2137 // lists to be incorrectly rendered.
2138 if ( !startBlock.is( 'ul', 'ol' ) )
2139 startBlock.appendBogus();
2140 }
2141 }
2142
2143 return {
2144 previousBlock: startBlock,
2145 nextBlock: endBlock,
2146 wasStartOfBlock: isStartOfBlock,
2147 wasEndOfBlock: isEndOfBlock,
2148 elementPath: elementPath
2149 };
2150 },
2151
2152 /**
2153 * Branch the specified element from the collapsed range position and
2154 * place the caret between the two result branches.
2155 *
2156 * **Note:** The range must be collapsed and been enclosed by this element.
2157 *
2158 * @param {CKEDITOR.dom.element} element
2159 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result elements.
2160 * @returns {CKEDITOR.dom.element} Root element of the new branch after the split.
2161 */
2162 splitElement: function( toSplit, cloneId ) {
2163 if ( !this.collapsed )
2164 return null;
2165
2166 // Extract the contents of the block from the selection point to the end
2167 // of its contents.
2168 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
2169 var documentFragment = this.extractContents( false, cloneId || false );
2170
2171 // Duplicate the element after it.
2172 var clone = toSplit.clone( false, cloneId || false );
2173
2174 // Place the extracted contents into the duplicated element.
2175 documentFragment.appendTo( clone );
2176 clone.insertAfter( toSplit );
2177 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
2178 return clone;
2179 },
2180
2181 /**
2182 * Recursively remove any empty path blocks at the range boundary.
2183 *
2184 * @method
2185 * @param {Boolean} atEnd Removal to perform at the end boundary,
2186 * otherwise to perform at the start.
2187 */
2188 removeEmptyBlocksAtEnd: ( function() {
2189
2190 var whitespace = CKEDITOR.dom.walker.whitespaces(),
2191 bookmark = CKEDITOR.dom.walker.bookmark( false );
2192
2193 function childEval( parent ) {
2194 return function( node ) {
2195 // Whitespace, bookmarks, empty inlines.
2196 if ( whitespace( node ) || bookmark( node ) ||
2197 node.type == CKEDITOR.NODE_ELEMENT &&
2198 node.isEmptyInlineRemoveable() ) {
2199 return false;
2200 } else if ( parent.is( 'table' ) && node.is( 'caption' ) ) {
2201 return false;
2202 }
2203
2204 return true;
2205 };
2206 }
2207
2208 return function( atEnd ) {
2209
2210 var bm = this.createBookmark();
2211 var path = this[ atEnd ? 'endPath' : 'startPath' ]();
2212 var block = path.block || path.blockLimit, parent;
2213
2214 // Remove any childless block, including list and table.
2215 while ( block && !block.equals( path.root ) &&
2216 !block.getFirst( childEval( block ) ) ) {
2217 parent = block.getParent();
2218 this[ atEnd ? 'setEndAt' : 'setStartAt' ]( block, CKEDITOR.POSITION_AFTER_END );
2219 block.remove( 1 );
2220 block = parent;
2221 }
2222
2223 this.moveToBookmark( bm );
2224 };
2225
2226 } )(),
2227
2228 /**
2229 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #startContainer}.
2230 *
2231 * @returns {CKEDITOR.dom.elementPath}
2232 */
2233 startPath: function() {
2234 return new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2235 },
2236
2237 /**
2238 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #endContainer}.
2239 *
2240 * @returns {CKEDITOR.dom.elementPath}
2241 */
2242 endPath: function() {
2243 return new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2244 },
2245
2246 /**
2247 * Check whether a range boundary is at the inner boundary of a given
2248 * element.
2249 *
2250 * @param {CKEDITOR.dom.element} element The target element to check.
2251 * @param {Number} checkType The boundary to check for both the range
2252 * and the element. It can be {@link CKEDITOR#START} or {@link CKEDITOR#END}.
2253 * @returns {Boolean} `true` if the range boundary is at the inner
2254 * boundary of the element.
2255 */
2256 checkBoundaryOfElement: function( element, checkType ) {
2257 var checkStart = ( checkType == CKEDITOR.START );
2258
2259 // Create a copy of this range, so we can manipulate it for our checks.
2260 var walkerRange = this.clone();
2261
2262 // Collapse the range at the proper size.
2263 walkerRange.collapse( checkStart );
2264
2265 // Expand the range to element boundary.
2266 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
2267
2268 // Create the walker, which will check if we have anything useful
2269 // in the range.
2270 var walker = new CKEDITOR.dom.walker( walkerRange );
2271 walker.evaluator = elementBoundaryEval( checkStart );
2272
2273 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
2274 },
2275
2276 /**
2277 * **Note:** Calls to this function may produce changes to the DOM. The range may
2278 * be updated to reflect such changes.
2279 *
2280 * @returns {Boolean}
2281 * @todo
2282 */
2283 checkStartOfBlock: function() {
2284 var startContainer = this.startContainer,
2285 startOffset = this.startOffset;
2286
2287 // [IE] Special handling for range start in text with a leading NBSP,
2288 // we it to be isolated, for bogus check.
2289 if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) {
2290 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
2291 if ( nbspRegExp.test( textBefore ) )
2292 this.trim( 0, 1 );
2293 }
2294
2295 // Antecipate the trim() call here, so the walker will not make
2296 // changes to the DOM, which would not get reflected into this
2297 // range otherwise.
2298 this.trim();
2299
2300 // We need to grab the block element holding the start boundary, so
2301 // let's use an element path for it.
2302 var path = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2303
2304 // Creates a range starting at the block start until the range start.
2305 var walkerRange = this.clone();
2306 walkerRange.collapse( true );
2307 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
2308
2309 var walker = new CKEDITOR.dom.walker( walkerRange );
2310 walker.evaluator = getCheckStartEndBlockEvalFunction();
2311
2312 return walker.checkBackward();
2313 },
2314
2315 /**
2316 * **Note:** Calls to this function may produce changes to the DOM. The range may
2317 * be updated to reflect such changes.
2318 *
2319 * @returns {Boolean}
2320 * @todo
2321 */
2322 checkEndOfBlock: function() {
2323 var endContainer = this.endContainer,
2324 endOffset = this.endOffset;
2325
2326 // [IE] Special handling for range end in text with a following NBSP,
2327 // we it to be isolated, for bogus check.
2328 if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT ) {
2329 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
2330 if ( nbspRegExp.test( textAfter ) )
2331 this.trim( 1, 0 );
2332 }
2333
2334 // Antecipate the trim() call here, so the walker will not make
2335 // changes to the DOM, which would not get reflected into this
2336 // range otherwise.
2337 this.trim();
2338
2339 // We need to grab the block element holding the start boundary, so
2340 // let's use an element path for it.
2341 var path = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2342
2343 // Creates a range starting at the block start until the range start.
2344 var walkerRange = this.clone();
2345 walkerRange.collapse( false );
2346 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
2347
2348 var walker = new CKEDITOR.dom.walker( walkerRange );
2349 walker.evaluator = getCheckStartEndBlockEvalFunction();
2350
2351 return walker.checkForward();
2352 },
2353
2354 /**
2355 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.
2356 *
2357 * @param {Function} evaluator Function used as the walker's evaluator.
2358 * @param {Function} [guard] Function used as the walker's guard.
2359 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2360 * default to the root editable if not defined.
2361 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2362 */
2363 getPreviousNode: function( evaluator, guard, boundary ) {
2364 var walkerRange = this.clone();
2365 walkerRange.collapse( 1 );
2366 walkerRange.setStartAt( boundary || this.root, CKEDITOR.POSITION_AFTER_START );
2367
2368 var walker = new CKEDITOR.dom.walker( walkerRange );
2369 walker.evaluator = evaluator;
2370 walker.guard = guard;
2371 return walker.previous();
2372 },
2373
2374 /**
2375 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.
2376 *
2377 * @param {Function} evaluator Function used as the walker's evaluator.
2378 * @param {Function} [guard] Function used as the walker's guard.
2379 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2380 * default to the root editable if not defined.
2381 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2382 */
2383 getNextNode: function( evaluator, guard, boundary ) {
2384 var walkerRange = this.clone();
2385 walkerRange.collapse();
2386 walkerRange.setEndAt( boundary || this.root, CKEDITOR.POSITION_BEFORE_END );
2387
2388 var walker = new CKEDITOR.dom.walker( walkerRange );
2389 walker.evaluator = evaluator;
2390 walker.guard = guard;
2391 return walker.next();
2392 },
2393
2394 /**
2395 * Check if elements at which the range boundaries anchor are read-only,
2396 * with respect to `contenteditable` attribute.
2397 *
2398 * @returns {Boolean}
2399 */
2400 checkReadOnly: ( function() {
2401 function checkNodesEditable( node, anotherEnd ) {
2402 while ( node ) {
2403 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
2404 if ( node.getAttribute( 'contentEditable' ) == 'false' && !node.data( 'cke-editable' ) )
2405 return 0;
2406
2407 // Range enclosed entirely in an editable element.
2408 else if ( node.is( 'html' ) || node.getAttribute( 'contentEditable' ) == 'true' && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
2409 break;
2410
2411 }
2412 node = node.getParent();
2413 }
2414
2415 return 1;
2416 }
2417
2418 return function() {
2419 var startNode = this.startContainer,
2420 endNode = this.endContainer;
2421
2422 // Check if elements path at both boundaries are editable.
2423 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
2424 };
2425 } )(),
2426
2427 /**
2428 * Moves the range boundaries to the first/end editing point inside an
2429 * element.
2430 *
2431 * For example, in an element tree like
2432 * `<p><b><i></i></b> Text</p>`, the start editing point is
2433 * `<p><b><i>^</i></b> Text</p>` (inside `<i>`).
2434 *
2435 * @param {CKEDITOR.dom.element} el The element into which look for the
2436 * editing spot.
2437 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
2438 * @returns {Boolean} Whether range was moved.
2439 */
2440 moveToElementEditablePosition: function( el, isMoveToEnd ) {
2441
2442 function nextDFS( node, childOnly ) {
2443 var next;
2444
2445 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )
2446 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( notIgnoredEval );
2447
2448 if ( !childOnly && !next )
2449 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( notIgnoredEval );
2450
2451 return next;
2452 }
2453
2454 // Handle non-editable element e.g. HR.
2455 if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) {
2456 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2457 return true;
2458 }
2459
2460 var found = 0;
2461
2462 while ( el ) {
2463 // Stop immediately if we've found a text node.
2464 if ( el.type == CKEDITOR.NODE_TEXT ) {
2465 // Put cursor before block filler.
2466 if ( isMoveToEnd && this.endContainer && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )
2467 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2468 else
2469 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2470 found = 1;
2471 break;
2472 }
2473
2474 // If an editable element is found, move inside it, but not stop the searching.
2475 if ( el.type == CKEDITOR.NODE_ELEMENT ) {
2476 if ( el.isEditable() ) {
2477 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_START );
2478 found = 1;
2479 }
2480 // Put cursor before padding block br.
2481 else if ( isMoveToEnd && el.is( 'br' ) && this.endContainer && this.checkEndOfBlock() )
2482 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2483 // Special case - non-editable block. Select entire element, because it does not make sense
2484 // to place collapsed selection next to it, because browsers can't handle that.
2485 else if ( el.getAttribute( 'contenteditable' ) == 'false' && el.is( CKEDITOR.dtd.$block ) ) {
2486 this.setStartBefore( el );
2487 this.setEndAfter( el );
2488 return true;
2489 }
2490 }
2491
2492 el = nextDFS( el, found );
2493 }
2494
2495 return !!found;
2496 },
2497
2498 /**
2499 * Moves the range boundaries to the closest editing point after/before an
2500 * element or the current range position (depends on whether the element was specified).
2501 *
2502 * For example, if the start element has `id="start"`,
2503 * `<p><b>foo</b><span id="start">start</start></p>`, the closest previous editing point is
2504 * `<p><b>foo</b>^<span id="start">start</start></p>` (between `<b>` and `<span>`).
2505 *
2506 * See also: {@link #moveToElementEditablePosition}.
2507 *
2508 * @since 4.3
2509 * @param {CKEDITOR.dom.element} [element] The starting element. If not specified, the current range
2510 * position will be used.
2511 * @param {Boolean} [isMoveForward] Whether move to the end of editable. Otherwise, look back.
2512 * @returns {Boolean} Whether the range was moved.
2513 */
2514 moveToClosestEditablePosition: function( element, isMoveForward ) {
2515 // We don't want to modify original range if there's no editable position.
2516 var range,
2517 found = 0,
2518 sibling,
2519 isElement,
2520 positions = [ CKEDITOR.POSITION_AFTER_END, CKEDITOR.POSITION_BEFORE_START ];
2521
2522 if ( element ) {
2523 // Set collapsed range at one of ends of element.
2524 // Can't clone this range, because this range might not be yet positioned (no containers => errors).
2525 range = new CKEDITOR.dom.range( this.root );
2526 range.moveToPosition( element, positions[ isMoveForward ? 0 : 1 ] );
2527 } else {
2528 range = this.clone();
2529 }
2530
2531 // Start element isn't a block, so we can automatically place range
2532 // next to it.
2533 if ( element && !element.is( CKEDITOR.dtd.$block ) )
2534 found = 1;
2535 else {
2536 // Look for first node that fulfills eval function and place range next to it.
2537 sibling = range[ isMoveForward ? 'getNextEditableNode' : 'getPreviousEditableNode' ]();
2538 if ( sibling ) {
2539 found = 1;
2540 isElement = sibling.type == CKEDITOR.NODE_ELEMENT;
2541
2542 // Special case - eval accepts block element only if it's a non-editable block,
2543 // which we want to select, not place collapsed selection next to it (which browsers
2544 // can't handle).
2545 if ( isElement && sibling.is( CKEDITOR.dtd.$block ) && sibling.getAttribute( 'contenteditable' ) == 'false' ) {
2546 range.setStartAt( sibling, CKEDITOR.POSITION_BEFORE_START );
2547 range.setEndAt( sibling, CKEDITOR.POSITION_AFTER_END );
2548 }
2549 // Handle empty blocks which can be selection containers on old IEs.
2550 else if ( !CKEDITOR.env.needsBrFiller && isElement && sibling.is( CKEDITOR.dom.walker.validEmptyBlockContainers ) ) {
2551 range.setEnd( sibling, 0 );
2552 range.collapse();
2553 } else {
2554 range.moveToPosition( sibling, positions[ isMoveForward ? 1 : 0 ] );
2555 }
2556 }
2557 }
2558
2559 if ( found )
2560 this.moveToRange( range );
2561
2562 return !!found;
2563 },
2564
2565 /**
2566 * See {@link #moveToElementEditablePosition}.
2567 *
2568 * @returns {Boolean} Whether range was moved.
2569 */
2570 moveToElementEditStart: function( target ) {
2571 return this.moveToElementEditablePosition( target );
2572 },
2573
2574 /**
2575 * See {@link #moveToElementEditablePosition}.
2576 *
2577 * @returns {Boolean} Whether range was moved.
2578 */
2579 moveToElementEditEnd: function( target ) {
2580 return this.moveToElementEditablePosition( target, true );
2581 },
2582
2583 /**
2584 * Get the single node enclosed within the range if there's one.
2585 *
2586 * @returns {CKEDITOR.dom.node}
2587 */
2588 getEnclosedNode: function() {
2589 var walkerRange = this.clone();
2590
2591 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
2592 walkerRange.optimize();
2593 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
2594 return null;
2595
2596 var walker = new CKEDITOR.dom.walker( walkerRange ),
2597 isNotBookmarks = CKEDITOR.dom.walker.bookmark( false, true ),
2598 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true );
2599
2600 walker.evaluator = function( node ) {
2601 return isNotWhitespaces( node ) && isNotBookmarks( node );
2602 };
2603 var node = walker.next();
2604 walker.reset();
2605 return node && node.equals( walker.previous() ) ? node : null;
2606 },
2607
2608 /**
2609 * Get the node adjacent to the range start or {@link #startContainer}.
2610 *
2611 * @returns {CKEDITOR.dom.node}
2612 */
2613 getTouchedStartNode: function() {
2614 var container = this.startContainer;
2615
2616 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2617 return container;
2618
2619 return container.getChild( this.startOffset ) || container;
2620 },
2621
2622 /**
2623 * Get the node adjacent to the range end or {@link #endContainer}.
2624 *
2625 * @returns {CKEDITOR.dom.node}
2626 */
2627 getTouchedEndNode: function() {
2628 var container = this.endContainer;
2629
2630 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2631 return container;
2632
2633 return container.getChild( this.endOffset - 1 ) || container;
2634 },
2635
2636 /**
2637 * Gets next node which can be a container of a selection.
2638 * This methods mimics a behavior of right/left arrow keys in case of
2639 * collapsed selection. It does not return an exact position (with offset) though,
2640 * but just a selection's container.
2641 *
2642 * Note: use this method on a collapsed range.
2643 *
2644 * @since 4.3
2645 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2646 */
2647 getNextEditableNode: getNextEditableNode(),
2648
2649 /**
2650 * See {@link #getNextEditableNode}.
2651 *
2652 * @since 4.3
2653 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2654 */
2655 getPreviousEditableNode: getNextEditableNode( 1 ),
2656
2657 /**
2658 * Scrolls the start of current range into view.
2659 */
2660 scrollIntoView: function() {
2661
2662 // The reference element contains a zero-width space to avoid
2663 // a premature removal. The view is to be scrolled with respect
2664 // to this element.
2665 var reference = new CKEDITOR.dom.element.createFromHtml( '<span>&nbsp;</span>', this.document ),
2666 afterCaretNode, startContainerText, isStartText;
2667
2668 var range = this.clone();
2669
2670 // Work with the range to obtain a proper caret position.
2671 range.optimize();
2672
2673 // Currently in a text node, so we need to split it into two
2674 // halves and put the reference between.
2675 if ( isStartText = range.startContainer.type == CKEDITOR.NODE_TEXT ) {
2676 // Keep the original content. It will be restored.
2677 startContainerText = range.startContainer.getText();
2678
2679 // Split the startContainer at the this position.
2680 afterCaretNode = range.startContainer.split( range.startOffset );
2681
2682 // Insert the reference between two text nodes.
2683 reference.insertAfter( range.startContainer );
2684 }
2685
2686 // If not in a text node, simply insert the reference into the range.
2687 else {
2688 range.insertNode( reference );
2689 }
2690
2691 // Scroll with respect to the reference element.
2692 reference.scrollIntoView();
2693
2694 // Get rid of split parts if "in a text node" case.
2695 // Revert the original text of the startContainer.
2696 if ( isStartText ) {
2697 range.startContainer.setText( startContainerText );
2698 afterCaretNode.remove();
2699 }
2700
2701 // Get rid of the reference node. It is no longer necessary.
2702 reference.remove();
2703 },
2704
2705 /**
2706 * Setter for the {@link #startContainer}.
2707 *
2708 * @since 4.4.6
2709 * @private
2710 * @param {CKEDITOR.dom.element} startContainer
2711 */
2712 _setStartContainer: function( startContainer ) {
2713 // %REMOVE_START%
2714 var isRootAscendantOrSelf = this.root.equals( startContainer ) || this.root.contains( startContainer );
2715
2716 if ( !isRootAscendantOrSelf ) {
2717 CKEDITOR.warn( 'range-startcontainer', { startContainer: startContainer, root: this.root } );
2718 }
2719 // %REMOVE_END%
2720 this.startContainer = startContainer;
2721 },
2722
2723 /**
2724 * Setter for the {@link #endContainer}.
2725 *
2726 * @since 4.4.6
2727 * @private
2728 * @param {CKEDITOR.dom.element} endContainer
2729 */
2730 _setEndContainer: function( endContainer ) {
2731 // %REMOVE_START%
2732 var isRootAscendantOrSelf = this.root.equals( endContainer ) || this.root.contains( endContainer );
2733
2734 if ( !isRootAscendantOrSelf ) {
2735 CKEDITOR.warn( 'range-endcontainer', { endContainer: endContainer, root: this.root } );
2736 }
2737 // %REMOVE_END%
2738 this.endContainer = endContainer;
2739 }
2740 };
2741
2742
2743} )();
2744
2745/**
2746 * Indicates a position after start of a node.
2747 *
2748 * // When used according to an element:
2749 * // <element>^contents</element>
2750 *
2751 * // When used according to a text node:
2752 * // "^text" (range is anchored in the text node)
2753 *
2754 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2755 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2756 *
2757 * @readonly
2758 * @member CKEDITOR
2759 * @property {Number} [=1]
2760 */
2761CKEDITOR.POSITION_AFTER_START = 1;
2762
2763/**
2764 * Indicates a position before end of a node.
2765 *
2766 * // When used according to an element:
2767 * // <element>contents^</element>
2768 *
2769 * // When used according to a text node:
2770 * // "text^" (range is anchored in the text node)
2771 *
2772 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2773 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2774 *
2775 * @readonly
2776 * @member CKEDITOR
2777 * @property {Number} [=2]
2778 */
2779CKEDITOR.POSITION_BEFORE_END = 2;
2780
2781/**
2782 * Indicates a position before start of a node.
2783 *
2784 * // When used according to an element:
2785 * // ^<element>contents</element> (range is anchored in element's parent)
2786 *
2787 * // When used according to a text node:
2788 * // ^"text" (range is anchored in text node's parent)
2789 *
2790 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2791 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2792 *
2793 * @readonly
2794 * @member CKEDITOR
2795 * @property {Number} [=3]
2796 */
2797CKEDITOR.POSITION_BEFORE_START = 3;
2798
2799/**
2800 * Indicates a position after end of a node.
2801 *
2802 * // When used according to an element:
2803 * // <element>contents</element>^ (range is anchored in element's parent)
2804 *
2805 * // When used according to a text node:
2806 * // "text"^ (range is anchored in text node's parent)
2807 *
2808 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2809 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2810 *
2811 * @readonly
2812 * @member CKEDITOR
2813 * @property {Number} [=4]
2814 */
2815CKEDITOR.POSITION_AFTER_END = 4;
2816
2817CKEDITOR.ENLARGE_ELEMENT = 1;
2818CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
2819CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
2820CKEDITOR.ENLARGE_INLINE = 4;
2821
2822// Check boundary types.
2823
2824/**
2825 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2826 *
2827 * @readonly
2828 * @member CKEDITOR
2829 * @property {Number} [=1]
2830 */
2831CKEDITOR.START = 1;
2832
2833/**
2834 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2835 *
2836 * @readonly
2837 * @member CKEDITOR
2838 * @property {Number} [=2]
2839 */
2840CKEDITOR.END = 2;
2841
2842// Shrink range types.
2843
2844/**
2845 * See {@link CKEDITOR.dom.range#shrink}.
2846 *
2847 * @readonly
2848 * @member CKEDITOR
2849 * @property {Number} [=1]
2850 */
2851CKEDITOR.SHRINK_ELEMENT = 1;
2852
2853/**
2854 * See {@link CKEDITOR.dom.range#shrink}.
2855 *
2856 * @readonly
2857 * @member CKEDITOR
2858 * @property {Number} [=2]
2859 */
2860CKEDITOR.SHRINK_TEXT = 2;
diff --git a/sources/core/dom/rangelist.js b/sources/core/dom/rangelist.js
new file mode 100644
index 00000000..9c558b36
--- /dev/null
+++ b/sources/core/dom/rangelist.js
@@ -0,0 +1,199 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /**
8 * Represents a list os CKEDITOR.dom.range objects, which can be easily
9 * iterated sequentially.
10 *
11 * @class
12 * @extends Array
13 * @constructor Creates a rangeList class instance.
14 * @param {CKEDITOR.dom.range/CKEDITOR.dom.range[]} [ranges] The ranges contained on this list.
15 * Note that, if an array of ranges is specified, the range sequence
16 * should match its DOM order. This class will not help to sort them.
17 */
18 CKEDITOR.dom.rangeList = function( ranges ) {
19 if ( ranges instanceof CKEDITOR.dom.rangeList )
20 return ranges;
21
22 if ( !ranges )
23 ranges = [];
24 else if ( ranges instanceof CKEDITOR.dom.range )
25 ranges = [ ranges ];
26
27 return CKEDITOR.tools.extend( ranges, mixins );
28 };
29
30 var mixins = {
31 /**
32 * Creates an instance of the rangeList iterator, it should be used
33 * only when the ranges processing could be DOM intrusive, which
34 * means it may pollute and break other ranges in this list.
35 * Otherwise, it's enough to just iterate over this array in a for loop.
36 *
37 * @returns {CKEDITOR.dom.rangeListIterator}
38 */
39 createIterator: function() {
40 var rangeList = this,
41 bookmark = CKEDITOR.dom.walker.bookmark(),
42 bookmarks = [],
43 current;
44
45 return {
46 /**
47 * Retrieves the next range in the list.
48 *
49 * @member CKEDITOR.dom.rangeListIterator
50 * @param {Boolean} [mergeConsequent=false] Whether join two adjacent
51 * ranges into single, e.g. consequent table cells.
52 */
53 getNextRange: function( mergeConsequent ) {
54 current = current === undefined ? 0 : current + 1;
55
56 var range = rangeList[ current ];
57
58 // Multiple ranges might be mangled by each other.
59 if ( range && rangeList.length > 1 ) {
60 // Bookmarking all other ranges on the first iteration,
61 // the range correctness after it doesn't matter since we'll
62 // restore them before the next iteration.
63 if ( !current ) {
64 // Make sure bookmark correctness by reverse processing.
65 for ( var i = rangeList.length - 1; i >= 0; i-- )
66 bookmarks.unshift( rangeList[ i ].createBookmark( true ) );
67 }
68
69 if ( mergeConsequent ) {
70 // Figure out how many ranges should be merged.
71 var mergeCount = 0;
72 while ( rangeList[ current + mergeCount + 1 ] ) {
73 var doc = range.document,
74 found = 0,
75 left = doc.getById( bookmarks[ mergeCount ].endNode ),
76 right = doc.getById( bookmarks[ mergeCount + 1 ].startNode ),
77 next;
78
79 // Check subsequent range.
80 while ( 1 ) {
81 next = left.getNextSourceNode( false );
82 if ( !right.equals( next ) ) {
83 // This could be yet another bookmark or
84 // walking across block boundaries.
85 if ( bookmark( next ) || ( next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() ) ) {
86 left = next;
87 continue;
88 }
89 } else {
90 found = 1;
91 }
92
93 break;
94 }
95
96 if ( !found )
97 break;
98
99 mergeCount++;
100 }
101 }
102
103 range.moveToBookmark( bookmarks.shift() );
104
105 // Merge ranges finally after moving to bookmarks.
106 while ( mergeCount-- ) {
107 next = rangeList[ ++current ];
108 next.moveToBookmark( bookmarks.shift() );
109 range.setEnd( next.endContainer, next.endOffset );
110 }
111 }
112
113 return range;
114 }
115 };
116 },
117
118 /**
119 * Create bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark}.
120 *
121 * @param {Boolean} [serializable=false] See {@link CKEDITOR.dom.range#createBookmark}.
122 * @returns {Array} Array of bookmarks.
123 */
124 createBookmarks: function( serializable ) {
125 var retval = [],
126 bookmark;
127 for ( var i = 0; i < this.length; i++ ) {
128 retval.push( bookmark = this[ i ].createBookmark( serializable, true ) );
129
130 // Updating the container & offset values for ranges
131 // that have been touched.
132 for ( var j = i + 1; j < this.length; j++ ) {
133 this[ j ] = updateDirtyRange( bookmark, this[ j ] );
134 this[ j ] = updateDirtyRange( bookmark, this[ j ], true );
135 }
136 }
137 return retval;
138 },
139
140 /**
141 * Create "unobtrusive" bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark2}.
142 *
143 * @param {Boolean} [normalized=false] See {@link CKEDITOR.dom.range#createBookmark2}.
144 * @returns {Array} Array of bookmarks.
145 */
146 createBookmarks2: function( normalized ) {
147 var bookmarks = [];
148
149 for ( var i = 0; i < this.length; i++ )
150 bookmarks.push( this[ i ].createBookmark2( normalized ) );
151
152 return bookmarks;
153 },
154
155 /**
156 * Move each range in the list to the position specified by a list of bookmarks.
157 *
158 * @param {Array} bookmarks The list of bookmarks, each one matching a range in the list.
159 */
160 moveToBookmarks: function( bookmarks ) {
161 for ( var i = 0; i < this.length; i++ )
162 this[ i ].moveToBookmark( bookmarks[ i ] );
163 }
164 };
165
166 // Update the specified range which has been mangled by previous insertion of
167 // range bookmark nodes.(#3256)
168 function updateDirtyRange( bookmark, dirtyRange, checkEnd ) {
169 var serializable = bookmark.serializable,
170 container = dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ],
171 offset = checkEnd ? 'endOffset' : 'startOffset';
172
173 var bookmarkStart = serializable ? dirtyRange.document.getById( bookmark.startNode ) : bookmark.startNode;
174
175 var bookmarkEnd = serializable ? dirtyRange.document.getById( bookmark.endNode ) : bookmark.endNode;
176
177 if ( container.equals( bookmarkStart.getPrevious() ) ) {
178 dirtyRange.startOffset = dirtyRange.startOffset - container.getLength() - bookmarkEnd.getPrevious().getLength();
179 container = bookmarkEnd.getNext();
180 } else if ( container.equals( bookmarkEnd.getPrevious() ) ) {
181 dirtyRange.startOffset = dirtyRange.startOffset - container.getLength();
182 container = bookmarkEnd.getNext();
183 }
184
185 container.equals( bookmarkStart.getParent() ) && dirtyRange[ offset ]++;
186 container.equals( bookmarkEnd.getParent() ) && dirtyRange[ offset ]++;
187
188 // Update and return this range.
189 dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ] = container;
190 return dirtyRange;
191 }
192} )();
193
194/**
195 * (Virtual Class) Do not call this constructor. This class is not really part
196 * of the API. It just describes the return type of {@link CKEDITOR.dom.rangeList#createIterator}.
197 *
198 * @class CKEDITOR.dom.rangeListIterator
199 */
diff --git a/sources/core/dom/text.js b/sources/core/dom/text.js
new file mode 100644
index 00000000..7c403f90
--- /dev/null
+++ b/sources/core/dom/text.js
@@ -0,0 +1,135 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.text} class, which represents
8 * a DOM text node.
9 */
10
11/**
12 * Represents a DOM text node.
13 *
14 * var nativeNode = document.createTextNode( 'Example' );
15 * var text = new CKEDITOR.dom.text( nativeNode );
16 *
17 * var text = new CKEDITOR.dom.text( 'Example' );
18 *
19 * @class
20 * @extends CKEDITOR.dom.node
21 * @constructor Creates a text class instance.
22 * @param {Object/String} text A native DOM text node or a string containing
23 * the text to use to create a new text node.
24 * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
25 * the node in case of new node creation. Defaults to the current document.
26 */
27CKEDITOR.dom.text = function( text, ownerDocument ) {
28 if ( typeof text == 'string' )
29 text = ( ownerDocument ? ownerDocument.$ : document ).createTextNode( text );
30
31 // Theoretically, we should call the base constructor here
32 // (not CKEDITOR.dom.node though). But, IE doesn't support expando
33 // properties on text node, so the features provided by domObject will not
34 // work for text nodes (which is not a big issue for us).
35 //
36 // CKEDITOR.dom.domObject.call( this, element );
37
38 this.$ = text;
39};
40
41CKEDITOR.dom.text.prototype = new CKEDITOR.dom.node();
42
43CKEDITOR.tools.extend( CKEDITOR.dom.text.prototype, {
44 /**
45 * The node type. This is a constant value set to {@link CKEDITOR#NODE_TEXT}.
46 *
47 * @readonly
48 * @property {Number} [=CKEDITOR.NODE_TEXT]
49 */
50 type: CKEDITOR.NODE_TEXT,
51
52 /**
53 * Gets length of node's value.
54 *
55 * @returns {Number}
56 */
57 getLength: function() {
58 return this.$.nodeValue.length;
59 },
60
61 /**
62 * Gets node's value.
63 *
64 * @returns {String}
65 */
66 getText: function() {
67 return this.$.nodeValue;
68 },
69
70 /**
71 * Sets node's value.
72 *
73 * @param {String} text
74 */
75 setText: function( text ) {
76 this.$.nodeValue = text;
77 },
78
79 /**
80 * Breaks this text node into two nodes at the specified offset,
81 * keeping both in the tree as siblings. This node then only contains
82 * all the content up to the offset point. A new text node, which is
83 * inserted as the next sibling of this node, contains all the content
84 * at and after the offset point. When the offset is equal to the
85 * length of this node, the new node has no data.
86 *
87 * @param {Number} The position at which to split, starting from zero.
88 * @returns {CKEDITOR.dom.text} The new text node.
89 */
90 split: function( offset ) {
91
92 // Saved the children count and text length beforehand.
93 var parent = this.$.parentNode,
94 count = parent.childNodes.length,
95 length = this.getLength();
96
97 var doc = this.getDocument();
98 var retval = new CKEDITOR.dom.text( this.$.splitText( offset ), doc );
99
100 if ( parent.childNodes.length == count ) {
101 // If the offset is after the last char, IE creates the text node
102 // on split, but don't include it into the DOM. So, we have to do
103 // that manually here.
104 if ( offset >= length ) {
105 retval = doc.createText( '' );
106 retval.insertAfter( this );
107 } else {
108 // IE BUG: IE8+ does not update the childNodes array in DOM after splitText(),
109 // we need to make some DOM changes to make it update. (#3436)
110 var workaround = doc.createText( '' );
111 workaround.insertAfter( retval );
112 workaround.remove();
113 }
114 }
115
116 return retval;
117 },
118
119 /**
120 * Extracts characters from indexA up to but not including `indexB`.
121 *
122 * @param {Number} indexA An integer between `0` and one less than the
123 * length of the text.
124 * @param {Number} [indexB] An integer between `0` and the length of the
125 * string. If omitted, extracts characters to the end of the text.
126 */
127 substring: function( indexA, indexB ) {
128 // We need the following check due to a Firefox bug
129 // https://bugzilla.mozilla.org/show_bug.cgi?id=458886
130 if ( typeof indexB != 'number' )
131 return this.$.nodeValue.substr( indexA );
132 else
133 return this.$.nodeValue.substring( indexA, indexB );
134 }
135} );
diff --git a/sources/core/dom/walker.js b/sources/core/dom/walker.js
new file mode 100644
index 00000000..5f2c8f40
--- /dev/null
+++ b/sources/core/dom/walker.js
@@ -0,0 +1,652 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 // This function is to be called under a "walker" instance scope.
8 function iterate( rtl, breakOnFalse ) {
9 var range = this.range;
10
11 // Return null if we have reached the end.
12 if ( this._.end )
13 return null;
14
15 // This is the first call. Initialize it.
16 if ( !this._.start ) {
17 this._.start = 1;
18
19 // A collapsed range must return null at first call.
20 if ( range.collapsed ) {
21 this.end();
22 return null;
23 }
24
25 // Move outside of text node edges.
26 range.optimize();
27 }
28
29 var node,
30 startCt = range.startContainer,
31 endCt = range.endContainer,
32 startOffset = range.startOffset,
33 endOffset = range.endOffset,
34 guard,
35 userGuard = this.guard,
36 type = this.type,
37 getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' );
38
39 // Create the LTR guard function, if necessary.
40 if ( !rtl && !this._.guardLTR ) {
41 // The node that stops walker from moving up.
42 var limitLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt : endCt.getParent();
43
44 // The node that stops the walker from going to next.
45 var blockerLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt.getChild( endOffset ) : endCt.getNext();
46
47 this._.guardLTR = function( node, movingOut ) {
48 return ( ( !movingOut || !limitLTR.equals( node ) ) && ( !blockerLTR || !node.equals( blockerLTR ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
49 };
50 }
51
52 // Create the RTL guard function, if necessary.
53 if ( rtl && !this._.guardRTL ) {
54 // The node that stops walker from moving up.
55 var limitRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startCt : startCt.getParent();
56
57 // The node that stops the walker from going to next.
58 var blockerRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startOffset ? startCt.getChild( startOffset - 1 ) : null : startCt.getPrevious();
59
60 this._.guardRTL = function( node, movingOut ) {
61 return ( ( !movingOut || !limitRTL.equals( node ) ) && ( !blockerRTL || !node.equals( blockerRTL ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
62 };
63 }
64
65 // Define which guard function to use.
66 var stopGuard = rtl ? this._.guardRTL : this._.guardLTR;
67
68 // Make the user defined guard function participate in the process,
69 // otherwise simply use the boundary guard.
70 if ( userGuard ) {
71 guard = function( node, movingOut ) {
72 if ( stopGuard( node, movingOut ) === false )
73 return false;
74
75 return userGuard( node, movingOut );
76 };
77 } else {
78 guard = stopGuard;
79 }
80
81 if ( this.current )
82 node = this.current[ getSourceNodeFn ]( false, type, guard );
83 else {
84 // Get the first node to be returned.
85 if ( rtl ) {
86 node = endCt;
87
88 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
89 if ( endOffset > 0 )
90 node = node.getChild( endOffset - 1 );
91 else
92 node = ( guard( node, true ) === false ) ? null : node.getPreviousSourceNode( true, type, guard );
93 }
94 } else {
95 node = startCt;
96
97 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
98 if ( !( node = node.getChild( startOffset ) ) )
99 node = ( guard( startCt, true ) === false ) ? null : startCt.getNextSourceNode( true, type, guard );
100 }
101 }
102
103 if ( node && guard( node ) === false )
104 node = null;
105 }
106
107 while ( node && !this._.end ) {
108 this.current = node;
109
110 if ( !this.evaluator || this.evaluator( node ) !== false ) {
111 if ( !breakOnFalse )
112 return node;
113 } else if ( breakOnFalse && this.evaluator ) {
114 return false;
115 }
116
117 node = node[ getSourceNodeFn ]( false, type, guard );
118 }
119
120 this.end();
121 return this.current = null;
122 }
123
124 function iterateToLast( rtl ) {
125 var node,
126 last = null;
127
128 while ( ( node = iterate.call( this, rtl ) ) )
129 last = node;
130
131 return last;
132 }
133
134 /**
135 * Utility class to "walk" the DOM inside range boundaries. If the
136 * range starts or ends in the middle of the text node, this node will
137 * be included as a whole. Outside changes to the range may break the walker.
138 *
139 * The walker may return nodes that are not totally included in the
140 * range boundaries. Let us take the following range representation,
141 * where the square brackets indicate the boundaries:
142 *
143 * [<p>Some <b>sample] text</b>
144 *
145 * While walking forward into the above range, the following nodes are
146 * returned: `<p>`, `"Some "`, `<b>` and `"sample"`. Going
147 * backwards instead we have: `"sample"` and `"Some "`. So note that the
148 * walker always returns nodes when "entering" them, but not when
149 * "leaving" them. The {@link #guard} function is instead called both when
150 * entering and when leaving nodes.
151 *
152 * @class
153 */
154 CKEDITOR.dom.walker = CKEDITOR.tools.createClass( {
155 /**
156 * Creates a walker class instance.
157 *
158 * @constructor
159 * @param {CKEDITOR.dom.range} range The range within which to walk.
160 */
161 $: function( range ) {
162 this.range = range;
163
164 /**
165 * A function executed for every matched node to check whether
166 * it is to be considered in the walk or not. If not provided, all
167 * matched nodes are considered good.
168 *
169 * If the function returns `false`, the node is ignored.
170 *
171 * @property {Function} evaluator
172 */
173 // this.evaluator = null;
174
175 /**
176 * A function executed for every node the walk passes by to check
177 * whether the walk is to be finished. It is called both when
178 * entering and when exiting nodes, as well as for the matched nodes.
179 *
180 * If this function returns `false`, the walking ends and no more
181 * nodes are evaluated.
182
183 * @property {Function} guard
184 */
185 // this.guard = null;
186
187 /** @private */
188 this._ = {};
189 },
190
191 // statics :
192 // {
193 // /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes.
194 // * @param {CKEDITOR.dom.node} startNode The node from which the walk
195 // * will start.
196 // * @param {CKEDITOR.dom.node} [endNode] The last node to be considered
197 // * in the walk. No more nodes are retrieved after touching or
198 // * passing it. If not provided, the walker stops at the
199 // * &lt;body&gt; closing boundary.
200 // * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the
201 // * provided nodes.
202 // */
203 // createOnNodes : function( startNode, endNode, startInclusive, endInclusive )
204 // {
205 // var range = new CKEDITOR.dom.range();
206 // if ( startNode )
207 // range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ;
208 // else
209 // range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ;
210 //
211 // if ( endNode )
212 // range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ;
213 // else
214 // range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ;
215 //
216 // return new CKEDITOR.dom.walker( range );
217 // }
218 // },
219 //
220 proto: {
221 /**
222 * Stops walking. No more nodes are retrieved if this function is called.
223 */
224 end: function() {
225 this._.end = 1;
226 },
227
228 /**
229 * Retrieves the next node (on the right).
230 *
231 * @returns {CKEDITOR.dom.node} The next node or `null` if no more
232 * nodes are available.
233 */
234 next: function() {
235 return iterate.call( this );
236 },
237
238 /**
239 * Retrieves the previous node (on the left).
240 *
241 * @returns {CKEDITOR.dom.node} The previous node or `null` if no more
242 * nodes are available.
243 */
244 previous: function() {
245 return iterate.call( this, 1 );
246 },
247
248 /**
249 * Checks all nodes on the right, executing the evaluation function.
250 *
251 * @returns {Boolean} `false` if the evaluator function returned
252 * `false` for any of the matched nodes. Otherwise `true`.
253 */
254 checkForward: function() {
255 return iterate.call( this, 0, 1 ) !== false;
256 },
257
258 /**
259 * Check all nodes on the left, executing the evaluation function.
260 *
261 * @returns {Boolean} `false` if the evaluator function returned
262 * `false` for any of the matched nodes. Otherwise `true`.
263 */
264 checkBackward: function() {
265 return iterate.call( this, 1, 1 ) !== false;
266 },
267
268 /**
269 * Executes a full walk forward (to the right), until no more nodes
270 * are available, returning the last valid node.
271 *
272 * @returns {CKEDITOR.dom.node} The last node on the right or `null`
273 * if no valid nodes are available.
274 */
275 lastForward: function() {
276 return iterateToLast.call( this );
277 },
278
279 /**
280 * Executes a full walk backwards (to the left), until no more nodes
281 * are available, returning the last valid node.
282 *
283 * @returns {CKEDITOR.dom.node} The last node on the left or `null`
284 * if no valid nodes are available.
285 */
286 lastBackward: function() {
287 return iterateToLast.call( this, 1 );
288 },
289
290 /**
291 * Resets the walker.
292 */
293 reset: function() {
294 delete this.current;
295 this._ = {};
296 }
297
298 }
299 } );
300
301 // Anything whose display computed style is block, list-item, table,
302 // table-row-group, table-header-group, table-footer-group, table-row,
303 // table-column-group, table-column, table-cell, table-caption, or whose node
304 // name is hr, br (when enterMode is br only) is a block boundary.
305 var blockBoundaryDisplayMatch = {
306 block: 1, 'list-item': 1, table: 1, 'table-row-group': 1,
307 'table-header-group': 1, 'table-footer-group': 1, 'table-row': 1, 'table-column-group': 1,
308 'table-column': 1, 'table-cell': 1, 'table-caption': 1
309 },
310 outOfFlowPositions = { absolute: 1, fixed: 1 };
311
312 /**
313 * Checks whether an element is displayed as a block.
314 *
315 * @member CKEDITOR.dom.element
316 * @param [customNodeNames] Custom list of nodes which will extend
317 * the default {@link CKEDITOR.dtd#$block} list.
318 * @returns {Boolean}
319 */
320 CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) {
321 // Whether element is in normal page flow. Floated or positioned elements are out of page flow.
322 // Don't consider floated or positioned formatting as block boundary, fall back to dtd check in that case. (#6297)
323 var inPageFlow = this.getComputedStyle( 'float' ) == 'none' && !( this.getComputedStyle( 'position' ) in outOfFlowPositions );
324
325 if ( inPageFlow && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] )
326 return true;
327
328 // Either in $block or in customNodeNames if defined.
329 return !!( this.is( CKEDITOR.dtd.$block ) || customNodeNames && this.is( customNodeNames ) );
330 };
331
332 /**
333 * Returns a function which checks whether the node is a block boundary.
334 * See {@link CKEDITOR.dom.element#isBlockBoundary}.
335 *
336 * @static
337 * @param customNodeNames
338 * @returns {Function}
339 */
340 CKEDITOR.dom.walker.blockBoundary = function( customNodeNames ) {
341 return function( node ) {
342 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary( customNodeNames ) );
343 };
344 };
345
346 /**
347 * @static
348 * @todo
349 */
350 CKEDITOR.dom.walker.listItemBoundary = function() {
351 return this.blockBoundary( { br: 1 } );
352 };
353
354 /**
355 * Returns a function which checks whether the node is a bookmark node or the bookmark node
356 * inner content.
357 *
358 * @static
359 * @param {Boolean} [contentOnly=false] Whether only test against the text content of
360 * a bookmark node instead of the element itself (default).
361 * @param {Boolean} [isReject=false] Whether to return `false` for the bookmark
362 * node instead of `true` (default).
363 * @returns {Function}
364 */
365 CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject ) {
366 function isBookmarkNode( node ) {
367 return ( node && node.getName && node.getName() == 'span' && node.data( 'cke-bookmark' ) );
368 }
369
370 return function( node ) {
371 var isBookmark, parent;
372 // Is bookmark inner text node?
373 isBookmark = ( node && node.type != CKEDITOR.NODE_ELEMENT && ( parent = node.getParent() ) && isBookmarkNode( parent ) );
374 // Is bookmark node?
375 isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node );
376 return !!( isReject ^ isBookmark );
377 };
378 };
379
380 /**
381 * Returns a function which checks whether the node is a text node containing only whitespace characters.
382 *
383 * @static
384 * @param {Boolean} [isReject=false]
385 * @returns {Function}
386 */
387 CKEDITOR.dom.walker.whitespaces = function( isReject ) {
388 return function( node ) {
389 var isWhitespace;
390 if ( node && node.type == CKEDITOR.NODE_TEXT ) {
391 // whitespace, as well as the text cursor filler node we used in Webkit. (#9384)
392 isWhitespace = !CKEDITOR.tools.trim( node.getText() ) ||
393 CKEDITOR.env.webkit && node.getText() == '\u200b';
394 }
395
396 return !!( isReject ^ isWhitespace );
397 };
398 };
399
400 /**
401 * Returns a function which checks whether the node is invisible in the WYSIWYG mode.
402 *
403 * @static
404 * @param {Boolean} [isReject=false]
405 * @returns {Function}
406 */
407 CKEDITOR.dom.walker.invisible = function( isReject ) {
408 var whitespace = CKEDITOR.dom.walker.whitespaces(),
409 // #12221 (Chrome) plus #11111 (Safari).
410 offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
411
412 return function( node ) {
413 var invisible;
414
415 if ( whitespace( node ) )
416 invisible = 1;
417 else {
418 // Visibility should be checked on element.
419 if ( node.type == CKEDITOR.NODE_TEXT )
420 node = node.getParent();
421
422 // Nodes that take no spaces in wysiwyg:
423 // 1. White-spaces but not including NBSP.
424 // 2. Empty inline elements, e.g. <b></b>.
425 // 3. <br> elements (bogus, surrounded by text) (#12423).
426 invisible = node.$.offsetWidth <= offsetWidth0;
427 }
428
429 return !!( isReject ^ invisible );
430 };
431 };
432
433 /**
434 * Returns a function which checks whether the node type is equal to the passed one.
435 *
436 * @static
437 * @param {Number} type
438 * @param {Boolean} [isReject=false]
439 * @returns {Function}
440 */
441 CKEDITOR.dom.walker.nodeType = function( type, isReject ) {
442 return function( node ) {
443 return !!( isReject ^ ( node.type == type ) );
444 };
445 };
446
447 /**
448 * Returns a function which checks whether the node is a bogus (filler) node from
449 * `contenteditable` element's point of view.
450 *
451 * @static
452 * @param {Boolean} [isReject=false]
453 * @returns {Function}
454 */
455 CKEDITOR.dom.walker.bogus = function( isReject ) {
456 function nonEmpty( node ) {
457 return !isWhitespaces( node ) && !isBookmark( node );
458 }
459
460 return function( node ) {
461 var isBogus = CKEDITOR.env.needsBrFiller ? node.is && node.is( 'br' ) : node.getText && tailNbspRegex.test( node.getText() );
462
463 if ( isBogus ) {
464 var parent = node.getParent(),
465 next = node.getNext( nonEmpty );
466
467 isBogus = parent.isBlockBoundary() && ( !next || next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() );
468 }
469
470 return !!( isReject ^ isBogus );
471 };
472 };
473
474 /**
475 * Returns a function which checks whether the node is a temporary element
476 * (element with the `data-cke-temp` attribute) or its child.
477 *
478 * @since 4.3
479 * @static
480 * @param {Boolean} [isReject=false] Whether to return `false` for the
481 * temporary element instead of `true` (default).
482 * @returns {Function}
483 */
484 CKEDITOR.dom.walker.temp = function( isReject ) {
485 return function( node ) {
486 if ( node.type != CKEDITOR.NODE_ELEMENT )
487 node = node.getParent();
488
489 var isTemp = node && node.hasAttribute( 'data-cke-temp' );
490
491 return !!( isReject ^ isTemp );
492 };
493 };
494
495 var tailNbspRegex = /^[\t\r\n ]*(?:&nbsp;|\xa0)$/,
496 isWhitespaces = CKEDITOR.dom.walker.whitespaces(),
497 isBookmark = CKEDITOR.dom.walker.bookmark(),
498 isTemp = CKEDITOR.dom.walker.temp(),
499 toSkip = function( node ) {
500 return isBookmark( node ) ||
501 isWhitespaces( node ) ||
502 node.type == CKEDITOR.NODE_ELEMENT && node.is( CKEDITOR.dtd.$inline ) && !node.is( CKEDITOR.dtd.$empty );
503 };
504
505 /**
506 * Returns a function which checks whether the node should be ignored in terms of "editability".
507 *
508 * This includes:
509 *
510 * * whitespaces (see {@link CKEDITOR.dom.walker#whitespaces}),
511 * * bookmarks (see {@link CKEDITOR.dom.walker#bookmark}),
512 * * temporary elements (see {@link CKEDITOR.dom.walker#temp}).
513 *
514 * @since 4.3
515 * @static
516 * @param {Boolean} [isReject=false] Whether to return `false` for the
517 * ignored element instead of `true` (default).
518 * @returns {Function}
519 */
520 CKEDITOR.dom.walker.ignored = function( isReject ) {
521 return function( node ) {
522 var isIgnored = isWhitespaces( node ) || isBookmark( node ) || isTemp( node );
523
524 return !!( isReject ^ isIgnored );
525 };
526 };
527
528 var isIgnored = CKEDITOR.dom.walker.ignored();
529
530 /**
531 * Returns a function which checks whether the node is empty.
532 *
533 * @since 4.5
534 * @static
535 * @param {Boolean} [isReject=false] Whether to return `false` for the
536 * ignored element instead of `true` (default).
537 * @returns {Function}
538 */
539 CKEDITOR.dom.walker.empty = function( isReject ) {
540 return function( node ) {
541 var i = 0,
542 l = node.getChildCount();
543
544 for ( ; i < l; ++i ) {
545 if ( !isIgnored( node.getChild( i ) ) ) {
546 return !!isReject;
547 }
548 }
549
550 return !isReject;
551 };
552 };
553
554 var isEmpty = CKEDITOR.dom.walker.empty();
555
556 function filterTextContainers( dtd ) {
557 var hash = {},
558 name;
559
560 for ( name in dtd ) {
561 if ( CKEDITOR.dtd[ name ][ '#' ] )
562 hash[ name ] = 1;
563 }
564 return hash;
565 }
566
567 /**
568 * A hash of element names which in browsers that {@link CKEDITOR.env#needsBrFiller do not need `<br>` fillers}
569 * can be selection containers despite being empty.
570 *
571 * @since 4.5
572 * @static
573 * @property {Object} validEmptyBlockContainers
574 */
575 var validEmptyBlocks = CKEDITOR.dom.walker.validEmptyBlockContainers = CKEDITOR.tools.extend(
576 filterTextContainers( CKEDITOR.dtd.$block ),
577 { caption: 1, td: 1, th: 1 }
578 );
579
580 function isEditable( node ) {
581 // Skip temporary elements, bookmarks and whitespaces.
582 if ( isIgnored( node ) )
583 return false;
584
585 if ( node.type == CKEDITOR.NODE_TEXT )
586 return true;
587
588 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
589 // All inline and non-editable elements are valid editable places.
590 // Note: the <hr> is currently the only element in CKEDITOR.dtd.$empty and CKEDITOR.dtd.$block,
591 // but generally speaking we need an intersection of these two sets.
592 // Note: non-editable block has to be treated differently (should be selected entirely).
593 if ( node.is( CKEDITOR.dtd.$inline ) || node.is( 'hr' ) || node.getAttribute( 'contenteditable' ) == 'false' )
594 return true;
595
596 // Empty blocks are editable on IE.
597 if ( !CKEDITOR.env.needsBrFiller && node.is( validEmptyBlocks ) && isEmpty( node ) )
598 return true;
599 }
600
601 // Skip all other nodes.
602 return false;
603 }
604
605 /**
606 * Returns a function which checks whether the node can be a container or a sibling
607 * of the selection end.
608 *
609 * This includes:
610 *
611 * * text nodes (but not whitespaces),
612 * * inline elements,
613 * * intersection of {@link CKEDITOR.dtd#$empty} and {@link CKEDITOR.dtd#$block} (currently
614 * it is only `<hr>`),
615 * * non-editable blocks (special case &mdash; such blocks cannot be containers nor
616 * siblings, they need to be selected entirely),
617 * * empty {@link #validEmptyBlockContainers blocks} which can contain text
618 * ({@link CKEDITOR.env#needsBrFiller old IEs only}).
619 *
620 * @since 4.3
621 * @static
622 * @param {Boolean} [isReject=false] Whether to return `false` for the
623 * ignored element instead of `true` (default).
624 * @returns {Function}
625 */
626 CKEDITOR.dom.walker.editable = function( isReject ) {
627 return function( node ) {
628 return !!( isReject ^ isEditable( node ) );
629 };
630 };
631
632 /**
633 * Checks if there is a filler node at the end of an element, and returns it.
634 *
635 * @member CKEDITOR.dom.element
636 * @returns {CKEDITOR.dom.node/Boolean} Bogus node or `false`.
637 */
638 CKEDITOR.dom.element.prototype.getBogus = function() {
639 // Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (#7070).
640 var tail = this;
641 do {
642 tail = tail.getPreviousSourceNode();
643 }
644 while ( toSkip( tail ) );
645
646 if ( tail && ( CKEDITOR.env.needsBrFiller ? tail.is && tail.is( 'br' ) : tail.getText && tailNbspRegex.test( tail.getText() ) ) )
647 return tail;
648
649 return false;
650 };
651
652} )();
diff --git a/sources/core/dom/window.js b/sources/core/dom/window.js
new file mode 100644
index 00000000..123c981a
--- /dev/null
+++ b/sources/core/dom/window.js
@@ -0,0 +1,95 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.document} class, which
8 * represents a DOM document.
9 */
10
11/**
12 * Represents a DOM window.
13 *
14 * var document = new CKEDITOR.dom.window( window );
15 *
16 * @class
17 * @extends CKEDITOR.dom.domObject
18 * @constructor Creates a window class instance.
19 * @param {Object} domWindow A native DOM window.
20 */
21CKEDITOR.dom.window = function( domWindow ) {
22 CKEDITOR.dom.domObject.call( this, domWindow );
23};
24
25CKEDITOR.dom.window.prototype = new CKEDITOR.dom.domObject();
26
27CKEDITOR.tools.extend( CKEDITOR.dom.window.prototype, {
28 /**
29 * Moves the selection focus to this window.
30 *
31 * var win = new CKEDITOR.dom.window( window );
32 * win.focus();
33 */
34 focus: function() {
35 this.$.focus();
36 },
37
38 /**
39 * Gets the width and height of this window's viewable area.
40 *
41 * var win = new CKEDITOR.dom.window( window );
42 * var size = win.getViewPaneSize();
43 * alert( size.width );
44 * alert( size.height );
45 *
46 * @returns {Object} An object with the `width` and `height`
47 * properties containing the size.
48 */
49 getViewPaneSize: function() {
50 var doc = this.$.document,
51 stdMode = doc.compatMode == 'CSS1Compat';
52 return {
53 width: ( stdMode ? doc.documentElement.clientWidth : doc.body.clientWidth ) || 0,
54 height: ( stdMode ? doc.documentElement.clientHeight : doc.body.clientHeight ) || 0
55 };
56 },
57
58 /**
59 * Gets the current position of the window's scroll.
60 *
61 * var win = new CKEDITOR.dom.window( window );
62 * var pos = win.getScrollPosition();
63 * alert( pos.x );
64 * alert( pos.y );
65 *
66 * @returns {Object} An object with the `x` and `y` properties
67 * containing the scroll position.
68 */
69 getScrollPosition: function() {
70 var $ = this.$;
71
72 if ( 'pageXOffset' in $ ) {
73 return {
74 x: $.pageXOffset || 0,
75 y: $.pageYOffset || 0
76 };
77 } else {
78 var doc = $.document;
79 return {
80 x: doc.documentElement.scrollLeft || doc.body.scrollLeft || 0,
81 y: doc.documentElement.scrollTop || doc.body.scrollTop || 0
82 };
83 }
84 },
85
86 /**
87 * Gets the frame element containing this window context.
88 *
89 * @returns {CKEDITOR.dom.element} The frame element or `null` if not in a frame context.
90 */
91 getFrame: function() {
92 var iframe = this.$.frameElement;
93 return iframe ? new CKEDITOR.dom.element.get( iframe ) : null;
94 }
95} );