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.js2183
-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.js902
-rw-r--r--sources/core/dom/nodelist.js43
-rw-r--r--sources/core/dom/range.js2978
-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, 8918 insertions, 0 deletions
diff --git a/sources/core/dom/comment.js b/sources/core/dom/comment.js
new file mode 100644
index 0000000..4abb453
--- /dev/null
+++ b/sources/core/dom/comment.js
@@ -0,0 +1,53 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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 0000000..51c5b19
--- /dev/null
+++ b/sources/core/dom/document.js
@@ -0,0 +1,326 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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 0000000..1058144
--- /dev/null
+++ b/sources/core/dom/documentfragment.js
@@ -0,0 +1,62 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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 0000000..4c593ff
--- /dev/null
+++ b/sources/core/dom/domobject.js
@@ -0,0 +1,266 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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 0000000..e02ff17
--- /dev/null
+++ b/sources/core/dom/element.js
@@ -0,0 +1,2183 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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 tmpElement,
373 current;
374
375 // Move the element outside the broken element.
376 range.insertNode( this.remove() );
377
378 // In case of Internet Explorer, we must check if there is no background-color
379 // added to the element. In such case, we have to overwrite it to prevent "switching it off"
380 // by a browser (#14667).
381 if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) {
382 tmpElement = new CKEDITOR.dom.element( 'div' );
383
384 while ( current = docFrag.getFirst() ) {
385 if ( current.$.style.backgroundColor ) {
386 // This is a necessary hack to make sure that IE will track backgroundColor CSS property, see
387 // http://dev.ckeditor.com/ticket/14667#comment:8 for more details.
388 current.$.style.backgroundColor = current.$.style.backgroundColor;
389 }
390
391 tmpElement.append( current );
392 }
393
394 // Re-insert the extracted piece after the element.
395 tmpElement.insertAfter( this );
396 tmpElement.remove( true );
397 } else {
398 // Re-insert the extracted piece after the element.
399 docFrag.insertAfterNode( this );
400 }
401 },
402
403 /**
404 * Checks if this element contains given node.
405 *
406 * @method
407 * @param {CKEDITOR.dom.node} node
408 * @returns {Boolean}
409 */
410 contains: !document.compareDocumentPosition ?
411 function( node ) {
412 var $ = this.$;
413
414 return node.type != CKEDITOR.NODE_ELEMENT ? $.contains( node.getParent().$ ) : $ != node.$ && $.contains( node.$ );
415 } : function( node ) {
416 return !!( this.$.compareDocumentPosition( node.$ ) & 16 );
417 },
418
419 /**
420 * Moves the selection focus to this element.
421 *
422 * var element = CKEDITOR.document.getById( 'myTextarea' );
423 * element.focus();
424 *
425 * @method
426 * @param {Boolean} defer Whether to asynchronously defer the
427 * execution by 100 ms.
428 */
429 focus: ( function() {
430 function exec() {
431 // IE throws error if the element is not visible.
432 try {
433 this.$.focus();
434 } catch ( e ) {}
435 }
436
437 return function( defer ) {
438 if ( defer )
439 CKEDITOR.tools.setTimeout( exec, 100, this );
440 else
441 exec.call( this );
442 };
443 } )(),
444
445 /**
446 * Gets the inner HTML of this element.
447 *
448 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
449 * alert( element.getHtml() ); // '<b>Example</b>'
450 *
451 * @returns {String} The inner HTML of this element.
452 */
453 getHtml: function() {
454 var retval = this.$.innerHTML;
455 // Strip <?xml:namespace> tags in IE. (#3341).
456 return CKEDITOR.env.ie ? retval.replace( /<\?[^>]*>/g, '' ) : retval;
457 },
458
459 /**
460 * Gets the outer (inner plus tags) HTML of this element.
461 *
462 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="bold"><b>Example</b></div>' );
463 * alert( element.getOuterHtml() ); // '<div class="bold"><b>Example</b></div>'
464 *
465 * @returns {String} The outer HTML of this element.
466 */
467 getOuterHtml: function() {
468 if ( this.$.outerHTML ) {
469 // IE includes the <?xml:namespace> tag in the outerHTML of
470 // namespaced element. So, we must strip it here. (#3341)
471 return this.$.outerHTML.replace( /<\?[^>]*>/, '' );
472 }
473
474 var tmpDiv = this.$.ownerDocument.createElement( 'div' );
475 tmpDiv.appendChild( this.$.cloneNode( true ) );
476 return tmpDiv.innerHTML;
477 },
478
479 /**
480 * Retrieve the bounding rectangle of the current element, in pixels,
481 * relative to the upper-left corner of the browser's client area.
482 *
483 * @returns {Object} The dimensions of the DOM element including
484 * `left`, `top`, `right`, `bottom`, `width` and `height`.
485 */
486 getClientRect: function() {
487 // http://help.dottoro.com/ljvmcrrn.php
488 var rect = CKEDITOR.tools.extend( {}, this.$.getBoundingClientRect() );
489
490 !rect.width && ( rect.width = rect.right - rect.left );
491 !rect.height && ( rect.height = rect.bottom - rect.top );
492
493 return rect;
494 },
495
496 /**
497 * Sets the inner HTML of this element.
498 *
499 * var p = new CKEDITOR.dom.element( 'p' );
500 * p.setHtml( '<b>Inner</b> HTML' );
501 *
502 * // Result: '<p><b>Inner</b> HTML</p>'
503 *
504 * @method
505 * @param {String} html The HTML to be set for this element.
506 * @returns {String} The inserted HTML.
507 */
508 setHtml: ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) ?
509 // old IEs throws error on HTML manipulation (through the "innerHTML" property)
510 // on the element which resides in an DTD invalid position, e.g. <span><div></div></span>
511 // fortunately it can be worked around with DOM manipulation.
512 function( html ) {
513 try {
514 var $ = this.$;
515
516 // Fix the case when setHtml is called on detached element.
517 // HTML5 shiv used for document in which this element was created
518 // won't affect that detached element. So get document fragment with
519 // all HTML5 elements enabled and set innerHTML while this element is appended to it.
520 if ( this.getParent() )
521 return ( $.innerHTML = html );
522 else {
523 var $frag = this.getDocument()._getHtml5ShivFrag();
524 $frag.appendChild( $ );
525 $.innerHTML = html;
526 $frag.removeChild( $ );
527
528 return html;
529 }
530 }
531 catch ( e ) {
532 this.$.innerHTML = '';
533
534 var temp = new CKEDITOR.dom.element( 'body', this.getDocument() );
535 temp.$.innerHTML = html;
536
537 var children = temp.getChildren();
538 while ( children.count() )
539 this.append( children.getItem( 0 ) );
540
541 return html;
542 }
543 } : function( html ) {
544 return ( this.$.innerHTML = html );
545 },
546
547 /**
548 * Sets the element contents as plain text.
549 *
550 * var element = new CKEDITOR.dom.element( 'div' );
551 * element.setText( 'A > B & C < D' );
552 * alert( element.innerHTML ); // 'A &gt; B &amp; C &lt; D'
553 *
554 * @param {String} text The text to be set.
555 * @returns {String} The inserted text.
556 */
557 setText: ( function() {
558 var supportsTextContent = document.createElement( 'p' );
559 supportsTextContent.innerHTML = 'x';
560 supportsTextContent = supportsTextContent.textContent;
561
562 return function( text ) {
563 this.$[ supportsTextContent ? 'textContent' : 'innerText' ] = text;
564 };
565 } )(),
566
567 /**
568 * Gets the value of an element attribute.
569 *
570 * var element = CKEDITOR.dom.element.createFromHtml( '<input type="text" />' );
571 * alert( element.getAttribute( 'type' ) ); // 'text'
572 *
573 * @method
574 * @param {String} name The attribute name.
575 * @returns {String} The attribute value or null if not defined.
576 */
577 getAttribute: ( function() {
578 var standard = function( name ) {
579 return this.$.getAttribute( name, 2 );
580 };
581
582 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
583 return function( name ) {
584 switch ( name ) {
585 case 'class':
586 name = 'className';
587 break;
588
589 case 'http-equiv':
590 name = 'httpEquiv';
591 break;
592
593 case 'name':
594 return this.$.name;
595
596 case 'tabindex':
597 var tabIndex = standard.call( this, name );
598
599 // IE returns tabIndex=0 by default for all
600 // elements. For those elements,
601 // getAtrribute( 'tabindex', 2 ) returns 32768
602 // instead. So, we must make this check to give a
603 // uniform result among all browsers.
604 if ( tabIndex !== 0 && this.$.tabIndex === 0 )
605 tabIndex = null;
606
607 return tabIndex;
608
609 case 'checked':
610 var attr = this.$.attributes.getNamedItem( name ),
611 attrValue = attr.specified ? attr.nodeValue // For value given by parser.
612 : this.$.checked; // For value created via DOM interface.
613
614 return attrValue ? 'checked' : null;
615
616 case 'hspace':
617 case 'value':
618 return this.$[ name ];
619
620 case 'style':
621 // IE does not return inline styles via getAttribute(). See #2947.
622 return this.$.style.cssText;
623
624 case 'contenteditable':
625 case 'contentEditable':
626 return this.$.attributes.getNamedItem( 'contentEditable' ).specified ? this.$.getAttribute( 'contentEditable' ) : null;
627 }
628
629 return standard.call( this, name );
630 };
631 } else {
632 return standard;
633 }
634 } )(),
635
636 /**
637 * Gets the values of all element attributes.
638 *
639 * @param {Array} exclude The names of attributes to be excluded from the returned object.
640 * @return {Object} An object containing all element attributes with their values.
641 */
642 getAttributes: function( exclude ) {
643 var attributes = {},
644 attrDefs = this.$.attributes,
645 i;
646
647 exclude = CKEDITOR.tools.isArray( exclude ) ? exclude : [];
648
649 for ( i = 0; i < attrDefs.length; i++ ) {
650 if ( CKEDITOR.tools.indexOf( exclude, attrDefs[ i ].name ) === -1 ) {
651 attributes[ attrDefs[ i ].name ] = attrDefs[ i ].value;
652 }
653 }
654
655 return attributes;
656 },
657
658 /**
659 * Gets the nodes list containing all children of this element.
660 *
661 * @returns {CKEDITOR.dom.nodeList}
662 */
663 getChildren: function() {
664 return new CKEDITOR.dom.nodeList( this.$.childNodes );
665 },
666
667 /**
668 * Gets the current computed value of one of the element CSS style
669 * properties.
670 *
671 * var element = new CKEDITOR.dom.element( 'span' );
672 * alert( element.getComputedStyle( 'display' ) ); // 'inline'
673 *
674 * @method
675 * @param {String} propertyName The style property name.
676 * @returns {String} The property value.
677 */
678 getComputedStyle: ( document.defaultView && document.defaultView.getComputedStyle ) ?
679 function( propertyName ) {
680 var style = this.getWindow().$.getComputedStyle( this.$, null );
681
682 // Firefox may return null if we call the above on a hidden iframe. (#9117)
683 return style ? style.getPropertyValue( propertyName ) : '';
684 } : function( propertyName ) {
685 return this.$.currentStyle[ CKEDITOR.tools.cssStyleToDomStyle( propertyName ) ];
686 },
687
688 /**
689 * Gets the DTD entries for this element.
690 *
691 * @returns {Object} An object containing the list of elements accepted
692 * by this element.
693 */
694 getDtd: function() {
695 var dtd = CKEDITOR.dtd[ this.getName() ];
696
697 this.getDtd = function() {
698 return dtd;
699 };
700
701 return dtd;
702 },
703
704 /**
705 * Gets all this element's descendants having given tag name.
706 *
707 * @method
708 * @param {String} tagName
709 */
710 getElementsByTag: CKEDITOR.dom.document.prototype.getElementsByTag,
711
712 /**
713 * Gets the computed tabindex for this element.
714 *
715 * var element = CKEDITOR.document.getById( 'myDiv' );
716 * alert( element.getTabIndex() ); // (e.g.) '-1'
717 *
718 * @method
719 * @returns {Number} The tabindex value.
720 */
721 getTabIndex: function() {
722 var tabIndex = this.$.tabIndex;
723
724 // IE returns tabIndex=0 by default for all elements. In
725 // those cases we must check that the element really has
726 // the tabindex attribute set to zero, or it is one of
727 // those element that should have zero by default.
728 if ( tabIndex === 0 && !CKEDITOR.dtd.$tabIndex[ this.getName() ] && parseInt( this.getAttribute( 'tabindex' ), 10 ) !== 0 )
729 return -1;
730
731 return tabIndex;
732 },
733
734 /**
735 * Gets the text value of this element.
736 *
737 * Only in IE (which uses innerText), `<br>` will cause linebreaks,
738 * and sucessive whitespaces (including line breaks) will be reduced to
739 * a single space. This behavior is ok for us, for now. It may change
740 * in the future.
741 *
742 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Sample <i>text</i>.</div>' );
743 * alert( <b>element.getText()</b> ); // 'Sample text.'
744 *
745 * @returns {String} The text value.
746 */
747 getText: function() {
748 return this.$.textContent || this.$.innerText || '';
749 },
750
751 /**
752 * Gets the window object that contains this element.
753 *
754 * @returns {CKEDITOR.dom.window} The window object.
755 */
756 getWindow: function() {
757 return this.getDocument().getWindow();
758 },
759
760 /**
761 * Gets the value of the `id` attribute of this element.
762 *
763 * var element = CKEDITOR.dom.element.createFromHtml( '<p id="myId"></p>' );
764 * alert( element.getId() ); // 'myId'
765 *
766 * @returns {String} The element id, or null if not available.
767 */
768 getId: function() {
769 return this.$.id || null;
770 },
771
772 /**
773 * Gets the value of the `name` attribute of this element.
774 *
775 * var element = CKEDITOR.dom.element.createFromHtml( '<input name="myName"></input>' );
776 * alert( <b>element.getNameAtt()</b> ); // 'myName'
777 *
778 * @returns {String} The element name, or null if not available.
779 */
780 getNameAtt: function() {
781 return this.$.name || null;
782 },
783
784 /**
785 * Gets the element name (tag name). The returned name is guaranteed to
786 * be always full lowercased.
787 *
788 * var element = new CKEDITOR.dom.element( 'span' );
789 * alert( element.getName() ); // 'span'
790 *
791 * @returns {String} The element name.
792 */
793 getName: function() {
794 // Cache the lowercased name inside a closure.
795 var nodeName = this.$.nodeName.toLowerCase();
796
797 if ( CKEDITOR.env.ie && ( document.documentMode <= 8 ) ) {
798 var scopeName = this.$.scopeName;
799 if ( scopeName != 'HTML' )
800 nodeName = scopeName.toLowerCase() + ':' + nodeName;
801 }
802
803 this.getName = function() {
804 return nodeName;
805 };
806
807 return this.getName();
808 },
809
810 /**
811 * Gets the value set to this element. This value is usually available
812 * for form field elements.
813 *
814 * @returns {String} The element value.
815 */
816 getValue: function() {
817 return this.$.value;
818 },
819
820 /**
821 * Gets the first child node of this element.
822 *
823 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
824 * var first = element.getFirst();
825 * alert( first.getName() ); // 'b'
826 *
827 * @param {Function} evaluator Filtering the result node.
828 * @returns {CKEDITOR.dom.node} The first child node or null if not available.
829 */
830 getFirst: function( evaluator ) {
831 var first = this.$.firstChild,
832 retval = first && new CKEDITOR.dom.node( first );
833 if ( retval && evaluator && !evaluator( retval ) )
834 retval = retval.getNext( evaluator );
835
836 return retval;
837 },
838
839 /**
840 * See {@link #getFirst}.
841 *
842 * @param {Function} evaluator Filtering the result node.
843 * @returns {CKEDITOR.dom.node}
844 */
845 getLast: function( evaluator ) {
846 var last = this.$.lastChild,
847 retval = last && new CKEDITOR.dom.node( last );
848 if ( retval && evaluator && !evaluator( retval ) )
849 retval = retval.getPrevious( evaluator );
850
851 return retval;
852 },
853
854 /**
855 * Gets CSS style value.
856 *
857 * @param {String} name The CSS property name.
858 * @returns {String} Style value.
859 */
860 getStyle: function( name ) {
861 return this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ];
862 },
863
864 /**
865 * Checks if the element name matches the specified criteria.
866 *
867 * var element = new CKEDITOR.element( 'span' );
868 * alert( element.is( 'span' ) ); // true
869 * alert( element.is( 'p', 'span' ) ); // true
870 * alert( element.is( 'p' ) ); // false
871 * alert( element.is( 'p', 'div' ) ); // false
872 * alert( element.is( { p:1,span:1 } ) ); // true
873 *
874 * @param {String.../Object} name One or more names to be checked, or a {@link CKEDITOR.dtd} object.
875 * @returns {Boolean} `true` if the element name matches any of the names.
876 */
877 is: function() {
878 var name = this.getName();
879
880 // Check against the specified DTD liternal.
881 if ( typeof arguments[ 0 ] == 'object' )
882 return !!arguments[ 0 ][ name ];
883
884 // Check for tag names
885 for ( var i = 0; i < arguments.length; i++ ) {
886 if ( arguments[ i ] == name )
887 return true;
888 }
889 return false;
890 },
891
892 /**
893 * Decide whether one element is able to receive cursor.
894 *
895 * @param {Boolean} [textCursor=true] Only consider element that could receive text child.
896 */
897 isEditable: function( textCursor ) {
898 var name = this.getName();
899
900 if ( this.isReadOnly() || this.getComputedStyle( 'display' ) == 'none' ||
901 this.getComputedStyle( 'visibility' ) == 'hidden' ||
902 CKEDITOR.dtd.$nonEditable[ name ] ||
903 CKEDITOR.dtd.$empty[ name ] ||
904 ( this.is( 'a' ) &&
905 ( this.data( 'cke-saved-name' ) || this.hasAttribute( 'name' ) ) &&
906 !this.getChildCount()
907 ) ) {
908 return false;
909 }
910
911 if ( textCursor !== false ) {
912 // Get the element DTD (defaults to span for unknown elements).
913 var dtd = CKEDITOR.dtd[ name ] || CKEDITOR.dtd.span;
914 // In the DTD # == text node.
915 return !!( dtd && dtd[ '#' ] );
916 }
917
918 return true;
919 },
920
921 /**
922 * Compare this element's inner html, tag name, attributes, etc. with other one.
923 *
924 * See [W3C's DOM Level 3 spec - node#isEqualNode](http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-isEqualNode)
925 * for more details.
926 *
927 * @param {CKEDITOR.dom.element} otherElement Element to compare.
928 * @returns {Boolean}
929 */
930 isIdentical: function( otherElement ) {
931 // do shallow clones, but with IDs
932 var thisEl = this.clone( 0, 1 ),
933 otherEl = otherElement.clone( 0, 1 );
934
935 // Remove distractions.
936 thisEl.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] );
937 otherEl.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] );
938
939 // Native comparison available.
940 if ( thisEl.$.isEqualNode ) {
941 // Styles order matters.
942 thisEl.$.style.cssText = CKEDITOR.tools.normalizeCssText( thisEl.$.style.cssText );
943 otherEl.$.style.cssText = CKEDITOR.tools.normalizeCssText( otherEl.$.style.cssText );
944 return thisEl.$.isEqualNode( otherEl.$ );
945 } else {
946 thisEl = thisEl.getOuterHtml();
947 otherEl = otherEl.getOuterHtml();
948
949 // Fix tiny difference between link href in older IEs.
950 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && this.is( 'a' ) ) {
951 var parent = this.getParent();
952 if ( parent.type == CKEDITOR.NODE_ELEMENT ) {
953 var el = parent.clone();
954 el.setHtml( thisEl ), thisEl = el.getHtml();
955 el.setHtml( otherEl ), otherEl = el.getHtml();
956 }
957 }
958
959 return thisEl == otherEl;
960 }
961 },
962
963 /**
964 * Checks if this element is visible. May not work if the element is
965 * child of an element with visibility set to `hidden`, but works well
966 * on the great majority of cases.
967 *
968 * @returns {Boolean} True if the element is visible.
969 */
970 isVisible: function() {
971 var isVisible = ( this.$.offsetHeight || this.$.offsetWidth ) && this.getComputedStyle( 'visibility' ) != 'hidden',
972 elementWindow, elementWindowFrame;
973
974 // Webkit and Opera report non-zero offsetHeight despite that
975 // element is inside an invisible iframe. (#4542)
976 if ( isVisible && CKEDITOR.env.webkit ) {
977 elementWindow = this.getWindow();
978
979 if ( !elementWindow.equals( CKEDITOR.document.getWindow() ) && ( elementWindowFrame = elementWindow.$.frameElement ) )
980 isVisible = new CKEDITOR.dom.element( elementWindowFrame ).isVisible();
981
982 }
983
984 return !!isVisible;
985 },
986
987 /**
988 * Whether it's an empty inline elements which has no visual impact when removed.
989 *
990 * @returns {Boolean}
991 */
992 isEmptyInlineRemoveable: function() {
993 if ( !CKEDITOR.dtd.$removeEmpty[ this.getName() ] )
994 return false;
995
996 var children = this.getChildren();
997 for ( var i = 0, count = children.count(); i < count; i++ ) {
998 var child = children.getItem( i );
999
1000 if ( child.type == CKEDITOR.NODE_ELEMENT && child.data( 'cke-bookmark' ) )
1001 continue;
1002
1003 if ( child.type == CKEDITOR.NODE_ELEMENT && !child.isEmptyInlineRemoveable() || child.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( child.getText() ) )
1004 return false;
1005
1006 }
1007 return true;
1008 },
1009
1010 /**
1011 * Checks if the element has any defined attributes.
1012 *
1013 * var element = CKEDITOR.dom.element.createFromHtml( '<div title="Test">Example</div>' );
1014 * alert( element.hasAttributes() ); // true
1015 *
1016 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Example</div>' );
1017 * alert( element.hasAttributes() ); // false
1018 *
1019 * @method
1020 * @returns {Boolean} True if the element has attributes.
1021 */
1022 hasAttributes: CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ?
1023 function() {
1024 var attributes = this.$.attributes;
1025
1026 for ( var i = 0; i < attributes.length; i++ ) {
1027 var attribute = attributes[ i ];
1028
1029 switch ( attribute.nodeName ) {
1030 case 'class':
1031 // IE has a strange bug. If calling removeAttribute('className'),
1032 // the attributes collection will still contain the "class"
1033 // attribute, which will be marked as "specified", even if the
1034 // outerHTML of the element is not displaying the class attribute.
1035 // Note : I was not able to reproduce it outside the editor,
1036 // but I've faced it while working on the TC of #1391.
1037 if ( this.getAttribute( 'class' ) ) {
1038 return true;
1039 }
1040
1041 // Attributes to be ignored.
1042 /* falls through */
1043 case 'data-cke-expando':
1044 continue;
1045
1046
1047 /* falls through */
1048 default:
1049 if ( attribute.specified ) {
1050 return true;
1051 }
1052 }
1053 }
1054
1055 return false;
1056 } : function() {
1057 var attrs = this.$.attributes,
1058 attrsNum = attrs.length;
1059
1060 // The _moz_dirty attribute might get into the element after pasting (#5455)
1061 var execludeAttrs = { 'data-cke-expando': 1, _moz_dirty: 1 };
1062
1063 return attrsNum > 0 && ( attrsNum > 2 || !execludeAttrs[ attrs[ 0 ].nodeName ] || ( attrsNum == 2 && !execludeAttrs[ attrs[ 1 ].nodeName ] ) );
1064 },
1065
1066 /**
1067 * Checks if the specified attribute is defined for this element.
1068 *
1069 * @method
1070 * @param {String} name The attribute name.
1071 * @returns {Boolean} `true` if the specified attribute is defined.
1072 */
1073 hasAttribute: ( function() {
1074 function ieHasAttribute( name ) {
1075 var $attr = this.$.attributes.getNamedItem( name );
1076
1077 if ( this.getName() == 'input' ) {
1078 switch ( name ) {
1079 case 'class':
1080 return this.$.className.length > 0;
1081 case 'checked':
1082 return !!this.$.checked;
1083 case 'value':
1084 var type = this.getAttribute( 'type' );
1085 return type == 'checkbox' || type == 'radio' ? this.$.value != 'on' : !!this.$.value;
1086 }
1087 }
1088
1089 if ( !$attr )
1090 return false;
1091
1092 return $attr.specified;
1093 }
1094
1095 if ( CKEDITOR.env.ie ) {
1096 if ( CKEDITOR.env.version < 8 ) {
1097 return function( name ) {
1098 // On IE < 8 the name attribute cannot be retrieved
1099 // right after the element creation and setting the
1100 // name with setAttribute.
1101 if ( name == 'name' )
1102 return !!this.$.name;
1103
1104 return ieHasAttribute.call( this, name );
1105 };
1106 } else {
1107 return ieHasAttribute;
1108 }
1109 } else {
1110 return function( name ) {
1111 // On other browsers specified property is deprecated and return always true,
1112 // but fortunately $.attributes contains only specified attributes.
1113 return !!this.$.attributes.getNamedItem( name );
1114 };
1115 }
1116 } )(),
1117
1118 /**
1119 * Hides this element (sets `display: none`).
1120 *
1121 * var element = CKEDITOR.document.getById( 'myElement' );
1122 * element.hide();
1123 */
1124 hide: function() {
1125 this.setStyle( 'display', 'none' );
1126 },
1127
1128 /**
1129 * Moves this element's children to the target element.
1130 *
1131 * @param {CKEDITOR.dom.element} target
1132 * @param {Boolean} [toStart=false] Insert moved children at the
1133 * beginning of the target element.
1134 */
1135 moveChildren: function( target, toStart ) {
1136 var $ = this.$;
1137 target = target.$;
1138
1139 if ( $ == target )
1140 return;
1141
1142 var child;
1143
1144 if ( toStart ) {
1145 while ( ( child = $.lastChild ) )
1146 target.insertBefore( $.removeChild( child ), target.firstChild );
1147 } else {
1148 while ( ( child = $.firstChild ) )
1149 target.appendChild( $.removeChild( child ) );
1150 }
1151 },
1152
1153 /**
1154 * Merges sibling elements that are identical to this one.
1155 *
1156 * Identical child elements are also merged. For example:
1157 *
1158 * <b><i></i></b><b><i></i></b> => <b><i></i></b>
1159 *
1160 * @method
1161 * @param {Boolean} [inlineOnly=true] Allow only inline elements to be merged.
1162 */
1163 mergeSiblings: ( function() {
1164 function mergeElements( element, sibling, isNext ) {
1165 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) {
1166 // Jumping over bookmark nodes and empty inline elements, e.g. <b><i></i></b>,
1167 // queuing them to be moved later. (#5567)
1168 var pendingNodes = [];
1169
1170 while ( sibling.data( 'cke-bookmark' ) || sibling.isEmptyInlineRemoveable() ) {
1171 pendingNodes.push( sibling );
1172 sibling = isNext ? sibling.getNext() : sibling.getPrevious();
1173 if ( !sibling || sibling.type != CKEDITOR.NODE_ELEMENT )
1174 return;
1175 }
1176
1177 if ( element.isIdentical( sibling ) ) {
1178 // Save the last child to be checked too, to merge things like
1179 // <b><i></i></b><b><i></i></b> => <b><i></i></b>
1180 var innerSibling = isNext ? element.getLast() : element.getFirst();
1181
1182 // Move pending nodes first into the target element.
1183 while ( pendingNodes.length )
1184 pendingNodes.shift().move( element, !isNext );
1185
1186 sibling.moveChildren( element, !isNext );
1187 sibling.remove();
1188
1189 // Now check the last inner child (see two comments above).
1190 if ( innerSibling && innerSibling.type == CKEDITOR.NODE_ELEMENT )
1191 innerSibling.mergeSiblings();
1192 }
1193 }
1194 }
1195
1196 return function( inlineOnly ) {
1197 // Merge empty links and anchors also. (#5567)
1198 if ( !( inlineOnly === false || CKEDITOR.dtd.$removeEmpty[ this.getName() ] || this.is( 'a' ) ) ) {
1199 return;
1200 }
1201
1202 mergeElements( this, this.getNext(), true );
1203 mergeElements( this, this.getPrevious() );
1204 };
1205 } )(),
1206
1207 /**
1208 * Shows this element (displays it).
1209 *
1210 * var element = CKEDITOR.document.getById( 'myElement' );
1211 * element.show();
1212 */
1213 show: function() {
1214 this.setStyles( {
1215 display: '',
1216 visibility: ''
1217 } );
1218 },
1219
1220 /**
1221 * Sets the value of an element attribute.
1222 *
1223 * var element = CKEDITOR.document.getById( 'myElement' );
1224 * element.setAttribute( 'class', 'myClass' );
1225 * element.setAttribute( 'title', 'This is an example' );
1226 *
1227 * @method
1228 * @param {String} name The name of the attribute.
1229 * @param {String} value The value to be set to the attribute.
1230 * @returns {CKEDITOR.dom.element} This element instance.
1231 */
1232 setAttribute: ( function() {
1233 var standard = function( name, value ) {
1234 this.$.setAttribute( name, value );
1235 return this;
1236 };
1237
1238 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
1239 return function( name, value ) {
1240 if ( name == 'class' )
1241 this.$.className = value;
1242 else if ( name == 'style' )
1243 this.$.style.cssText = value;
1244 else if ( name == 'tabindex' ) // Case sensitive.
1245 this.$.tabIndex = value;
1246 else if ( name == 'checked' )
1247 this.$.checked = value;
1248 else if ( name == 'contenteditable' )
1249 standard.call( this, 'contentEditable', value );
1250 else
1251 standard.apply( this, arguments );
1252 return this;
1253 };
1254 } else if ( CKEDITOR.env.ie8Compat && CKEDITOR.env.secure ) {
1255 return function( name, value ) {
1256 // IE8 throws error when setting src attribute to non-ssl value. (#7847)
1257 if ( name == 'src' && value.match( /^http:\/\// ) ) {
1258 try {
1259 standard.apply( this, arguments );
1260 } catch ( e ) {}
1261 } else {
1262 standard.apply( this, arguments );
1263 }
1264 return this;
1265 };
1266 } else {
1267 return standard;
1268 }
1269 } )(),
1270
1271 /**
1272 * Sets the value of several element attributes.
1273 *
1274 * var element = CKEDITOR.document.getById( 'myElement' );
1275 * element.setAttributes( {
1276 * 'class': 'myClass',
1277 * title: 'This is an example'
1278 * } );
1279 *
1280 * @chainable
1281 * @param {Object} attributesPairs An object containing the names and
1282 * values of the attributes.
1283 * @returns {CKEDITOR.dom.element} This element instance.
1284 */
1285 setAttributes: function( attributesPairs ) {
1286 for ( var name in attributesPairs )
1287 this.setAttribute( name, attributesPairs[ name ] );
1288 return this;
1289 },
1290
1291 /**
1292 * Sets the element value. This function is usually used with form
1293 * field element.
1294 *
1295 * @chainable
1296 * @param {String} value The element value.
1297 * @returns {CKEDITOR.dom.element} This element instance.
1298 */
1299 setValue: function( value ) {
1300 this.$.value = value;
1301 return this;
1302 },
1303
1304 /**
1305 * Removes an attribute from the element.
1306 *
1307 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="classA"></div>' );
1308 * element.removeAttribute( 'class' );
1309 *
1310 * @method
1311 * @param {String} name The attribute name.
1312 */
1313 removeAttribute: ( function() {
1314 var standard = function( name ) {
1315 this.$.removeAttribute( name );
1316 };
1317
1318 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
1319 return function( name ) {
1320 if ( name == 'class' )
1321 name = 'className';
1322 else if ( name == 'tabindex' )
1323 name = 'tabIndex';
1324 else if ( name == 'contenteditable' )
1325 name = 'contentEditable';
1326 standard.call( this, name );
1327 };
1328 } else {
1329 return standard;
1330 }
1331 } )(),
1332
1333 /**
1334 * Removes all element's attributes or just given ones.
1335 *
1336 * @param {Array} [attributes] The array with attributes names.
1337 */
1338 removeAttributes: function( attributes ) {
1339 if ( CKEDITOR.tools.isArray( attributes ) ) {
1340 for ( var i = 0; i < attributes.length; i++ ) {
1341 this.removeAttribute( attributes[ i ] );
1342 }
1343 } else {
1344 attributes = attributes || this.getAttributes();
1345
1346 for ( var attr in attributes ) {
1347 attributes.hasOwnProperty( attr ) && this.removeAttribute( attr );
1348 }
1349 }
1350 },
1351
1352 /**
1353 * Removes a style from the element.
1354 *
1355 * var element = CKEDITOR.dom.element.createFromHtml( '<div style="display:none"></div>' );
1356 * element.removeStyle( 'display' );
1357 *
1358 * @method
1359 * @param {String} name The style name.
1360 */
1361 removeStyle: function( name ) {
1362 // Removes the specified property from the current style object.
1363 var $ = this.$.style;
1364
1365 // "removeProperty" need to be specific on the following styles.
1366 if ( !$.removeProperty && ( name == 'border' || name == 'margin' || name == 'padding' ) ) {
1367 var names = expandedRules( name );
1368 for ( var i = 0 ; i < names.length ; i++ )
1369 this.removeStyle( names[ i ] );
1370 return;
1371 }
1372
1373 $.removeProperty ? $.removeProperty( name ) : $.removeAttribute( CKEDITOR.tools.cssStyleToDomStyle( name ) );
1374
1375 // Eventually remove empty style attribute.
1376 if ( !this.$.style.cssText )
1377 this.removeAttribute( 'style' );
1378 },
1379
1380 /**
1381 * Sets the value of an element style.
1382 *
1383 * var element = CKEDITOR.document.getById( 'myElement' );
1384 * element.setStyle( 'background-color', '#ff0000' );
1385 * element.setStyle( 'margin-top', '10px' );
1386 * element.setStyle( 'float', 'right' );
1387 *
1388 * @param {String} name The name of the style. The CSS naming notation
1389 * must be used (e.g. `background-color`).
1390 * @param {String} value The value to be set to the style.
1391 * @returns {CKEDITOR.dom.element} This element instance.
1392 */
1393 setStyle: function( name, value ) {
1394 this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ] = value;
1395 return this;
1396 },
1397
1398 /**
1399 * Sets the value of several element styles.
1400 *
1401 * var element = CKEDITOR.document.getById( 'myElement' );
1402 * element.setStyles( {
1403 * position: 'absolute',
1404 * float: 'right'
1405 * } );
1406 *
1407 * @param {Object} stylesPairs An object containing the names and
1408 * values of the styles.
1409 * @returns {CKEDITOR.dom.element} This element instance.
1410 */
1411 setStyles: function( stylesPairs ) {
1412 for ( var name in stylesPairs )
1413 this.setStyle( name, stylesPairs[ name ] );
1414 return this;
1415 },
1416
1417 /**
1418 * Sets the opacity of an element.
1419 *
1420 * var element = CKEDITOR.document.getById( 'myElement' );
1421 * element.setOpacity( 0.75 );
1422 *
1423 * @param {Number} opacity A number within the range `[0.0, 1.0]`.
1424 */
1425 setOpacity: function( opacity ) {
1426 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
1427 opacity = Math.round( opacity * 100 );
1428 this.setStyle( 'filter', opacity >= 100 ? '' : 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + opacity + ')' );
1429 } else {
1430 this.setStyle( 'opacity', opacity );
1431 }
1432 },
1433
1434 /**
1435 * Makes the element and its children unselectable.
1436 *
1437 * var element = CKEDITOR.document.getById( 'myElement' );
1438 * element.unselectable();
1439 *
1440 * @method
1441 */
1442 unselectable: function() {
1443 // CSS unselectable.
1444 this.setStyles( CKEDITOR.tools.cssVendorPrefix( 'user-select', 'none' ) );
1445
1446 // For IE/Opera which doesn't support for the above CSS style,
1447 // the unselectable="on" attribute only specifies the selection
1448 // process cannot start in the element itself, and it doesn't inherit.
1449 if ( CKEDITOR.env.ie ) {
1450 this.setAttribute( 'unselectable', 'on' );
1451
1452 var element,
1453 elements = this.getElementsByTag( '*' );
1454
1455 for ( var i = 0, count = elements.count() ; i < count ; i++ ) {
1456 element = elements.getItem( i );
1457 element.setAttribute( 'unselectable', 'on' );
1458 }
1459 }
1460 },
1461
1462 /**
1463 * Gets closest positioned (`position != static`) ancestor.
1464 *
1465 * @returns {CKEDITOR.dom.element} Positioned ancestor or `null`.
1466 */
1467 getPositionedAncestor: function() {
1468 var current = this;
1469 while ( current.getName() != 'html' ) {
1470 if ( current.getComputedStyle( 'position' ) != 'static' )
1471 return current;
1472
1473 current = current.getParent();
1474 }
1475 return null;
1476 },
1477
1478 /**
1479 * Gets this element's position in document.
1480 *
1481 * @param {CKEDITOR.dom.document} [refDocument]
1482 * @returns {Object} Element's position.
1483 * @returns {Number} return.x
1484 * @returns {Number} return.y
1485 * @todo refDocument
1486 */
1487 getDocumentPosition: function( refDocument ) {
1488 var x = 0,
1489 y = 0,
1490 doc = this.getDocument(),
1491 body = doc.getBody(),
1492 quirks = doc.$.compatMode == 'BackCompat';
1493
1494 if ( document.documentElement.getBoundingClientRect &&
1495 ( CKEDITOR.env.ie ? CKEDITOR.env.version !== 8 : true ) ) {
1496 var box = this.$.getBoundingClientRect(),
1497 $doc = doc.$,
1498 $docElem = $doc.documentElement;
1499
1500 var clientTop = $docElem.clientTop || body.$.clientTop || 0,
1501 clientLeft = $docElem.clientLeft || body.$.clientLeft || 0,
1502 needAdjustScrollAndBorders = true;
1503
1504 // #3804: getBoundingClientRect() works differently on IE and non-IE
1505 // browsers, regarding scroll positions.
1506 //
1507 // On IE, the top position of the <html> element is always 0, no matter
1508 // how much you scrolled down.
1509 //
1510 // On other browsers, the top position of the <html> element is negative
1511 // scrollTop.
1512 if ( CKEDITOR.env.ie ) {
1513 var inDocElem = doc.getDocumentElement().contains( this ),
1514 inBody = doc.getBody().contains( this );
1515
1516 needAdjustScrollAndBorders = ( quirks && inBody ) || ( !quirks && inDocElem );
1517 }
1518
1519 // #12747.
1520 if ( needAdjustScrollAndBorders ) {
1521 var scrollRelativeLeft,
1522 scrollRelativeTop;
1523
1524 // See #12758 to know more about document.(documentElement|body).scroll(Left|Top) in Webkit.
1525 if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version >= 12 ) ) {
1526 scrollRelativeLeft = body.$.scrollLeft || $docElem.scrollLeft;
1527 scrollRelativeTop = body.$.scrollTop || $docElem.scrollTop;
1528 } else {
1529 var scrollRelativeElement = quirks ? body.$ : $docElem;
1530
1531 scrollRelativeLeft = scrollRelativeElement.scrollLeft;
1532 scrollRelativeTop = scrollRelativeElement.scrollTop;
1533 }
1534
1535 x = box.left + scrollRelativeLeft - clientLeft;
1536 y = box.top + scrollRelativeTop - clientTop;
1537 }
1538 } else {
1539 var current = this,
1540 previous = null,
1541 offsetParent;
1542 while ( current && !( current.getName() == 'body' || current.getName() == 'html' ) ) {
1543 x += current.$.offsetLeft - current.$.scrollLeft;
1544 y += current.$.offsetTop - current.$.scrollTop;
1545
1546 // Opera includes clientTop|Left into offsetTop|Left.
1547 if ( !current.equals( this ) ) {
1548 x += ( current.$.clientLeft || 0 );
1549 y += ( current.$.clientTop || 0 );
1550 }
1551
1552 var scrollElement = previous;
1553 while ( scrollElement && !scrollElement.equals( current ) ) {
1554 x -= scrollElement.$.scrollLeft;
1555 y -= scrollElement.$.scrollTop;
1556 scrollElement = scrollElement.getParent();
1557 }
1558
1559 previous = current;
1560 current = ( offsetParent = current.$.offsetParent ) ? new CKEDITOR.dom.element( offsetParent ) : null;
1561 }
1562 }
1563
1564 if ( refDocument ) {
1565 var currentWindow = this.getWindow(),
1566 refWindow = refDocument.getWindow();
1567
1568 if ( !currentWindow.equals( refWindow ) && currentWindow.$.frameElement ) {
1569 var iframePosition = ( new CKEDITOR.dom.element( currentWindow.$.frameElement ) ).getDocumentPosition( refDocument );
1570
1571 x += iframePosition.x;
1572 y += iframePosition.y;
1573 }
1574 }
1575
1576 if ( !document.documentElement.getBoundingClientRect ) {
1577 // In Firefox, we'll endup one pixel before the element positions,
1578 // so we must add it here.
1579 if ( CKEDITOR.env.gecko && !quirks ) {
1580 x += this.$.clientLeft ? 1 : 0;
1581 y += this.$.clientTop ? 1 : 0;
1582 }
1583 }
1584
1585 return { x: x, y: y };
1586 },
1587
1588 /**
1589 * Make any page element visible inside the browser viewport.
1590 *
1591 * @param {Boolean} [alignToTop=false]
1592 */
1593 scrollIntoView: function( alignToTop ) {
1594 var parent = this.getParent();
1595 if ( !parent )
1596 return;
1597
1598 // Scroll the element into parent container from the inner out.
1599 do {
1600 // Check ancestors that overflows.
1601 var overflowed =
1602 parent.$.clientWidth && parent.$.clientWidth < parent.$.scrollWidth ||
1603 parent.$.clientHeight && parent.$.clientHeight < parent.$.scrollHeight;
1604
1605 // Skip body element, which will report wrong clientHeight when containing
1606 // floated content. (#9523)
1607 if ( overflowed && !parent.is( 'body' ) )
1608 this.scrollIntoParent( parent, alignToTop, 1 );
1609
1610 // Walk across the frame.
1611 if ( parent.is( 'html' ) ) {
1612 var win = parent.getWindow();
1613
1614 // Avoid security error.
1615 try {
1616 var iframe = win.$.frameElement;
1617 iframe && ( parent = new CKEDITOR.dom.element( iframe ) );
1618 } catch ( er ) {}
1619 }
1620 }
1621 while ( ( parent = parent.getParent() ) );
1622 },
1623
1624 /**
1625 * Make any page element visible inside one of the ancestors by scrolling the parent.
1626 *
1627 * @param {CKEDITOR.dom.element/CKEDITOR.dom.window} parent The container to scroll into.
1628 * @param {Boolean} [alignToTop] Align the element's top side with the container's
1629 * when `true` is specified; align the bottom with viewport bottom when
1630 * `false` is specified. Otherwise scroll on either side with the minimum
1631 * amount to show the element.
1632 * @param {Boolean} [hscroll] Whether horizontal overflow should be considered.
1633 */
1634 scrollIntoParent: function( parent, alignToTop, hscroll ) {
1635 !parent && ( parent = this.getWindow() );
1636
1637 var doc = parent.getDocument();
1638 var isQuirks = doc.$.compatMode == 'BackCompat';
1639
1640 // On window <html> is scrolled while quirks scrolls <body>.
1641 if ( parent instanceof CKEDITOR.dom.window )
1642 parent = isQuirks ? doc.getBody() : doc.getDocumentElement();
1643
1644 // Scroll the parent by the specified amount.
1645 function scrollBy( x, y ) {
1646 // Webkit doesn't support "scrollTop/scrollLeft"
1647 // on documentElement/body element.
1648 if ( /body|html/.test( parent.getName() ) )
1649 parent.getWindow().$.scrollBy( x, y );
1650 else {
1651 parent.$.scrollLeft += x;
1652 parent.$.scrollTop += y;
1653 }
1654 }
1655
1656 // Figure out the element position relative to the specified window.
1657 function screenPos( element, refWin ) {
1658 var pos = { x: 0, y: 0 };
1659
1660 if ( !( element.is( isQuirks ? 'body' : 'html' ) ) ) {
1661 var box = element.$.getBoundingClientRect();
1662 pos.x = box.left, pos.y = box.top;
1663 }
1664
1665 var win = element.getWindow();
1666 if ( !win.equals( refWin ) ) {
1667 var outerPos = screenPos( CKEDITOR.dom.element.get( win.$.frameElement ), refWin );
1668 pos.x += outerPos.x, pos.y += outerPos.y;
1669 }
1670
1671 return pos;
1672 }
1673
1674 // calculated margin size.
1675 function margin( element, side ) {
1676 return parseInt( element.getComputedStyle( 'margin-' + side ) || 0, 10 ) || 0;
1677 }
1678
1679 // [WebKit] Reset stored scrollTop value to not break scrollIntoView() method flow.
1680 // Scrolling breaks when range.select() is used right after element.scrollIntoView(). (#14659)
1681 if ( CKEDITOR.env.webkit ) {
1682 var editor = this.getEditor( false );
1683
1684 if ( editor ) {
1685 editor._.previousScrollTop = null;
1686 }
1687 }
1688
1689 var win = parent.getWindow();
1690
1691 var thisPos = screenPos( this, win ),
1692 parentPos = screenPos( parent, win ),
1693 eh = this.$.offsetHeight,
1694 ew = this.$.offsetWidth,
1695 ch = parent.$.clientHeight,
1696 cw = parent.$.clientWidth,
1697 lt, br;
1698
1699 // Left-top margins.
1700 lt = {
1701 x: thisPos.x - margin( this, 'left' ) - parentPos.x || 0,
1702 y: thisPos.y - margin( this, 'top' ) - parentPos.y || 0
1703 };
1704
1705 // Bottom-right margins.
1706 br = {
1707 x: thisPos.x + ew + margin( this, 'right' ) - ( ( parentPos.x ) + cw ) || 0,
1708 y: thisPos.y + eh + margin( this, 'bottom' ) - ( ( parentPos.y ) + ch ) || 0
1709 };
1710
1711 // 1. Do the specified alignment as much as possible;
1712 // 2. Otherwise be smart to scroll only the minimum amount;
1713 // 3. Never cut at the top;
1714 // 4. DO NOT scroll when already visible.
1715 if ( lt.y < 0 || br.y > 0 )
1716 scrollBy( 0, alignToTop === true ? lt.y : alignToTop === false ? br.y : lt.y < 0 ? lt.y : br.y );
1717
1718 if ( hscroll && ( lt.x < 0 || br.x > 0 ) )
1719 scrollBy( lt.x < 0 ? lt.x : br.x, 0 );
1720 },
1721
1722 /**
1723 * Switch the `class` attribute to reflect one of the triple states of an
1724 * element in one of {@link CKEDITOR#TRISTATE_ON}, {@link CKEDITOR#TRISTATE_OFF}
1725 * or {@link CKEDITOR#TRISTATE_DISABLED}.
1726 *
1727 * link.setState( CKEDITOR.TRISTATE_ON );
1728 * // <a class="cke_on" aria-pressed="true">...</a>
1729 * link.setState( CKEDITOR.TRISTATE_OFF );
1730 * // <a class="cke_off">...</a>
1731 * link.setState( CKEDITOR.TRISTATE_DISABLED );
1732 * // <a class="cke_disabled" aria-disabled="true">...</a>
1733 *
1734 * span.setState( CKEDITOR.TRISTATE_ON, 'cke_button' );
1735 * // <span class="cke_button_on">...</span>
1736 *
1737 * @param {Number} state Indicate the element state. One of {@link CKEDITOR#TRISTATE_ON},
1738 * {@link CKEDITOR#TRISTATE_OFF}, {@link CKEDITOR#TRISTATE_DISABLED}.
1739 * @param [base='cke'] The prefix apply to each of the state class name.
1740 * @param [useAria=true] Whether toggle the ARIA state attributes besides of class name change.
1741 */
1742 setState: function( state, base, useAria ) {
1743 base = base || 'cke';
1744
1745 switch ( state ) {
1746 case CKEDITOR.TRISTATE_ON:
1747 this.addClass( base + '_on' );
1748 this.removeClass( base + '_off' );
1749 this.removeClass( base + '_disabled' );
1750 useAria && this.setAttribute( 'aria-pressed', true );
1751 useAria && this.removeAttribute( 'aria-disabled' );
1752 break;
1753
1754 case CKEDITOR.TRISTATE_DISABLED:
1755 this.addClass( base + '_disabled' );
1756 this.removeClass( base + '_off' );
1757 this.removeClass( base + '_on' );
1758 useAria && this.setAttribute( 'aria-disabled', true );
1759 useAria && this.removeAttribute( 'aria-pressed' );
1760 break;
1761
1762 default:
1763 this.addClass( base + '_off' );
1764 this.removeClass( base + '_on' );
1765 this.removeClass( base + '_disabled' );
1766 useAria && this.removeAttribute( 'aria-pressed' );
1767 useAria && this.removeAttribute( 'aria-disabled' );
1768 break;
1769 }
1770 },
1771
1772 /**
1773 * Returns the inner document of this `<iframe>` element.
1774 *
1775 * @returns {CKEDITOR.dom.document} The inner document.
1776 */
1777 getFrameDocument: function() {
1778 var $ = this.$;
1779
1780 try {
1781 // In IE, with custom document.domain, it may happen that
1782 // the iframe is not yet available, resulting in "Access
1783 // Denied" for the following property access.
1784 $.contentWindow.document;
1785 } catch ( e ) {
1786 // Trick to solve this issue, forcing the iframe to get ready
1787 // by simply setting its "src" property.
1788 $.src = $.src;
1789 }
1790
1791 return $ && new CKEDITOR.dom.document( $.contentWindow.document );
1792 },
1793
1794 /**
1795 * Copy all the attributes from one node to the other, kinda like a clone
1796 * skipAttributes is an object with the attributes that must **not** be copied.
1797 *
1798 * @param {CKEDITOR.dom.element} dest The destination element.
1799 * @param {Object} skipAttributes A dictionary of attributes to skip.
1800 */
1801 copyAttributes: function( dest, skipAttributes ) {
1802 var attributes = this.$.attributes;
1803 skipAttributes = skipAttributes || {};
1804
1805 for ( var n = 0; n < attributes.length; n++ ) {
1806 var attribute = attributes[ n ];
1807
1808 // Lowercase attribute name hard rule is broken for
1809 // some attribute on IE, e.g. CHECKED.
1810 var attrName = attribute.nodeName.toLowerCase(),
1811 attrValue;
1812
1813 // We can set the type only once, so do it with the proper value, not copying it.
1814 if ( attrName in skipAttributes )
1815 continue;
1816
1817 if ( attrName == 'checked' && ( attrValue = this.getAttribute( attrName ) ) )
1818 dest.setAttribute( attrName, attrValue );
1819 // IE contains not specified attributes in $.attributes so we need to check
1820 // if elements attribute is specified using hasAttribute.
1821 else if ( !CKEDITOR.env.ie || this.hasAttribute( attrName ) ) {
1822 attrValue = this.getAttribute( attrName );
1823 if ( attrValue === null )
1824 attrValue = attribute.nodeValue;
1825
1826 dest.setAttribute( attrName, attrValue );
1827 }
1828 }
1829
1830 // The style:
1831 if ( this.$.style.cssText !== '' )
1832 dest.$.style.cssText = this.$.style.cssText;
1833 },
1834
1835 /**
1836 * Changes the tag name of the current element.
1837 *
1838 * @param {String} newTag The new tag for the element.
1839 */
1840 renameNode: function( newTag ) {
1841 // If it's already correct exit here.
1842 if ( this.getName() == newTag )
1843 return;
1844
1845 var doc = this.getDocument();
1846
1847 // Create the new node.
1848 var newNode = new CKEDITOR.dom.element( newTag, doc );
1849
1850 // Copy all attributes.
1851 this.copyAttributes( newNode );
1852
1853 // Move children to the new node.
1854 this.moveChildren( newNode );
1855
1856 // Replace the node.
1857 this.getParent( true ) && this.$.parentNode.replaceChild( newNode.$, this.$ );
1858 newNode.$[ 'data-cke-expando' ] = this.$[ 'data-cke-expando' ];
1859 this.$ = newNode.$;
1860 // Bust getName's cache. (#8663)
1861 delete this.getName;
1862 },
1863
1864 /**
1865 * Gets a DOM tree descendant under the current node.
1866 *
1867 * var strong = p.getChild( 0 );
1868 *
1869 * @method
1870 * @param {Array/Number} indices The child index or array of child indices under the node.
1871 * @returns {CKEDITOR.dom.node} The specified DOM child under the current node. Null if child does not exist.
1872 */
1873 getChild: ( function() {
1874 function getChild( rawNode, index ) {
1875 var childNodes = rawNode.childNodes;
1876
1877 if ( index >= 0 && index < childNodes.length )
1878 return childNodes[ index ];
1879 }
1880
1881 return function( indices ) {
1882 var rawNode = this.$;
1883
1884 if ( !indices.slice )
1885 rawNode = getChild( rawNode, indices );
1886 else {
1887 indices = indices.slice();
1888 while ( indices.length > 0 && rawNode )
1889 rawNode = getChild( rawNode, indices.shift() );
1890 }
1891
1892 return rawNode ? new CKEDITOR.dom.node( rawNode ) : null;
1893 };
1894 } )(),
1895
1896 /**
1897 * Gets number of element's children.
1898 *
1899 * @returns {Number}
1900 */
1901 getChildCount: function() {
1902 return this.$.childNodes.length;
1903 },
1904
1905 /**
1906 * Disables browser's context menu in this element.
1907 */
1908 disableContextMenu: function() {
1909 this.on( 'contextmenu', function( evt ) {
1910 // Cancel the browser context menu.
1911 if ( !evt.data.getTarget().getAscendant( enablesContextMenu, true ) )
1912 evt.data.preventDefault();
1913 } );
1914
1915 function enablesContextMenu( node ) {
1916 return node.type == CKEDITOR.NODE_ELEMENT && node.hasClass( 'cke_enable_context_menu' );
1917 }
1918 },
1919
1920 /**
1921 * Gets element's direction. Supports both CSS `direction` prop and `dir` attr.
1922 */
1923 getDirection: function( useComputed ) {
1924 if ( useComputed ) {
1925 return this.getComputedStyle( 'direction' ) ||
1926 this.getDirection() ||
1927 this.getParent() && this.getParent().getDirection( 1 ) ||
1928 this.getDocument().$.dir ||
1929 'ltr';
1930 }
1931 else {
1932 return this.getStyle( 'direction' ) || this.getAttribute( 'dir' );
1933 }
1934 },
1935
1936 /**
1937 * Gets, sets and removes custom data to be stored as HTML5 data-* attributes.
1938 *
1939 * element.data( 'extra-info', 'test' ); // Appended the attribute data-extra-info="test" to the element.
1940 * alert( element.data( 'extra-info' ) ); // 'test'
1941 * element.data( 'extra-info', false ); // Remove the data-extra-info attribute from the element.
1942 *
1943 * @param {String} name The name of the attribute, excluding the `data-` part.
1944 * @param {String} [value] The value to set. If set to false, the attribute will be removed.
1945 */
1946 data: function( name, value ) {
1947 name = 'data-' + name;
1948 if ( value === undefined )
1949 return this.getAttribute( name );
1950 else if ( value === false )
1951 this.removeAttribute( name );
1952 else
1953 this.setAttribute( name, value );
1954
1955 return null;
1956 },
1957
1958 /**
1959 * Retrieves an editor instance which is based on this element (if any).
1960 * It basically loops over {@link CKEDITOR#instances} in search for an instance
1961 * that uses the element.
1962 *
1963 * var element = new CKEDITOR.dom.element( 'div' );
1964 * element.appendTo( CKEDITOR.document.getBody() );
1965 * CKEDITOR.replace( element );
1966 * alert( element.getEditor().name ); // 'editor1'
1967 *
1968 * By default this method considers only original DOM elements upon which the editor
1969 * was created. Setting `optimized` parameter to `false` will consider editor editable
1970 * and its children.
1971 *
1972 * @param {Boolean} [optimized=true] If set to `false` it will scan every editor editable.
1973 * @returns {CKEDITOR.editor} An editor instance or null if nothing has been found.
1974 */
1975 getEditor: function( optimized ) {
1976 var instances = CKEDITOR.instances,
1977 name, instance, editable;
1978
1979 optimized = optimized || optimized === undefined;
1980
1981 for ( name in instances ) {
1982 instance = instances[ name ];
1983
1984 if ( instance.element.equals( this ) && instance.elementMode != CKEDITOR.ELEMENT_MODE_APPENDTO )
1985 return instance;
1986
1987 if ( !optimized ) {
1988 editable = instance.editable();
1989
1990 if ( editable && ( editable.equals( this ) || editable.contains( this ) ) ) {
1991 return instance;
1992 }
1993 }
1994 }
1995
1996 return null;
1997 },
1998
1999 /**
2000 * Returns list of elements within this element that match specified `selector`.
2001 *
2002 * **Notes:**
2003 *
2004 * * Not available in IE7.
2005 * * Returned list is not a live collection (like a result of native `querySelectorAll`).
2006 * * Unlike native `querySelectorAll` this method ensures selector contextualization. This is:
2007 *
2008 * HTML: '<body><div><i>foo</i></div></body>'
2009 * Native: div.querySelectorAll( 'body i' ) // -> [ <i>foo</i> ]
2010 * Method: div.find( 'body i' ) // -> []
2011 * div.find( 'i' ) // -> [ <i>foo</i> ]
2012 *
2013 * @since 4.3
2014 * @param {String} selector
2015 * @returns {CKEDITOR.dom.nodeList}
2016 */
2017 find: function( selector ) {
2018 var removeTmpId = createTmpId( this ),
2019 list = new CKEDITOR.dom.nodeList(
2020 this.$.querySelectorAll( getContextualizedSelector( this, selector ) )
2021 );
2022
2023 removeTmpId();
2024
2025 return list;
2026 },
2027
2028 /**
2029 * Returns first element within this element that matches specified `selector`.
2030 *
2031 * **Notes:**
2032 *
2033 * * Not available in IE7.
2034 * * Unlike native `querySelectorAll` this method ensures selector contextualization. This is:
2035 *
2036 * HTML: '<body><div><i>foo</i></div></body>'
2037 * Native: div.querySelector( 'body i' ) // -> <i>foo</i>
2038 * Method: div.findOne( 'body i' ) // -> null
2039 * div.findOne( 'i' ) // -> <i>foo</i>
2040 *
2041 * @since 4.3
2042 * @param {String} selector
2043 * @returns {CKEDITOR.dom.element}
2044 */
2045 findOne: function( selector ) {
2046 var removeTmpId = createTmpId( this ),
2047 found = this.$.querySelector( getContextualizedSelector( this, selector ) );
2048
2049 removeTmpId();
2050
2051 return found ? new CKEDITOR.dom.element( found ) : null;
2052 },
2053
2054 /**
2055 * Traverse the DOM of this element (inclusive), executing a callback for
2056 * each node.
2057 *
2058 * var element = CKEDITOR.dom.element.createFromHtml( '<div><p>foo<b>bar</b>bom</p></div>' );
2059 * element.forEach( function( node ) {
2060 * console.log( node );
2061 * } );
2062 * // Will log:
2063 * // 1. <div> element,
2064 * // 2. <p> element,
2065 * // 3. "foo" text node,
2066 * // 4. <b> element,
2067 * // 5. "bar" text node,
2068 * // 6. "bom" text node.
2069 *
2070 * @since 4.3
2071 * @param {Function} callback Function to be executed on every node.
2072 * If `callback` returns `false` descendants of the node will be ignored.
2073 * @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument.
2074 * @param {Number} [type] If specified `callback` will be executed only on
2075 * nodes of this type.
2076 * @param {Boolean} [skipRoot] Don't execute `callback` on this element.
2077 */
2078 forEach: function( callback, type, skipRoot ) {
2079 if ( !skipRoot && ( !type || this.type == type ) )
2080 var ret = callback( this );
2081
2082 // Do not filter children if callback returned false.
2083 if ( ret === false )
2084 return;
2085
2086 var children = this.getChildren(),
2087 node,
2088 i = 0;
2089
2090 // We do not cache the size, because the live list of nodes may be changed by the callback.
2091 for ( ; i < children.count(); i++ ) {
2092 node = children.getItem( i );
2093 if ( node.type == CKEDITOR.NODE_ELEMENT )
2094 node.forEach( callback, type );
2095 else if ( !type || node.type == type )
2096 callback( node );
2097 }
2098 }
2099 } );
2100
2101 function createTmpId( element ) {
2102 var hadId = true;
2103
2104 if ( !element.$.id ) {
2105 element.$.id = 'cke_tmp_' + CKEDITOR.tools.getNextNumber();
2106 hadId = false;
2107 }
2108
2109 return function() {
2110 if ( !hadId )
2111 element.removeAttribute( 'id' );
2112 };
2113 }
2114
2115 function getContextualizedSelector( element, selector ) {
2116 var id = CKEDITOR.tools.escapeCss( element.$.id );
2117 return '#' + id + ' ' + selector.split( /,\s*/ ).join( ', #' + id + ' ' );
2118 }
2119
2120 var sides = {
2121 width: [ 'border-left-width', 'border-right-width', 'padding-left', 'padding-right' ],
2122 height: [ 'border-top-width', 'border-bottom-width', 'padding-top', 'padding-bottom' ]
2123 };
2124
2125 // Generate list of specific style rules, applicable to margin/padding/border.
2126 function expandedRules( style ) {
2127 var sides = [ 'top', 'left', 'right', 'bottom' ], components;
2128
2129 if ( style == 'border' )
2130 components = [ 'color', 'style', 'width' ];
2131
2132 var styles = [];
2133 for ( var i = 0 ; i < sides.length ; i++ ) {
2134
2135 if ( components ) {
2136 for ( var j = 0 ; j < components.length ; j++ )
2137 styles.push( [ style, sides[ i ], components[ j ] ].join( '-' ) );
2138 } else {
2139 styles.push( [ style, sides[ i ] ].join( '-' ) );
2140 }
2141 }
2142
2143 return styles;
2144 }
2145
2146 function marginAndPaddingSize( type ) {
2147 var adjustment = 0;
2148 for ( var i = 0, len = sides[ type ].length; i < len; i++ )
2149 adjustment += parseFloat( this.getComputedStyle( sides[ type ][ i ] ) || 0, 10 ) || 0;
2150 return adjustment;
2151 }
2152
2153 /**
2154 * Sets the element size considering the box model.
2155 *
2156 * @param {'width'/'height'} type The dimension to set.
2157 * @param {Number} size The length unit in px.
2158 * @param {Boolean} isBorderBox Apply the size based on the border box model.
2159 */
2160 CKEDITOR.dom.element.prototype.setSize = function( type, size, isBorderBox ) {
2161 if ( typeof size == 'number' ) {
2162 if ( isBorderBox && !( CKEDITOR.env.ie && CKEDITOR.env.quirks ) )
2163 size -= marginAndPaddingSize.call( this, type );
2164
2165 this.setStyle( type, size + 'px' );
2166 }
2167 };
2168
2169 /**
2170 * Gets the element size, possibly considering the box model.
2171 *
2172 * @param {'width'/'height'} type The dimension to get.
2173 * @param {Boolean} isBorderBox Get the size based on the border box model.
2174 */
2175 CKEDITOR.dom.element.prototype.getSize = function( type, isBorderBox ) {
2176 var size = Math.max( this.$[ 'offset' + CKEDITOR.tools.capitalize( type ) ], this.$[ 'client' + CKEDITOR.tools.capitalize( type ) ] ) || 0;
2177
2178 if ( isBorderBox )
2179 size -= marginAndPaddingSize.call( this, type );
2180
2181 return size;
2182 };
2183} )();
diff --git a/sources/core/dom/elementpath.js b/sources/core/dom/elementpath.js
new file mode 100644
index 0000000..1a3aed0
--- /dev/null
+++ b/sources/core/dom/elementpath.js
@@ -0,0 +1,251 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'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 0000000..8b1193a
--- /dev/null
+++ b/sources/core/dom/event.js
@@ -0,0 +1,208 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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 0000000..50056ec
--- /dev/null
+++ b/sources/core/dom/iterator.js
@@ -0,0 +1,565 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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 {@link 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 0000000..51bba18
--- /dev/null
+++ b/sources/core/dom/node.js
@@ -0,0 +1,902 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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 && isEmpty( current ) ) {
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 || isEmpty( current ) ) )
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 isEmpty( sibling ) ? getAdjacentNonEmptyTextNode( sibling, lookForward ) : sibling;
405 }
406
407 // Checks whether a text node is empty or is FCSeq string (which will be totally removed when normalizing).
408 function isEmpty( textNode ) {
409 return !textNode.nodeValue || textNode.nodeValue == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE;
410 }
411 },
412
413 /**
414 * @todo
415 */
416 getNextSourceNode: function( startFromSibling, nodeType, guard ) {
417 // If "guard" is a node, transform it in a function.
418 if ( guard && !guard.call ) {
419 var guardNode = guard;
420 guard = function( node ) {
421 return !node.equals( guardNode );
422 };
423 }
424
425 var node = ( !startFromSibling && this.getFirst && this.getFirst() ),
426 parent;
427
428 // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
429 // send the 'moving out' signal even we don't actually dive into.
430 if ( !node ) {
431 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
432 return null;
433 node = this.getNext();
434 }
435
436 while ( !node && ( parent = ( parent || this ).getParent() ) ) {
437 // The guard check sends the "true" paramenter to indicate that
438 // we are moving "out" of the element.
439 if ( guard && guard( parent, true ) === false )
440 return null;
441
442 node = parent.getNext();
443 }
444
445 if ( !node )
446 return null;
447
448 if ( guard && guard( node ) === false )
449 return null;
450
451 if ( nodeType && nodeType != node.type )
452 return node.getNextSourceNode( false, nodeType, guard );
453
454 return node;
455 },
456
457 /**
458 * @todo
459 */
460 getPreviousSourceNode: function( startFromSibling, nodeType, guard ) {
461 if ( guard && !guard.call ) {
462 var guardNode = guard;
463 guard = function( node ) {
464 return !node.equals( guardNode );
465 };
466 }
467
468 var node = ( !startFromSibling && this.getLast && this.getLast() ),
469 parent;
470
471 // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
472 // send the 'moving out' signal even we don't actually dive into.
473 if ( !node ) {
474 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
475 return null;
476 node = this.getPrevious();
477 }
478
479 while ( !node && ( parent = ( parent || this ).getParent() ) ) {
480 // The guard check sends the "true" paramenter to indicate that
481 // we are moving "out" of the element.
482 if ( guard && guard( parent, true ) === false )
483 return null;
484
485 node = parent.getPrevious();
486 }
487
488 if ( !node )
489 return null;
490
491 if ( guard && guard( node ) === false )
492 return null;
493
494 if ( nodeType && node.type != nodeType )
495 return node.getPreviousSourceNode( false, nodeType, guard );
496
497 return node;
498 },
499
500 /**
501 * Gets the node that preceeds this element in its parent's child list.
502 *
503 * var element = CKEDITOR.dom.element.createFromHtml( '<div><i>prev</i><b>Example</b></div>' );
504 * var first = element.getLast().getPrev();
505 * alert( first.getName() ); // 'i'
506 *
507 * @param {Function} [evaluator] Filtering the result node.
508 * @returns {CKEDITOR.dom.node} The previous node or null if not available.
509 */
510 getPrevious: function( evaluator ) {
511 var previous = this.$,
512 retval;
513 do {
514 previous = previous.previousSibling;
515
516 // Avoid returning the doc type node.
517 // http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927
518 retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous );
519 }
520 while ( retval && evaluator && !evaluator( retval ) );
521 return retval;
522 },
523
524 /**
525 * Gets the node that follows this element in its parent's child list.
526 *
527 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b><i>next</i></div>' );
528 * var last = element.getFirst().getNext();
529 * alert( last.getName() ); // 'i'
530 *
531 * @param {Function} [evaluator] Filtering the result node.
532 * @returns {CKEDITOR.dom.node} The next node or null if not available.
533 */
534 getNext: function( evaluator ) {
535 var next = this.$,
536 retval;
537 do {
538 next = next.nextSibling;
539 retval = next && new CKEDITOR.dom.node( next );
540 }
541 while ( retval && evaluator && !evaluator( retval ) );
542 return retval;
543 },
544
545 /**
546 * Gets the parent element for this node.
547 *
548 * var node = editor.document.getBody().getFirst();
549 * var parent = node.getParent();
550 * alert( parent.getName() ); // 'body'
551 *
552 * @param {Boolean} [allowFragmentParent=false] Consider also parent node that is of
553 * fragment type {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
554 * @returns {CKEDITOR.dom.element} The parent element.
555 */
556 getParent: function( allowFragmentParent ) {
557 var parent = this.$.parentNode;
558 return ( parent && ( parent.nodeType == CKEDITOR.NODE_ELEMENT || allowFragmentParent && parent.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) ? new CKEDITOR.dom.node( parent ) : null;
559 },
560
561 /**
562 * Returns an array containing node parents and the node itself. By default nodes are in _descending_ order.
563 *
564 * // Assuming that body has paragraph as the first child.
565 * var node = editor.document.getBody().getFirst();
566 * var parents = node.getParents();
567 * alert( parents[ 0 ].getName() + ',' + parents[ 2 ].getName() ); // 'html,p'
568 *
569 * @param {Boolean} [closerFirst=false] Determines the order of returned nodes.
570 * @returns {Array} Returns an array of {@link CKEDITOR.dom.node}.
571 */
572 getParents: function( closerFirst ) {
573 var node = this;
574 var parents = [];
575
576 do {
577 parents[ closerFirst ? 'push' : 'unshift' ]( node );
578 }
579 while ( ( node = node.getParent() ) );
580
581 return parents;
582 },
583
584 /**
585 * @todo
586 */
587 getCommonAncestor: function( node ) {
588 if ( node.equals( this ) )
589 return this;
590
591 if ( node.contains && node.contains( this ) )
592 return node;
593
594 var start = this.contains ? this : this.getParent();
595
596 do {
597 if ( start.contains( node ) ) return start;
598 }
599 while ( ( start = start.getParent() ) );
600
601 return null;
602 },
603
604 /**
605 * Determines the position relation between this node and the given {@link CKEDITOR.dom.node} in the document.
606 * This node can be preceding ({@link CKEDITOR#POSITION_PRECEDING}) or following ({@link CKEDITOR#POSITION_FOLLOWING})
607 * the given node. This node can also contain ({@link CKEDITOR#POSITION_CONTAINS}) or be contained by
608 * ({@link CKEDITOR#POSITION_IS_CONTAINED}) the given node. The function returns a bitmask of constants
609 * listed above or {@link CKEDITOR#POSITION_IDENTICAL} if the given node is the same as this node.
610 *
611 * @param {CKEDITOR.dom.node} otherNode A node to check relation with.
612 * @returns {Number} Position relation between this node and given node.
613 */
614 getPosition: function( otherNode ) {
615 var $ = this.$;
616 var $other = otherNode.$;
617
618 if ( $.compareDocumentPosition )
619 return $.compareDocumentPosition( $other );
620
621 // IE and Safari have no support for compareDocumentPosition.
622
623 if ( $ == $other )
624 return CKEDITOR.POSITION_IDENTICAL;
625
626 // Only element nodes support contains and sourceIndex.
627 if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) {
628 if ( $.contains ) {
629 if ( $.contains( $other ) )
630 return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING;
631
632 if ( $other.contains( $ ) )
633 return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
634 }
635
636 if ( 'sourceIndex' in $ )
637 return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
638
639 }
640
641 // For nodes that don't support compareDocumentPosition, contains
642 // or sourceIndex, their "address" is compared.
643
644 var addressOfThis = this.getAddress(),
645 addressOfOther = otherNode.getAddress(),
646 minLevel = Math.min( addressOfThis.length, addressOfOther.length );
647
648 // Determinate preceding/following relationship.
649 for ( var i = 0; i < minLevel; i++ ) {
650 if ( addressOfThis[ i ] != addressOfOther[ i ] ) {
651 return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
652 }
653 }
654
655 // Determinate contains/contained relationship.
656 return ( addressOfThis.length < addressOfOther.length ) ? CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
657 },
658
659 /**
660 * Gets the closest ancestor node of this node, specified by its name or using an evaluator function.
661 *
662 * // Suppose we have the following HTML structure:
663 * // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div>
664 * // If node == <b>
665 * ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner">
666 * ascendant = node.getAscendant( 'b' ); // ascendant == null
667 * ascendant = node.getAscendant( 'b', true ); // ascendant == <b>
668 * ascendant = node.getAscendant( { div:1, p:1 } ); // Searches for the first 'div' or 'p': ascendant == <div id="inner">
669 *
670 * // Using custom evaluator:
671 * ascendant = node.getAscendant( function( el ) {
672 * return el.getId() == 'inner';
673 * } );
674 * // ascendant == <div id="inner">
675 *
676 * @since 3.6.1
677 * @param {String/Function/Object} query The name of the ancestor node to search or
678 * an object with the node names to search for or an evaluator function.
679 * @param {Boolean} [includeSelf] Whether to include the current
680 * node in the search.
681 * @returns {CKEDITOR.dom.node} The located ancestor node or `null` if not found.
682 */
683 getAscendant: function( query, includeSelf ) {
684 var $ = this.$,
685 evaluator,
686 isCustomEvaluator;
687
688 if ( !includeSelf ) {
689 $ = $.parentNode;
690 }
691
692 // Custom checker provided in an argument.
693 if ( typeof query == 'function' ) {
694 isCustomEvaluator = true;
695 evaluator = query;
696 } else {
697 // Predefined tag name checker.
698 isCustomEvaluator = false;
699 evaluator = function( $ ) {
700 var name = ( typeof $.nodeName == 'string' ? $.nodeName.toLowerCase() : '' );
701
702 return ( typeof query == 'string' ? name == query : name in query );
703 };
704 }
705
706 while ( $ ) {
707 // For user provided checker we use CKEDITOR.dom.node.
708 if ( evaluator( isCustomEvaluator ? new CKEDITOR.dom.node( $ ) : $ ) ) {
709 return new CKEDITOR.dom.node( $ );
710 }
711
712 try {
713 $ = $.parentNode;
714 } catch ( e ) {
715 $ = null;
716 }
717 }
718
719 return null;
720 },
721
722 /**
723 * @todo
724 */
725 hasAscendant: function( name, includeSelf ) {
726 var $ = this.$;
727
728 if ( !includeSelf )
729 $ = $.parentNode;
730
731 while ( $ ) {
732 if ( $.nodeName && $.nodeName.toLowerCase() == name )
733 return true;
734
735 $ = $.parentNode;
736 }
737 return false;
738 },
739
740 /**
741 * @todo
742 */
743 move: function( target, toStart ) {
744 target.append( this.remove(), toStart );
745 },
746
747 /**
748 * Removes this node from the document DOM.
749 *
750 * var element = CKEDITOR.document.getById( 'MyElement' );
751 * element.remove();
752 *
753 * @param {Boolean} [preserveChildren=false] Indicates that the children
754 * elements must remain in the document, removing only the outer tags.
755 */
756 remove: function( preserveChildren ) {
757 var $ = this.$;
758 var parent = $.parentNode;
759
760 if ( parent ) {
761 if ( preserveChildren ) {
762 // Move all children before the node.
763 for ( var child;
764 ( child = $.firstChild ); ) {
765 parent.insertBefore( $.removeChild( child ), $ );
766 }
767 }
768
769 parent.removeChild( $ );
770 }
771
772 return this;
773 },
774
775 /**
776 * @todo
777 */
778 replace: function( nodeToReplace ) {
779 this.insertBefore( nodeToReplace );
780 nodeToReplace.remove();
781 },
782
783 /**
784 * @todo
785 */
786 trim: function() {
787 this.ltrim();
788 this.rtrim();
789 },
790
791 /**
792 * @todo
793 */
794 ltrim: function() {
795 var child;
796 while ( this.getFirst && ( child = this.getFirst() ) ) {
797 if ( child.type == CKEDITOR.NODE_TEXT ) {
798 var trimmed = CKEDITOR.tools.ltrim( child.getText() ),
799 originalLength = child.getLength();
800
801 if ( !trimmed ) {
802 child.remove();
803 continue;
804 } else if ( trimmed.length < originalLength ) {
805 child.split( originalLength - trimmed.length );
806
807 // IE BUG: child.remove() may raise JavaScript errors here. (#81)
808 this.$.removeChild( this.$.firstChild );
809 }
810 }
811 break;
812 }
813 },
814
815 /**
816 * @todo
817 */
818 rtrim: function() {
819 var child;
820 while ( this.getLast && ( child = this.getLast() ) ) {
821 if ( child.type == CKEDITOR.NODE_TEXT ) {
822 var trimmed = CKEDITOR.tools.rtrim( child.getText() ),
823 originalLength = child.getLength();
824
825 if ( !trimmed ) {
826 child.remove();
827 continue;
828 } else if ( trimmed.length < originalLength ) {
829 child.split( trimmed.length );
830
831 // IE BUG: child.getNext().remove() may raise JavaScript errors here.
832 // (#81)
833 this.$.lastChild.parentNode.removeChild( this.$.lastChild );
834 }
835 }
836 break;
837 }
838
839 if ( CKEDITOR.env.needsBrFiller ) {
840 child = this.$.lastChild;
841
842 if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) {
843 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324).
844 child.parentNode.removeChild( child );
845 }
846 }
847 },
848
849 /**
850 * Checks if this node is read-only (should not be changed).
851 *
852 * // For the following HTML:
853 * // <b>foo</b><div contenteditable="false"><i>bar</i></div>
854 *
855 * elB.isReadOnly(); // -> false
856 * foo.isReadOnly(); // -> false
857 * elDiv.isReadOnly(); // -> true
858 * elI.isReadOnly(); // -> true
859 *
860 * This method works in two modes depending on browser support for the `element.isContentEditable` property and
861 * the value of the `checkOnlyAttributes` parameter. The `element.isContentEditable` check is faster, but it is known
862 * to malfunction in hidden or detached nodes. Additionally, when processing some detached DOM tree you may want to imitate
863 * that this happens inside an editable container (like it would happen inside the {@link CKEDITOR.editable}). To do so,
864 * you can temporarily attach this tree to an element with the `data-cke-editable` attribute and use the
865 * `checkOnlyAttributes` mode.
866 *
867 * @since 3.5
868 * @param {Boolean} [checkOnlyAttributes=false] If `true`, only attributes will be checked, native methods will not
869 * be used. This parameter needs to be `true` to check hidden or detached elements. Introduced in 4.5.
870 * @returns {Boolean}
871 */
872 isReadOnly: function( checkOnlyAttributes ) {
873 var element = this;
874 if ( this.type != CKEDITOR.NODE_ELEMENT )
875 element = this.getParent();
876
877 // Prevent Edge crash (#13609, #13919).
878 if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) {
879 checkOnlyAttributes = true;
880 }
881
882 if ( !checkOnlyAttributes && element && typeof element.$.isContentEditable != 'undefined' ) {
883 return !( element.$.isContentEditable || element.data( 'cke-editable' ) );
884 }
885 else {
886 // Degrade for old browsers which don't support "isContentEditable", e.g. FF3
887
888 while ( element ) {
889 if ( element.data( 'cke-editable' ) ) {
890 return false;
891 } else if ( element.hasAttribute( 'contenteditable' ) ) {
892 return element.getAttribute( 'contenteditable' ) == 'false';
893 }
894
895 element = element.getParent();
896 }
897
898 // Reached the root of DOM tree, no editable found.
899 return true;
900 }
901 }
902} );
diff --git a/sources/core/dom/nodelist.js b/sources/core/dom/nodelist.js
new file mode 100644
index 0000000..165a415
--- /dev/null
+++ b/sources/core/dom/nodelist.js
@@ -0,0 +1,43 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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 0000000..6407074
--- /dev/null
+++ b/sources/core/dom/range.js
@@ -0,0 +1,2978 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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.getText().replace( CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE, '' ).length;
808
809 return sum;
810 }
811
812 function normalizeTextNodes( 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 one node 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, safe node.
826 // (safe == one for which getIndex(true) does not return -1, so one which won't disappear).
827 if ( container.type == CKEDITOR.NODE_ELEMENT && offset > 0 ) {
828 offset = getPrecedingSafeNodeIndex( container, offset ) + 1;
829 }
830
831 // The last step - fix the offset inside text node by adding
832 // lengths of preceding text nodes which will be merged with container.
833 if ( container.type == CKEDITOR.NODE_TEXT ) {
834 var precedingLength = getLengthOfPrecedingTextNodes( container );
835
836 // Normal case - text node is not empty.
837 if ( container.getText() ) {
838 offset += precedingLength;
839
840 // Awful case - the text node is empty and thus will be totally lost.
841 // In this case we are trying to normalize the limit to the left:
842 // * either to the preceding text node,
843 // * or to the "gap" after the preceding element.
844 } else {
845 // Find the closest non-text sibling.
846 var precedingContainer = container.getPrevious( isNotText );
847
848 // If there are any characters on the left, that means that we can anchor
849 // there, because this text node will not be lost.
850 if ( precedingLength ) {
851 offset = precedingLength;
852
853 if ( precedingContainer ) {
854 // The text node is the first node after the closest non-text sibling.
855 container = precedingContainer.getNext();
856 } else {
857 // But if there was no non-text sibling, then the text node is the first child.
858 container = container.getParent().getFirst();
859 }
860
861 // If there are no characters on the left, then anchor after the previous non-text node.
862 // E.g. (see tests for a legend :D):
863 // <b>x</b>(foo)({}bar) -> <b>x</b>[](foo)(bar)
864 } else {
865 container = container.getParent();
866 offset = precedingContainer ? ( precedingContainer.getIndex( true ) + 1 ) : 0;
867 }
868 }
869 }
870
871 limit.container = container;
872 limit.offset = offset;
873 }
874
875 function normalizeFCSeq( limit, root ) {
876 var fcseq = root.getCustomData( 'cke-fillingChar' );
877
878 if ( !fcseq ) {
879 return;
880 }
881
882 var container = limit.container;
883
884 if ( fcseq.equals( container ) ) {
885 limit.offset -= CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE.length;
886
887 // == 0 handles case when limit was at the end of FCS.
888 // < 0 handles all cases where limit was somewhere in the middle or at the beginning.
889 // > 0 (the "else" case) means cases where there are some more characters in the FCS node (FCSabc^def).
890 if ( limit.offset <= 0 ) {
891 limit.offset = container.getIndex();
892 limit.container = container.getParent();
893 }
894 return;
895 }
896
897 // And here goes the funny part - all other cases are handled inside node.getAddress() and getIndex() thanks to
898 // node.getIndex() being aware of FCS (handling it as an empty node).
899 }
900
901 // Finds a normalized index of a safe node preceding this one.
902 // Safe == one that will not disappear, so one for which getIndex( true ) does not return -1.
903 // Return -1 if there's no safe preceding node.
904 function getPrecedingSafeNodeIndex( container, offset ) {
905 var index;
906
907 while ( offset-- ) {
908 index = container.getChild( offset ).getIndex( true );
909
910 if ( index >= 0 )
911 return index;
912 }
913
914 return -1;
915 }
916
917 return function( normalized ) {
918 var collapsed = this.collapsed,
919 bmStart = {
920 container: this.startContainer,
921 offset: this.startOffset
922 },
923 bmEnd = {
924 container: this.endContainer,
925 offset: this.endOffset
926 };
927
928 if ( normalized ) {
929 normalizeTextNodes( bmStart );
930 normalizeFCSeq( bmStart, this.root );
931
932 if ( !collapsed ) {
933 normalizeTextNodes( bmEnd );
934 normalizeFCSeq( bmEnd, this.root );
935 }
936 }
937
938 return {
939 start: bmStart.container.getAddress( normalized ),
940 end: collapsed ? null : bmEnd.container.getAddress( normalized ),
941 startOffset: bmStart.offset,
942 endOffset: bmEnd.offset,
943 normalized: normalized,
944 collapsed: collapsed,
945 is2: true // It's a createBookmark2 bookmark.
946 };
947 };
948 } )(),
949
950 /**
951 * Moves this range to the given bookmark. See {@link #createBookmark} and {@link #createBookmark2}.
952 *
953 * If serializable bookmark passed, then its `<span>` markers will be removed.
954 *
955 * @param {Object} bookmark
956 */
957 moveToBookmark: function( bookmark ) {
958 // Created with createBookmark2().
959 if ( bookmark.is2 ) {
960 // Get the start information.
961 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
962 startOffset = bookmark.startOffset;
963
964 // Get the end information.
965 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
966 endOffset = bookmark.endOffset;
967
968 // Set the start boundary.
969 this.setStart( startContainer, startOffset );
970
971 // Set the end boundary. If not available, collapse it.
972 if ( endContainer )
973 this.setEnd( endContainer, endOffset );
974 else
975 this.collapse( true );
976 }
977 // Created with createBookmark().
978 else {
979 var serializable = bookmark.serializable,
980 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
981 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
982
983 // Set the range start at the bookmark start node position.
984 this.setStartBefore( startNode );
985
986 // Remove it, because it may interfere in the setEndBefore call.
987 startNode.remove();
988
989 // Set the range end at the bookmark end node position, or simply
990 // collapse it if it is not available.
991 if ( endNode ) {
992 this.setEndBefore( endNode );
993 endNode.remove();
994 } else {
995 this.collapse( true );
996 }
997 }
998 },
999
1000 /**
1001 * Returns two nodes which are on the boundaries of this range.
1002 *
1003 * @returns {Object}
1004 * @returns {CKEDITOR.dom.node} return.startNode
1005 * @returns {CKEDITOR.dom.node} return.endNode
1006 * @todo precise desc/algorithm
1007 */
1008 getBoundaryNodes: function() {
1009 var startNode = this.startContainer,
1010 endNode = this.endContainer,
1011 startOffset = this.startOffset,
1012 endOffset = this.endOffset,
1013 childCount;
1014
1015 if ( startNode.type == CKEDITOR.NODE_ELEMENT ) {
1016 childCount = startNode.getChildCount();
1017 if ( childCount > startOffset ) {
1018 startNode = startNode.getChild( startOffset );
1019 } else if ( childCount < 1 ) {
1020 startNode = startNode.getPreviousSourceNode();
1021 }
1022 // startOffset > childCount but childCount is not 0
1023 else {
1024 // Try to take the node just after the current position.
1025 startNode = startNode.$;
1026 while ( startNode.lastChild )
1027 startNode = startNode.lastChild;
1028 startNode = new CKEDITOR.dom.node( startNode );
1029
1030 // Normally we should take the next node in DFS order. But it
1031 // is also possible that we've already reached the end of
1032 // document.
1033 startNode = startNode.getNextSourceNode() || startNode;
1034 }
1035 }
1036
1037 if ( endNode.type == CKEDITOR.NODE_ELEMENT ) {
1038 childCount = endNode.getChildCount();
1039 if ( childCount > endOffset ) {
1040 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
1041 } else if ( childCount < 1 ) {
1042 endNode = endNode.getPreviousSourceNode();
1043 }
1044 // endOffset > childCount but childCount is not 0.
1045 else {
1046 // Try to take the node just before the current position.
1047 endNode = endNode.$;
1048 while ( endNode.lastChild )
1049 endNode = endNode.lastChild;
1050 endNode = new CKEDITOR.dom.node( endNode );
1051 }
1052 }
1053
1054 // Sometimes the endNode will come right before startNode for collapsed
1055 // ranges. Fix it. (#3780)
1056 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
1057 startNode = endNode;
1058
1059 return { startNode: startNode, endNode: endNode };
1060 },
1061
1062 /**
1063 * Find the node which fully contains the range.
1064 *
1065 * @param {Boolean} [includeSelf=false]
1066 * @param {Boolean} [ignoreTextNode=false] Whether ignore {@link CKEDITOR#NODE_TEXT} type.
1067 * @returns {CKEDITOR.dom.element}
1068 */
1069 getCommonAncestor: function( includeSelf, ignoreTextNode ) {
1070 var start = this.startContainer,
1071 end = this.endContainer,
1072 ancestor;
1073
1074 if ( start.equals( end ) ) {
1075 if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 )
1076 ancestor = start.getChild( this.startOffset );
1077 else
1078 ancestor = start;
1079 } else {
1080 ancestor = start.getCommonAncestor( end );
1081 }
1082
1083 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
1084 },
1085
1086 /**
1087 * Transforms the {@link #startContainer} and {@link #endContainer} properties from text
1088 * nodes to element nodes, whenever possible. This is actually possible
1089 * if either of the boundary containers point to a text node, and its
1090 * offset is set to zero, or after the last char in the node.
1091 */
1092 optimize: function() {
1093 var container = this.startContainer;
1094 var offset = this.startOffset;
1095
1096 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1097 if ( !offset )
1098 this.setStartBefore( container );
1099 else if ( offset >= container.getLength() )
1100 this.setStartAfter( container );
1101 }
1102
1103 container = this.endContainer;
1104 offset = this.endOffset;
1105
1106 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1107 if ( !offset )
1108 this.setEndBefore( container );
1109 else if ( offset >= container.getLength() )
1110 this.setEndAfter( container );
1111 }
1112 },
1113
1114 /**
1115 * Move the range out of bookmark nodes if they'd been the container.
1116 */
1117 optimizeBookmark: function() {
1118 var startNode = this.startContainer,
1119 endNode = this.endContainer;
1120
1121 if ( startNode.is && startNode.is( 'span' ) && startNode.data( 'cke-bookmark' ) )
1122 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
1123 if ( endNode && endNode.is && endNode.is( 'span' ) && endNode.data( 'cke-bookmark' ) )
1124 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
1125 },
1126
1127 /**
1128 * @param {Boolean} [ignoreStart=false]
1129 * @param {Boolean} [ignoreEnd=false]
1130 * @todo precise desc/algorithm
1131 */
1132 trim: function( ignoreStart, ignoreEnd ) {
1133 var startContainer = this.startContainer,
1134 startOffset = this.startOffset,
1135 collapsed = this.collapsed;
1136 if ( ( !ignoreStart || collapsed ) && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1137 // If the offset is zero, we just insert the new node before
1138 // the start.
1139 if ( !startOffset ) {
1140 startOffset = startContainer.getIndex();
1141 startContainer = startContainer.getParent();
1142 }
1143 // If the offset is at the end, we'll insert it after the text
1144 // node.
1145 else if ( startOffset >= startContainer.getLength() ) {
1146 startOffset = startContainer.getIndex() + 1;
1147 startContainer = startContainer.getParent();
1148 }
1149 // In other case, we split the text node and insert the new
1150 // node at the split point.
1151 else {
1152 var nextText = startContainer.split( startOffset );
1153
1154 startOffset = startContainer.getIndex() + 1;
1155 startContainer = startContainer.getParent();
1156
1157 // Check all necessity of updating the end boundary.
1158 if ( this.startContainer.equals( this.endContainer ) )
1159 this.setEnd( nextText, this.endOffset - this.startOffset );
1160 else if ( startContainer.equals( this.endContainer ) )
1161 this.endOffset += 1;
1162 }
1163
1164 this.setStart( startContainer, startOffset );
1165
1166 if ( collapsed ) {
1167 this.collapse( true );
1168 return;
1169 }
1170 }
1171
1172 var endContainer = this.endContainer;
1173 var endOffset = this.endOffset;
1174
1175 if ( !( ignoreEnd || collapsed ) && endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1176 // If the offset is zero, we just insert the new node before
1177 // the start.
1178 if ( !endOffset ) {
1179 endOffset = endContainer.getIndex();
1180 endContainer = endContainer.getParent();
1181 }
1182 // If the offset is at the end, we'll insert it after the text
1183 // node.
1184 else if ( endOffset >= endContainer.getLength() ) {
1185 endOffset = endContainer.getIndex() + 1;
1186 endContainer = endContainer.getParent();
1187 }
1188 // In other case, we split the text node and insert the new
1189 // node at the split point.
1190 else {
1191 endContainer.split( endOffset );
1192
1193 endOffset = endContainer.getIndex() + 1;
1194 endContainer = endContainer.getParent();
1195 }
1196
1197 this.setEnd( endContainer, endOffset );
1198 }
1199 },
1200
1201 /**
1202 * Expands the range so that partial units are completely contained.
1203 *
1204 * @param {Number} unit The unit type to expand with. Use one of following values: {@link CKEDITOR#ENLARGE_BLOCK_CONTENTS},
1205 * {@link CKEDITOR#ENLARGE_ELEMENT}, {@link CKEDITOR#ENLARGE_INLINE}, {@link CKEDITOR#ENLARGE_LIST_ITEM_CONTENTS}.
1206 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
1207 */
1208 enlarge: function( unit, excludeBrs ) {
1209 var leadingWhitespaceRegex = new RegExp( /[^\s\ufeff]/ );
1210
1211 switch ( unit ) {
1212 case CKEDITOR.ENLARGE_INLINE:
1213 var enlargeInlineOnly = 1;
1214
1215 /* falls through */
1216 case CKEDITOR.ENLARGE_ELEMENT:
1217
1218 if ( this.collapsed )
1219 return;
1220
1221 // Get the common ancestor.
1222 var commonAncestor = this.getCommonAncestor();
1223
1224 var boundary = this.root;
1225
1226 // For each boundary
1227 // a. Depending on its position, find out the first node to be checked (a sibling) or,
1228 // if not available, to be enlarge.
1229 // b. Go ahead checking siblings and enlarging the boundary as much as possible until the
1230 // common ancestor is not reached. After reaching the common ancestor, just save the
1231 // enlargeable node to be used later.
1232
1233 var startTop, endTop;
1234
1235 var enlargeable, sibling, commonReached;
1236
1237 // Indicates that the node can be added only if whitespace
1238 // is available before it.
1239 var needsWhiteSpace = false;
1240 var isWhiteSpace;
1241 var siblingText;
1242
1243 // Process the start boundary.
1244
1245 var container = this.startContainer;
1246 var offset = this.startOffset;
1247
1248 if ( container.type == CKEDITOR.NODE_TEXT ) {
1249 if ( offset ) {
1250 // Check if there is any non-space text before the
1251 // offset. Otherwise, container is null.
1252 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
1253
1254 // If we found only whitespace in the node, it
1255 // means that we'll need more whitespace to be able
1256 // to expand. For example, <i> can be expanded in
1257 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1258 needsWhiteSpace = !!container;
1259 }
1260
1261 if ( container ) {
1262 if ( !( sibling = container.getPrevious() ) )
1263 enlargeable = container.getParent();
1264 }
1265 } else {
1266 // If we have offset, get the node preceeding it as the
1267 // first sibling to be checked.
1268 if ( offset )
1269 sibling = container.getChild( offset - 1 ) || container.getLast();
1270
1271 // If there is no sibling, mark the container to be
1272 // enlarged.
1273 if ( !sibling )
1274 enlargeable = container;
1275 }
1276
1277 // Ensures that enlargeable can be indeed enlarged, if not it will be nulled.
1278 enlargeable = getValidEnlargeable( enlargeable );
1279
1280 while ( enlargeable || sibling ) {
1281 if ( enlargeable && !sibling ) {
1282 // If we reached the common ancestor, mark the flag
1283 // for it.
1284 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1285 commonReached = true;
1286
1287 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1288 break;
1289
1290 // If we don't need space or this element breaks
1291 // the line, then enlarge it.
1292 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1293 needsWhiteSpace = false;
1294
1295 // If the common ancestor has been reached,
1296 // we'll not enlarge it immediately, but just
1297 // mark it to be enlarged later if the end
1298 // boundary also enlarges it.
1299 if ( commonReached )
1300 startTop = enlargeable;
1301 else
1302 this.setStartBefore( enlargeable );
1303 }
1304
1305 sibling = enlargeable.getPrevious();
1306 }
1307
1308 // Check all sibling nodes preceeding the enlargeable
1309 // node. The node wil lbe enlarged only if none of them
1310 // blocks it.
1311 while ( sibling ) {
1312 // This flag indicates that this node has
1313 // whitespaces at the end.
1314 isWhiteSpace = false;
1315
1316 if ( sibling.type == CKEDITOR.NODE_COMMENT ) {
1317 sibling = sibling.getPrevious();
1318 continue;
1319 } else if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1320 siblingText = sibling.getText();
1321
1322 if ( leadingWhitespaceRegex.test( siblingText ) )
1323 sibling = null;
1324
1325 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
1326 } else {
1327 // #12221 (Chrome) plus #11111 (Safari).
1328 var offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
1329
1330 // If this is a visible element.
1331 // We need to check for the bookmark attribute because IE insists on
1332 // rendering the display:none nodes we use for bookmarks. (#3363)
1333 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1334 if ( ( sibling.$.offsetWidth > offsetWidth0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1335 // We'll accept it only if we need
1336 // whitespace, and this is an inline
1337 // element with whitespace only.
1338 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1339 // It must contains spaces and inline elements only.
1340
1341 siblingText = sibling.getText();
1342
1343 if ( leadingWhitespaceRegex.test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
1344 sibling = null;
1345 else {
1346 var allChildren = sibling.$.getElementsByTagName( '*' );
1347 for ( var i = 0, child; child = allChildren[ i++ ]; ) {
1348 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1349 sibling = null;
1350 break;
1351 }
1352 }
1353 }
1354
1355 if ( sibling )
1356 isWhiteSpace = !!siblingText.length;
1357 } else {
1358 sibling = null;
1359 }
1360 }
1361 }
1362
1363 // A node with whitespaces has been found.
1364 if ( isWhiteSpace ) {
1365 // Enlarge the last enlargeable node, if we
1366 // were waiting for spaces.
1367 if ( needsWhiteSpace ) {
1368 if ( commonReached )
1369 startTop = enlargeable;
1370 else if ( enlargeable )
1371 this.setStartBefore( enlargeable );
1372 } else {
1373 needsWhiteSpace = true;
1374 }
1375 }
1376
1377 if ( sibling ) {
1378 var next = sibling.getPrevious();
1379
1380 if ( !enlargeable && !next ) {
1381 // Set the sibling as enlargeable, so it's
1382 // parent will be get later outside this while.
1383 enlargeable = sibling;
1384 sibling = null;
1385 break;
1386 }
1387
1388 sibling = next;
1389 } else {
1390 // If sibling has been set to null, then we
1391 // need to stop enlarging.
1392 enlargeable = null;
1393 }
1394 }
1395
1396 if ( enlargeable )
1397 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1398 }
1399
1400 // Process the end boundary. This is basically the same
1401 // code used for the start boundary, with small changes to
1402 // make it work in the oposite side (to the right). This
1403 // makes it difficult to reuse the code here. So, fixes to
1404 // the above code are likely to be replicated here.
1405
1406 container = this.endContainer;
1407 offset = this.endOffset;
1408
1409 // Reset the common variables.
1410 enlargeable = sibling = null;
1411 commonReached = needsWhiteSpace = false;
1412
1413 // Function check if there are only whitespaces from the given starting point
1414 // (startContainer and startOffset) till the end on block.
1415 // Examples ("[" is the start point):
1416 // - <p>foo[ </p> - will return true,
1417 // - <p><b>foo[ </b> </p> - will return true,
1418 // - <p>foo[ bar</p> - will return false,
1419 // - <p><b>foo[ </b>bar</p> - will return false,
1420 // - <p>foo[ <b></b></p> - will return false.
1421 function onlyWhiteSpaces( startContainer, startOffset ) {
1422 // We need to enlarge range if there is white space at the end of the block,
1423 // because it is not displayed in WYSIWYG mode and user can not select it. So
1424 // "<p>foo[bar] </p>" should be changed to "<p>foo[bar ]</p>". On the other hand
1425 // we should do nothing if we are not at the end of the block, so this should not
1426 // be changed: "<p><i>[foo] </i>bar</p>".
1427 var walkerRange = new CKEDITOR.dom.range( boundary );
1428 walkerRange.setStart( startContainer, startOffset );
1429 // The guard will find the end of range so I put boundary here.
1430 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1431
1432 var walker = new CKEDITOR.dom.walker( walkerRange ),
1433 node;
1434
1435 walker.guard = function( node ) {
1436 // Stop if you exit block.
1437 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() );
1438 };
1439
1440 while ( ( node = walker.next() ) ) {
1441 if ( node.type != CKEDITOR.NODE_TEXT ) {
1442 // Stop if you enter to any node (walker.next() will return node only
1443 // it goes out, not if it is go into node).
1444 return false;
1445 } else {
1446 // Trim the first node to startOffset.
1447 if ( node != startContainer )
1448 siblingText = node.getText();
1449 else
1450 siblingText = node.substring( startOffset );
1451
1452 // Check if it is white space.
1453 if ( leadingWhitespaceRegex.test( siblingText ) )
1454 return false;
1455 }
1456 }
1457
1458 return true;
1459 }
1460
1461 if ( container.type == CKEDITOR.NODE_TEXT ) {
1462 // Check if there is only white space after the offset.
1463 if ( CKEDITOR.tools.trim( container.substring( offset ) ).length ) {
1464 // If we found only whitespace in the node, it
1465 // means that we'll need more whitespace to be able
1466 // to expand. For example, <i> can be expanded in
1467 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1468 needsWhiteSpace = true;
1469 } else {
1470 needsWhiteSpace = !container.getLength();
1471
1472 if ( offset == container.getLength() ) {
1473 // If we are at the end of container and this is the last text node,
1474 // we should enlarge end to the parent.
1475 if ( !( sibling = container.getNext() ) )
1476 enlargeable = container.getParent();
1477 } else {
1478 // If we are in the middle on text node and there are only whitespaces
1479 // till the end of block, we should enlarge element.
1480 if ( onlyWhiteSpaces( container, offset ) )
1481 enlargeable = container.getParent();
1482 }
1483 }
1484 } else {
1485 // Get the node right after the boudary to be checked
1486 // first.
1487 sibling = container.getChild( offset );
1488
1489 if ( !sibling )
1490 enlargeable = container;
1491 }
1492
1493 while ( enlargeable || sibling ) {
1494 if ( enlargeable && !sibling ) {
1495 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1496 commonReached = true;
1497
1498 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1499 break;
1500
1501 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1502 needsWhiteSpace = false;
1503
1504 if ( commonReached )
1505 endTop = enlargeable;
1506 else if ( enlargeable )
1507 this.setEndAfter( enlargeable );
1508 }
1509
1510 sibling = enlargeable.getNext();
1511 }
1512
1513 while ( sibling ) {
1514 isWhiteSpace = false;
1515
1516 if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1517 siblingText = sibling.getText();
1518
1519 // Check if there are not whitespace characters till the end of editable.
1520 // If so stop expanding.
1521 if ( !onlyWhiteSpaces( sibling, 0 ) )
1522 sibling = null;
1523
1524 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
1525 } else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) {
1526 // If this is a visible element.
1527 // We need to check for the bookmark attribute because IE insists on
1528 // rendering the display:none nodes we use for bookmarks. (#3363)
1529 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1530 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1531 // We'll accept it only if we need
1532 // whitespace, and this is an inline
1533 // element with whitespace only.
1534 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1535 // It must contains spaces and inline elements only.
1536
1537 siblingText = sibling.getText();
1538
1539 if ( leadingWhitespaceRegex.test( siblingText ) )
1540 sibling = null;
1541 else {
1542 allChildren = sibling.$.getElementsByTagName( '*' );
1543 for ( i = 0; child = allChildren[ i++ ]; ) {
1544 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1545 sibling = null;
1546 break;
1547 }
1548 }
1549 }
1550
1551 if ( sibling )
1552 isWhiteSpace = !!siblingText.length;
1553 } else {
1554 sibling = null;
1555 }
1556 }
1557 } else {
1558 isWhiteSpace = 1;
1559 }
1560
1561 if ( isWhiteSpace ) {
1562 if ( needsWhiteSpace ) {
1563 if ( commonReached )
1564 endTop = enlargeable;
1565 else
1566 this.setEndAfter( enlargeable );
1567 }
1568 }
1569
1570 if ( sibling ) {
1571 next = sibling.getNext();
1572
1573 if ( !enlargeable && !next ) {
1574 enlargeable = sibling;
1575 sibling = null;
1576 break;
1577 }
1578
1579 sibling = next;
1580 } else {
1581 // If sibling has been set to null, then we
1582 // need to stop enlarging.
1583 enlargeable = null;
1584 }
1585 }
1586
1587 if ( enlargeable )
1588 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1589 }
1590
1591 // If the common ancestor can be enlarged by both boundaries, then include it also.
1592 if ( startTop && endTop ) {
1593 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
1594
1595 this.setStartBefore( commonAncestor );
1596 this.setEndAfter( commonAncestor );
1597 }
1598 break;
1599
1600 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
1601 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
1602
1603 // Enlarging the start boundary.
1604 var walkerRange = new CKEDITOR.dom.range( this.root );
1605
1606 boundary = this.root;
1607
1608 walkerRange.setStartAt( boundary, CKEDITOR.POSITION_AFTER_START );
1609 walkerRange.setEnd( this.startContainer, this.startOffset );
1610
1611 var walker = new CKEDITOR.dom.walker( walkerRange ),
1612 blockBoundary, // The node on which the enlarging should stop.
1613 tailBr, // In case BR as block boundary.
1614 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary( ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br: 1 } : null ),
1615 inNonEditable = null,
1616 // Record the encountered 'blockBoundary' for later use.
1617 boundaryGuard = function( node ) {
1618 // We should not check contents of non-editable elements. It may happen
1619 // that inline widget has display:table child which should not block range#enlarge.
1620 // When encoutered non-editable element...
1621 if ( node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'false' ) {
1622 if ( inNonEditable ) {
1623 // ... in which we already were, reset it (because we're leaving it) and return.
1624 if ( inNonEditable.equals( node ) ) {
1625 inNonEditable = null;
1626 return;
1627 }
1628 // ... which we're entering, remember it but check it (no return).
1629 } else {
1630 inNonEditable = node;
1631 }
1632 // When we are in non-editable element, do not check if current node is a block boundary.
1633 } else if ( inNonEditable ) {
1634 return;
1635 }
1636
1637 var retval = notBlockBoundary( node );
1638 if ( !retval )
1639 blockBoundary = node;
1640 return retval;
1641 },
1642 // Record the encounted 'tailBr' for later use.
1643 tailBrGuard = function( node ) {
1644 var retval = boundaryGuard( node );
1645 if ( !retval && node.is && node.is( 'br' ) )
1646 tailBr = node;
1647 return retval;
1648 };
1649
1650 walker.guard = boundaryGuard;
1651
1652 enlargeable = walker.lastBackward();
1653
1654 // It's the body which stop the enlarging if no block boundary found.
1655 blockBoundary = blockBoundary || boundary;
1656
1657 // Start the range either after the end of found block (<p>...</p>[text)
1658 // or at the start of block (<p>[text...), by comparing the document position
1659 // with 'enlargeable' node.
1660 this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() ||
1661 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END );
1662
1663 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
1664 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) {
1665 var theRange = this.clone();
1666 walker = new CKEDITOR.dom.walker( theRange );
1667
1668 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
1669 bookmark = CKEDITOR.dom.walker.bookmark();
1670
1671 walker.evaluator = function( node ) {
1672 return !whitespaces( node ) && !bookmark( node );
1673 };
1674 var previous = walker.previous();
1675 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
1676 return;
1677 }
1678
1679 // Enlarging the end boundary.
1680 // Set up new range and reset all flags (blockBoundary, inNonEditable, tailBr).
1681
1682 walkerRange = this.clone();
1683 walkerRange.collapse();
1684 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1685 walker = new CKEDITOR.dom.walker( walkerRange );
1686
1687 // tailBrGuard only used for on range end.
1688 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? tailBrGuard : boundaryGuard;
1689 blockBoundary = inNonEditable = tailBr = null;
1690
1691 // End the range right before the block boundary node.
1692 enlargeable = walker.lastForward();
1693
1694 // It's the body which stop the enlarging if no block boundary found.
1695 blockBoundary = blockBoundary || boundary;
1696
1697 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
1698 // by comparing the document position with 'enlargeable' node.
1699 this.setEndAt( blockBoundary, ( !enlargeable && this.checkEndOfBlock() ||
1700 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_BEFORE_START );
1701 // We must include the <br> at the end of range if there's
1702 // one and we're expanding list item contents
1703 if ( tailBr ) {
1704 this.setEndAfter( tailBr );
1705 }
1706 }
1707
1708 // Ensures that returned element can be enlarged by selection, null otherwise.
1709 // @param {CKEDITOR.dom.element} enlargeable
1710 // @returns {CKEDITOR.dom.element/null}
1711 function getValidEnlargeable( enlargeable ) {
1712 return enlargeable && enlargeable.type == CKEDITOR.NODE_ELEMENT && enlargeable.hasAttribute( 'contenteditable' ) ?
1713 null : enlargeable;
1714 }
1715 },
1716
1717 /**
1718 * Descrease the range to make sure that boundaries
1719 * always anchor beside text nodes or innermost element.
1720 *
1721 * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}).
1722 *
1723 * * {@link CKEDITOR#SHRINK_ELEMENT} - Shrink the range boundaries to the edge of the innermost element.
1724 * * {@link CKEDITOR#SHRINK_TEXT} - Shrink the range boudaries to anchor by the side of enclosed text
1725 * node, range remains if there's no text nodes on boundaries at all.
1726 *
1727 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
1728 */
1729 shrink: function( mode, selectContents, shrinkOnBlockBoundary ) {
1730 // Unable to shrink a collapsed range.
1731 if ( !this.collapsed ) {
1732 mode = mode || CKEDITOR.SHRINK_TEXT;
1733
1734 var walkerRange = this.clone();
1735
1736 var startContainer = this.startContainer,
1737 endContainer = this.endContainer,
1738 startOffset = this.startOffset,
1739 endOffset = this.endOffset;
1740
1741 // Whether the start/end boundary is moveable.
1742 var moveStart = 1,
1743 moveEnd = 1;
1744
1745 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1746 if ( !startOffset )
1747 walkerRange.setStartBefore( startContainer );
1748 else if ( startOffset >= startContainer.getLength() )
1749 walkerRange.setStartAfter( startContainer );
1750 else {
1751 // Enlarge the range properly to avoid walker making
1752 // DOM changes caused by triming the text nodes later.
1753 walkerRange.setStartBefore( startContainer );
1754 moveStart = 0;
1755 }
1756 }
1757
1758 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1759 if ( !endOffset )
1760 walkerRange.setEndBefore( endContainer );
1761 else if ( endOffset >= endContainer.getLength() )
1762 walkerRange.setEndAfter( endContainer );
1763 else {
1764 walkerRange.setEndAfter( endContainer );
1765 moveEnd = 0;
1766 }
1767 }
1768
1769 var walker = new CKEDITOR.dom.walker( walkerRange ),
1770 isBookmark = CKEDITOR.dom.walker.bookmark();
1771
1772 walker.evaluator = function( node ) {
1773 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
1774 };
1775
1776 var currentElement;
1777 walker.guard = function( node, movingOut ) {
1778 if ( isBookmark( node ) )
1779 return true;
1780
1781 // Stop when we're shrink in element mode while encountering a text node.
1782 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
1783 return false;
1784
1785 // Stop when we've already walked "through" an element.
1786 if ( movingOut && node.equals( currentElement ) )
1787 return false;
1788
1789 if ( shrinkOnBlockBoundary === false && node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() )
1790 return false;
1791
1792 // Stop shrinking when encountering an editable border.
1793 if ( node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'contenteditable' ) )
1794 return false;
1795
1796 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
1797 currentElement = node;
1798
1799 return true;
1800 };
1801
1802 if ( moveStart ) {
1803 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next' ]();
1804 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
1805 }
1806
1807 if ( moveEnd ) {
1808 walker.reset();
1809 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous' ]();
1810 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
1811 }
1812
1813 return !!( moveStart || moveEnd );
1814 }
1815 },
1816
1817 /**
1818 * Inserts a node at the start of the range. The range will be expanded
1819 * the contain the node.
1820 *
1821 * @param {CKEDITOR.dom.node} node
1822 */
1823 insertNode: function( node ) {
1824 this.optimizeBookmark();
1825 this.trim( false, true );
1826
1827 var startContainer = this.startContainer;
1828 var startOffset = this.startOffset;
1829
1830 var nextNode = startContainer.getChild( startOffset );
1831
1832 if ( nextNode )
1833 node.insertBefore( nextNode );
1834 else
1835 startContainer.append( node );
1836
1837 // Check if we need to update the end boundary.
1838 if ( node.getParent() && node.getParent().equals( this.endContainer ) )
1839 this.endOffset++;
1840
1841 // Expand the range to embrace the new node.
1842 this.setStartBefore( node );
1843 },
1844
1845 /**
1846 * Moves the range to given position according to specified node.
1847 *
1848 * // HTML: <p>Foo <b>bar</b></p>
1849 * range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START );
1850 * // Range will be moved to: <p>Foo ^<b>bar</b></p>
1851 *
1852 * See also {@link #setStartAt} and {@link #setEndAt}.
1853 *
1854 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
1855 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1856 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1857 * {@link CKEDITOR#POSITION_AFTER_END}.
1858 */
1859 moveToPosition: function( node, position ) {
1860 this.setStartAt( node, position );
1861 this.collapse( true );
1862 },
1863
1864 /**
1865 * Moves the range to the exact position of the specified range.
1866 *
1867 * @param {CKEDITOR.dom.range} range
1868 */
1869 moveToRange: function( range ) {
1870 this.setStart( range.startContainer, range.startOffset );
1871 this.setEnd( range.endContainer, range.endOffset );
1872 },
1873
1874 /**
1875 * Select nodes content. Range will start and end inside this node.
1876 *
1877 * @param {CKEDITOR.dom.node} node
1878 */
1879 selectNodeContents: function( node ) {
1880 this.setStart( node, 0 );
1881 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
1882 },
1883
1884 /**
1885 * Sets the start position of a range.
1886 *
1887 * @param {CKEDITOR.dom.node} startNode The node to start the range.
1888 * @param {Number} startOffset An integer greater than or equal to zero
1889 * representing the offset for the start of the range from the start
1890 * of `startNode`.
1891 */
1892 setStart: function( startNode, startOffset ) {
1893 // W3C requires a check for the new position. If it is after the end
1894 // boundary, the range should be collapsed to the new start. It seams
1895 // we will not need this check for our use of this class so we can
1896 // ignore it for now.
1897
1898 // Fixing invalid range start inside dtd empty elements.
1899 if ( startNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ startNode.getName() ] )
1900 startOffset = startNode.getIndex(), startNode = startNode.getParent();
1901
1902 this._setStartContainer( startNode );
1903 this.startOffset = startOffset;
1904
1905 if ( !this.endContainer ) {
1906 this._setEndContainer( startNode );
1907 this.endOffset = startOffset;
1908 }
1909
1910 updateCollapsed( this );
1911 },
1912
1913 /**
1914 * Sets the end position of a Range.
1915 *
1916 * @param {CKEDITOR.dom.node} endNode The node to end the range.
1917 * @param {Number} endOffset An integer greater than or equal to zero
1918 * representing the offset for the end of the range from the start
1919 * of `endNode`.
1920 */
1921 setEnd: function( endNode, endOffset ) {
1922 // W3C requires a check for the new position. If it is before the start
1923 // boundary, the range should be collapsed to the new end. It seams we
1924 // will not need this check for our use of this class so we can ignore
1925 // it for now.
1926
1927 // Fixing invalid range end inside dtd empty elements.
1928 if ( endNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ endNode.getName() ] )
1929 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
1930
1931 this._setEndContainer( endNode );
1932 this.endOffset = endOffset;
1933
1934 if ( !this.startContainer ) {
1935 this._setStartContainer( endNode );
1936 this.startOffset = endOffset;
1937 }
1938
1939 updateCollapsed( this );
1940 },
1941
1942 /**
1943 * Sets start of this range after the specified node.
1944 *
1945 * // Range: <p>foo<b>bar</b>^</p>
1946 * range.setStartAfter( textFoo );
1947 * // The range will be changed to:
1948 * // <p>foo[<b>bar</b>]</p>
1949 *
1950 * @param {CKEDITOR.dom.node} node
1951 */
1952 setStartAfter: function( node ) {
1953 this.setStart( node.getParent(), node.getIndex() + 1 );
1954 },
1955
1956 /**
1957 * Sets start of this range after the specified node.
1958 *
1959 * // Range: <p>foo<b>bar</b>^</p>
1960 * range.setStartBefore( elB );
1961 * // The range will be changed to:
1962 * // <p>foo[<b>bar</b>]</p>
1963 *
1964 * @param {CKEDITOR.dom.node} node
1965 */
1966 setStartBefore: function( node ) {
1967 this.setStart( node.getParent(), node.getIndex() );
1968 },
1969
1970 /**
1971 * Sets end of this range after the specified node.
1972 *
1973 * // Range: <p>foo^<b>bar</b></p>
1974 * range.setEndAfter( elB );
1975 * // The range will be changed to:
1976 * // <p>foo[<b>bar</b>]</p>
1977 *
1978 * @param {CKEDITOR.dom.node} node
1979 */
1980 setEndAfter: function( node ) {
1981 this.setEnd( node.getParent(), node.getIndex() + 1 );
1982 },
1983
1984 /**
1985 * Sets end of this range before the specified node.
1986 *
1987 * // Range: <p>^foo<b>bar</b></p>
1988 * range.setStartAfter( textBar );
1989 * // The range will be changed to:
1990 * // <p>[foo<b>]bar</b></p>
1991 *
1992 * @param {CKEDITOR.dom.node} node
1993 */
1994 setEndBefore: function( node ) {
1995 this.setEnd( node.getParent(), node.getIndex() );
1996 },
1997
1998 /**
1999 * Moves the start of this range to given position according to specified node.
2000 *
2001 * // HTML: <p>Foo <b>bar</b>^</p>
2002 * range.setStartAt( elB, CKEDITOR.POSITION_AFTER_START );
2003 * // The range will be changed to:
2004 * // <p>Foo <b>[bar</b>]</p>
2005 *
2006 * See also {@link #setEndAt} and {@link #moveToPosition}.
2007 *
2008 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2009 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2010 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2011 * {@link CKEDITOR#POSITION_AFTER_END}.
2012 */
2013 setStartAt: function( node, position ) {
2014 switch ( position ) {
2015 case CKEDITOR.POSITION_AFTER_START:
2016 this.setStart( node, 0 );
2017 break;
2018
2019 case CKEDITOR.POSITION_BEFORE_END:
2020 if ( node.type == CKEDITOR.NODE_TEXT )
2021 this.setStart( node, node.getLength() );
2022 else
2023 this.setStart( node, node.getChildCount() );
2024 break;
2025
2026 case CKEDITOR.POSITION_BEFORE_START:
2027 this.setStartBefore( node );
2028 break;
2029
2030 case CKEDITOR.POSITION_AFTER_END:
2031 this.setStartAfter( node );
2032 }
2033
2034 updateCollapsed( this );
2035 },
2036
2037 /**
2038 * Moves the end of this range to given position according to specified node.
2039 *
2040 * // HTML: <p>^Foo <b>bar</b></p>
2041 * range.setEndAt( textBar, CKEDITOR.POSITION_BEFORE_START );
2042 * // The range will be changed to:
2043 * // <p>[Foo <b>]bar</b></p>
2044 *
2045 * See also {@link #setStartAt} and {@link #moveToPosition}.
2046 *
2047 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2048 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2049 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2050 * {@link CKEDITOR#POSITION_AFTER_END}.
2051 */
2052 setEndAt: function( node, position ) {
2053 switch ( position ) {
2054 case CKEDITOR.POSITION_AFTER_START:
2055 this.setEnd( node, 0 );
2056 break;
2057
2058 case CKEDITOR.POSITION_BEFORE_END:
2059 if ( node.type == CKEDITOR.NODE_TEXT )
2060 this.setEnd( node, node.getLength() );
2061 else
2062 this.setEnd( node, node.getChildCount() );
2063 break;
2064
2065 case CKEDITOR.POSITION_BEFORE_START:
2066 this.setEndBefore( node );
2067 break;
2068
2069 case CKEDITOR.POSITION_AFTER_END:
2070 this.setEndAfter( node );
2071 }
2072
2073 updateCollapsed( this );
2074 },
2075
2076 /**
2077 * Wraps inline content found around the range's start or end boundary
2078 * with a block element.
2079 *
2080 * // Assuming the following range:
2081 * // <h1>foo</h1>ba^r<br />bom<p>foo</p>
2082 * // The result of executing:
2083 * range.fixBlock( true, 'p' );
2084 * // will be:
2085 * // <h1>foo</h1><p>ba^r<br />bom</p><p>foo</p>
2086 *
2087 * Non-collapsed range:
2088 *
2089 * // Assuming the following range:
2090 * // ba[r<p>foo</p>bo]m
2091 * // The result of executing:
2092 * range.fixBlock( false, 'p' );
2093 * // will be:
2094 * // ba[r<p>foo</p><p>bo]m</p>
2095 *
2096 * @param {Boolean} isStart Whether the start or end boundary of a range should be checked.
2097 * @param {String} blockTag The name of a block element in which content will be wrapped.
2098 * For example: `'p'`.
2099 * @returns {CKEDITOR.dom.element} Created block wrapper.
2100 */
2101 fixBlock: function( isStart, blockTag ) {
2102 var bookmark = this.createBookmark(),
2103 fixedBlock = this.document.createElement( blockTag );
2104
2105 this.collapse( isStart );
2106
2107 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
2108
2109 this.extractContents().appendTo( fixedBlock );
2110 fixedBlock.trim();
2111
2112 this.insertNode( fixedBlock );
2113
2114 // Bogus <br> could already exist in the range's container before fixBlock() was called. In such case it was
2115 // extracted and appended to the fixBlock. However, we are not sure that it's at the end of
2116 // the fixedBlock, because of FF's terrible bug. When creating a bookmark in an empty editable
2117 // FF moves the bogus <br> before that bookmark (<editable><br /><bm />[]</editable>).
2118 // So even if the initial range was placed before the bogus <br>, after creating the bookmark it
2119 // is placed before the bookmark.
2120 // Fortunately, getBogus() is able to skip the bookmark so it finds the bogus <br> in this case.
2121 // We remove incorrectly placed one and add a brand new one. (#13001)
2122 var bogus = fixedBlock.getBogus();
2123 if ( bogus ) {
2124 bogus.remove();
2125 }
2126 fixedBlock.appendBogus();
2127
2128 this.moveToBookmark( bookmark );
2129
2130 return fixedBlock;
2131 },
2132
2133 /**
2134 * @todo
2135 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result blocks.
2136 */
2137 splitBlock: function( blockTag, cloneId ) {
2138 var startPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ),
2139 endPath = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2140
2141 var startBlockLimit = startPath.blockLimit,
2142 endBlockLimit = endPath.blockLimit;
2143
2144 var startBlock = startPath.block,
2145 endBlock = endPath.block;
2146
2147 var elementPath = null;
2148 // Do nothing if the boundaries are in different block limits.
2149 if ( !startBlockLimit.equals( endBlockLimit ) )
2150 return null;
2151
2152 // Get or fix current blocks.
2153 if ( blockTag != 'br' ) {
2154 if ( !startBlock ) {
2155 startBlock = this.fixBlock( true, blockTag );
2156 endBlock = new CKEDITOR.dom.elementPath( this.endContainer, this.root ).block;
2157 }
2158
2159 if ( !endBlock )
2160 endBlock = this.fixBlock( false, blockTag );
2161 }
2162
2163 // Get the range position.
2164 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
2165 isEndOfBlock = endBlock && this.checkEndOfBlock();
2166
2167 // Delete the current contents.
2168 // TODO: Why is 2.x doing CheckIsEmpty()?
2169 this.deleteContents();
2170
2171 if ( startBlock && startBlock.equals( endBlock ) ) {
2172 if ( isEndOfBlock ) {
2173 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2174 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
2175 endBlock = null;
2176 } else if ( isStartOfBlock ) {
2177 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2178 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
2179 startBlock = null;
2180 } else {
2181 endBlock = this.splitElement( startBlock, cloneId || false );
2182
2183 // In Gecko, the last child node must be a bogus <br>.
2184 // Note: bogus <br> added under <ul> or <ol> would cause
2185 // lists to be incorrectly rendered.
2186 if ( !startBlock.is( 'ul', 'ol' ) )
2187 startBlock.appendBogus();
2188 }
2189 }
2190
2191 return {
2192 previousBlock: startBlock,
2193 nextBlock: endBlock,
2194 wasStartOfBlock: isStartOfBlock,
2195 wasEndOfBlock: isEndOfBlock,
2196 elementPath: elementPath
2197 };
2198 },
2199
2200 /**
2201 * Branch the specified element from the collapsed range position and
2202 * place the caret between the two result branches.
2203 *
2204 * **Note:** The range must be collapsed and been enclosed by this element.
2205 *
2206 * @param {CKEDITOR.dom.element} element
2207 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result elements.
2208 * @returns {CKEDITOR.dom.element} Root element of the new branch after the split.
2209 */
2210 splitElement: function( toSplit, cloneId ) {
2211 if ( !this.collapsed )
2212 return null;
2213
2214 // Extract the contents of the block from the selection point to the end
2215 // of its contents.
2216 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
2217 var documentFragment = this.extractContents( false, cloneId || false );
2218
2219 // Duplicate the element after it.
2220 var clone = toSplit.clone( false, cloneId || false );
2221
2222 // Place the extracted contents into the duplicated element.
2223 documentFragment.appendTo( clone );
2224 clone.insertAfter( toSplit );
2225 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
2226 return clone;
2227 },
2228
2229 /**
2230 * Recursively remove any empty path blocks at the range boundary.
2231 *
2232 * @method
2233 * @param {Boolean} atEnd Removal to perform at the end boundary,
2234 * otherwise to perform at the start.
2235 */
2236 removeEmptyBlocksAtEnd: ( function() {
2237
2238 var whitespace = CKEDITOR.dom.walker.whitespaces(),
2239 bookmark = CKEDITOR.dom.walker.bookmark( false );
2240
2241 function childEval( parent ) {
2242 return function( node ) {
2243 // Whitespace, bookmarks, empty inlines.
2244 if ( whitespace( node ) || bookmark( node ) ||
2245 node.type == CKEDITOR.NODE_ELEMENT &&
2246 node.isEmptyInlineRemoveable() ) {
2247 return false;
2248 } else if ( parent.is( 'table' ) && node.is( 'caption' ) ) {
2249 return false;
2250 }
2251
2252 return true;
2253 };
2254 }
2255
2256 return function( atEnd ) {
2257
2258 var bm = this.createBookmark();
2259 var path = this[ atEnd ? 'endPath' : 'startPath' ]();
2260 var block = path.block || path.blockLimit, parent;
2261
2262 // Remove any childless block, including list and table.
2263 while ( block && !block.equals( path.root ) &&
2264 !block.getFirst( childEval( block ) ) ) {
2265 parent = block.getParent();
2266 this[ atEnd ? 'setEndAt' : 'setStartAt' ]( block, CKEDITOR.POSITION_AFTER_END );
2267 block.remove( 1 );
2268 block = parent;
2269 }
2270
2271 this.moveToBookmark( bm );
2272 };
2273
2274 } )(),
2275
2276 /**
2277 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #startContainer}.
2278 *
2279 * @returns {CKEDITOR.dom.elementPath}
2280 */
2281 startPath: function() {
2282 return new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2283 },
2284
2285 /**
2286 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #endContainer}.
2287 *
2288 * @returns {CKEDITOR.dom.elementPath}
2289 */
2290 endPath: function() {
2291 return new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2292 },
2293
2294 /**
2295 * Check whether a range boundary is at the inner boundary of a given
2296 * element.
2297 *
2298 * @param {CKEDITOR.dom.element} element The target element to check.
2299 * @param {Number} checkType The boundary to check for both the range
2300 * and the element. It can be {@link CKEDITOR#START} or {@link CKEDITOR#END}.
2301 * @returns {Boolean} `true` if the range boundary is at the inner
2302 * boundary of the element.
2303 */
2304 checkBoundaryOfElement: function( element, checkType ) {
2305 var checkStart = ( checkType == CKEDITOR.START );
2306
2307 // Create a copy of this range, so we can manipulate it for our checks.
2308 var walkerRange = this.clone();
2309
2310 // Collapse the range at the proper size.
2311 walkerRange.collapse( checkStart );
2312
2313 // Expand the range to element boundary.
2314 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
2315
2316 // Create the walker, which will check if we have anything useful
2317 // in the range.
2318 var walker = new CKEDITOR.dom.walker( walkerRange );
2319 walker.evaluator = elementBoundaryEval( checkStart );
2320
2321 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
2322 },
2323
2324 /**
2325 * **Note:** Calls to this function may produce changes to the DOM. The range may
2326 * be updated to reflect such changes.
2327 *
2328 * @returns {Boolean}
2329 * @todo
2330 */
2331 checkStartOfBlock: function() {
2332 var startContainer = this.startContainer,
2333 startOffset = this.startOffset;
2334
2335 // [IE] Special handling for range start in text with a leading NBSP,
2336 // we it to be isolated, for bogus check.
2337 if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) {
2338 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
2339 if ( nbspRegExp.test( textBefore ) )
2340 this.trim( 0, 1 );
2341 }
2342
2343 // Antecipate the trim() call here, so the walker will not make
2344 // changes to the DOM, which would not get reflected into this
2345 // range otherwise.
2346 this.trim();
2347
2348 // We need to grab the block element holding the start boundary, so
2349 // let's use an element path for it.
2350 var path = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2351
2352 // Creates a range starting at the block start until the range start.
2353 var walkerRange = this.clone();
2354 walkerRange.collapse( true );
2355 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
2356
2357 var walker = new CKEDITOR.dom.walker( walkerRange );
2358 walker.evaluator = getCheckStartEndBlockEvalFunction();
2359
2360 return walker.checkBackward();
2361 },
2362
2363 /**
2364 * **Note:** Calls to this function may produce changes to the DOM. The range may
2365 * be updated to reflect such changes.
2366 *
2367 * @returns {Boolean}
2368 * @todo
2369 */
2370 checkEndOfBlock: function() {
2371 var endContainer = this.endContainer,
2372 endOffset = this.endOffset;
2373
2374 // [IE] Special handling for range end in text with a following NBSP,
2375 // we it to be isolated, for bogus check.
2376 if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT ) {
2377 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
2378 if ( nbspRegExp.test( textAfter ) )
2379 this.trim( 1, 0 );
2380 }
2381
2382 // Antecipate the trim() call here, so the walker will not make
2383 // changes to the DOM, which would not get reflected into this
2384 // range otherwise.
2385 this.trim();
2386
2387 // We need to grab the block element holding the start boundary, so
2388 // let's use an element path for it.
2389 var path = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2390
2391 // Creates a range starting at the block start until the range start.
2392 var walkerRange = this.clone();
2393 walkerRange.collapse( false );
2394 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
2395
2396 var walker = new CKEDITOR.dom.walker( walkerRange );
2397 walker.evaluator = getCheckStartEndBlockEvalFunction();
2398
2399 return walker.checkForward();
2400 },
2401
2402 /**
2403 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.
2404 *
2405 * @param {Function} evaluator Function used as the walker's evaluator.
2406 * @param {Function} [guard] Function used as the walker's guard.
2407 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2408 * default to the root editable if not defined.
2409 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2410 */
2411 getPreviousNode: function( evaluator, guard, boundary ) {
2412 var walkerRange = this.clone();
2413 walkerRange.collapse( 1 );
2414 walkerRange.setStartAt( boundary || this.root, CKEDITOR.POSITION_AFTER_START );
2415
2416 var walker = new CKEDITOR.dom.walker( walkerRange );
2417 walker.evaluator = evaluator;
2418 walker.guard = guard;
2419 return walker.previous();
2420 },
2421
2422 /**
2423 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.
2424 *
2425 * @param {Function} evaluator Function used as the walker's evaluator.
2426 * @param {Function} [guard] Function used as the walker's guard.
2427 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2428 * default to the root editable if not defined.
2429 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2430 */
2431 getNextNode: function( evaluator, guard, boundary ) {
2432 var walkerRange = this.clone();
2433 walkerRange.collapse();
2434 walkerRange.setEndAt( boundary || this.root, CKEDITOR.POSITION_BEFORE_END );
2435
2436 var walker = new CKEDITOR.dom.walker( walkerRange );
2437 walker.evaluator = evaluator;
2438 walker.guard = guard;
2439 return walker.next();
2440 },
2441
2442 /**
2443 * Check if elements at which the range boundaries anchor are read-only,
2444 * with respect to `contenteditable` attribute.
2445 *
2446 * @returns {Boolean}
2447 */
2448 checkReadOnly: ( function() {
2449 function checkNodesEditable( node, anotherEnd ) {
2450 while ( node ) {
2451 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
2452 if ( node.getAttribute( 'contentEditable' ) == 'false' && !node.data( 'cke-editable' ) )
2453 return 0;
2454
2455 // Range enclosed entirely in an editable element.
2456 else if ( node.is( 'html' ) || node.getAttribute( 'contentEditable' ) == 'true' && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
2457 break;
2458
2459 }
2460 node = node.getParent();
2461 }
2462
2463 return 1;
2464 }
2465
2466 return function() {
2467 var startNode = this.startContainer,
2468 endNode = this.endContainer;
2469
2470 // Check if elements path at both boundaries are editable.
2471 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
2472 };
2473 } )(),
2474
2475 /**
2476 * Moves the range boundaries to the first/end editing point inside an
2477 * element.
2478 *
2479 * For example, in an element tree like
2480 * `<p><b><i></i></b> Text</p>`, the start editing point is
2481 * `<p><b><i>^</i></b> Text</p>` (inside `<i>`).
2482 *
2483 * @param {CKEDITOR.dom.element} el The element into which look for the
2484 * editing spot.
2485 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
2486 * @returns {Boolean} Whether range was moved.
2487 */
2488 moveToElementEditablePosition: function( el, isMoveToEnd ) {
2489
2490 function nextDFS( node, childOnly ) {
2491 var next;
2492
2493 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )
2494 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( notIgnoredEval );
2495
2496 if ( !childOnly && !next )
2497 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( notIgnoredEval );
2498
2499 return next;
2500 }
2501
2502 // Handle non-editable element e.g. HR.
2503 if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) {
2504 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2505 return true;
2506 }
2507
2508 var found = 0;
2509
2510 while ( el ) {
2511 // Stop immediately if we've found a text node.
2512 if ( el.type == CKEDITOR.NODE_TEXT ) {
2513 // Put cursor before block filler.
2514 if ( isMoveToEnd && this.endContainer && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )
2515 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2516 else
2517 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2518 found = 1;
2519 break;
2520 }
2521
2522 // If an editable element is found, move inside it, but not stop the searching.
2523 if ( el.type == CKEDITOR.NODE_ELEMENT ) {
2524 if ( el.isEditable() ) {
2525 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_START );
2526 found = 1;
2527 }
2528 // Put cursor before padding block br.
2529 else if ( isMoveToEnd && el.is( 'br' ) && this.endContainer && this.checkEndOfBlock() )
2530 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2531 // Special case - non-editable block. Select entire element, because it does not make sense
2532 // to place collapsed selection next to it, because browsers can't handle that.
2533 else if ( el.getAttribute( 'contenteditable' ) == 'false' && el.is( CKEDITOR.dtd.$block ) ) {
2534 this.setStartBefore( el );
2535 this.setEndAfter( el );
2536 return true;
2537 }
2538 }
2539
2540 el = nextDFS( el, found );
2541 }
2542
2543 return !!found;
2544 },
2545
2546 /**
2547 * Moves the range boundaries to the closest editing point after/before an
2548 * element or the current range position (depends on whether the element was specified).
2549 *
2550 * For example, if the start element has `id="start"`,
2551 * `<p><b>foo</b><span id="start">start</start></p>`, the closest previous editing point is
2552 * `<p><b>foo</b>^<span id="start">start</start></p>` (between `<b>` and `<span>`).
2553 *
2554 * See also: {@link #moveToElementEditablePosition}.
2555 *
2556 * @since 4.3
2557 * @param {CKEDITOR.dom.element} [element] The starting element. If not specified, the current range
2558 * position will be used.
2559 * @param {Boolean} [isMoveForward] Whether move to the end of editable. Otherwise, look back.
2560 * @returns {Boolean} Whether the range was moved.
2561 */
2562 moveToClosestEditablePosition: function( element, isMoveForward ) {
2563 // We don't want to modify original range if there's no editable position.
2564 var range,
2565 found = 0,
2566 sibling,
2567 isElement,
2568 positions = [ CKEDITOR.POSITION_AFTER_END, CKEDITOR.POSITION_BEFORE_START ];
2569
2570 if ( element ) {
2571 // Set collapsed range at one of ends of element.
2572 // Can't clone this range, because this range might not be yet positioned (no containers => errors).
2573 range = new CKEDITOR.dom.range( this.root );
2574 range.moveToPosition( element, positions[ isMoveForward ? 0 : 1 ] );
2575 } else {
2576 range = this.clone();
2577 }
2578
2579 // Start element isn't a block, so we can automatically place range
2580 // next to it.
2581 if ( element && !element.is( CKEDITOR.dtd.$block ) )
2582 found = 1;
2583 else {
2584 // Look for first node that fulfills eval function and place range next to it.
2585 sibling = range[ isMoveForward ? 'getNextEditableNode' : 'getPreviousEditableNode' ]();
2586 if ( sibling ) {
2587 found = 1;
2588 isElement = sibling.type == CKEDITOR.NODE_ELEMENT;
2589
2590 // Special case - eval accepts block element only if it's a non-editable block,
2591 // which we want to select, not place collapsed selection next to it (which browsers
2592 // can't handle).
2593 if ( isElement && sibling.is( CKEDITOR.dtd.$block ) && sibling.getAttribute( 'contenteditable' ) == 'false' ) {
2594 range.setStartAt( sibling, CKEDITOR.POSITION_BEFORE_START );
2595 range.setEndAt( sibling, CKEDITOR.POSITION_AFTER_END );
2596 }
2597 // Handle empty blocks which can be selection containers on old IEs.
2598 else if ( !CKEDITOR.env.needsBrFiller && isElement && sibling.is( CKEDITOR.dom.walker.validEmptyBlockContainers ) ) {
2599 range.setEnd( sibling, 0 );
2600 range.collapse();
2601 } else {
2602 range.moveToPosition( sibling, positions[ isMoveForward ? 1 : 0 ] );
2603 }
2604 }
2605 }
2606
2607 if ( found )
2608 this.moveToRange( range );
2609
2610 return !!found;
2611 },
2612
2613 /**
2614 * See {@link #moveToElementEditablePosition}.
2615 *
2616 * @returns {Boolean} Whether range was moved.
2617 */
2618 moveToElementEditStart: function( target ) {
2619 return this.moveToElementEditablePosition( target );
2620 },
2621
2622 /**
2623 * See {@link #moveToElementEditablePosition}.
2624 *
2625 * @returns {Boolean} Whether range was moved.
2626 */
2627 moveToElementEditEnd: function( target ) {
2628 return this.moveToElementEditablePosition( target, true );
2629 },
2630
2631 /**
2632 * Get the single node enclosed within the range if there's one.
2633 *
2634 * @returns {CKEDITOR.dom.node}
2635 */
2636 getEnclosedNode: function() {
2637 var walkerRange = this.clone();
2638
2639 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
2640 walkerRange.optimize();
2641 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
2642 return null;
2643
2644 var walker = new CKEDITOR.dom.walker( walkerRange ),
2645 isNotBookmarks = CKEDITOR.dom.walker.bookmark( false, true ),
2646 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true );
2647
2648 walker.evaluator = function( node ) {
2649 return isNotWhitespaces( node ) && isNotBookmarks( node );
2650 };
2651 var node = walker.next();
2652 walker.reset();
2653 return node && node.equals( walker.previous() ) ? node : null;
2654 },
2655
2656 /**
2657 * Get the node adjacent to the range start or {@link #startContainer}.
2658 *
2659 * @returns {CKEDITOR.dom.node}
2660 */
2661 getTouchedStartNode: function() {
2662 var container = this.startContainer;
2663
2664 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2665 return container;
2666
2667 return container.getChild( this.startOffset ) || container;
2668 },
2669
2670 /**
2671 * Get the node adjacent to the range end or {@link #endContainer}.
2672 *
2673 * @returns {CKEDITOR.dom.node}
2674 */
2675 getTouchedEndNode: function() {
2676 var container = this.endContainer;
2677
2678 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2679 return container;
2680
2681 return container.getChild( this.endOffset - 1 ) || container;
2682 },
2683
2684 /**
2685 * Gets next node which can be a container of a selection.
2686 * This methods mimics a behavior of right/left arrow keys in case of
2687 * collapsed selection. It does not return an exact position (with offset) though,
2688 * but just a selection's container.
2689 *
2690 * Note: use this method on a collapsed range.
2691 *
2692 * @since 4.3
2693 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2694 */
2695 getNextEditableNode: getNextEditableNode(),
2696
2697 /**
2698 * See {@link #getNextEditableNode}.
2699 *
2700 * @since 4.3
2701 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2702 */
2703 getPreviousEditableNode: getNextEditableNode( 1 ),
2704
2705 /**
2706 * Scrolls the start of current range into view.
2707 */
2708 scrollIntoView: function() {
2709
2710 // The reference element contains a zero-width space to avoid
2711 // a premature removal. The view is to be scrolled with respect
2712 // to this element.
2713 var reference = new CKEDITOR.dom.element.createFromHtml( '<span>&nbsp;</span>', this.document ),
2714 afterCaretNode, startContainerText, isStartText;
2715
2716 var range = this.clone();
2717
2718 // Work with the range to obtain a proper caret position.
2719 range.optimize();
2720
2721 // Currently in a text node, so we need to split it into two
2722 // halves and put the reference between.
2723 if ( isStartText = range.startContainer.type == CKEDITOR.NODE_TEXT ) {
2724 // Keep the original content. It will be restored.
2725 startContainerText = range.startContainer.getText();
2726
2727 // Split the startContainer at the this position.
2728 afterCaretNode = range.startContainer.split( range.startOffset );
2729
2730 // Insert the reference between two text nodes.
2731 reference.insertAfter( range.startContainer );
2732 }
2733
2734 // If not in a text node, simply insert the reference into the range.
2735 else {
2736 range.insertNode( reference );
2737 }
2738
2739 // Scroll with respect to the reference element.
2740 reference.scrollIntoView();
2741
2742 // Get rid of split parts if "in a text node" case.
2743 // Revert the original text of the startContainer.
2744 if ( isStartText ) {
2745 range.startContainer.setText( startContainerText );
2746 afterCaretNode.remove();
2747 }
2748
2749 // Get rid of the reference node. It is no longer necessary.
2750 reference.remove();
2751 },
2752
2753 /**
2754 * Setter for the {@link #startContainer}.
2755 *
2756 * @since 4.4.6
2757 * @private
2758 * @param {CKEDITOR.dom.element} startContainer
2759 */
2760 _setStartContainer: function( startContainer ) {
2761 // %REMOVE_START%
2762 var isRootAscendantOrSelf = this.root.equals( startContainer ) || this.root.contains( startContainer );
2763
2764 if ( !isRootAscendantOrSelf ) {
2765 CKEDITOR.warn( 'range-startcontainer', { startContainer: startContainer, root: this.root } );
2766 }
2767 // %REMOVE_END%
2768 this.startContainer = startContainer;
2769 },
2770
2771 /**
2772 * Setter for the {@link #endContainer}.
2773 *
2774 * @since 4.4.6
2775 * @private
2776 * @param {CKEDITOR.dom.element} endContainer
2777 */
2778 _setEndContainer: function( endContainer ) {
2779 // %REMOVE_START%
2780 var isRootAscendantOrSelf = this.root.equals( endContainer ) || this.root.contains( endContainer );
2781
2782 if ( !isRootAscendantOrSelf ) {
2783 CKEDITOR.warn( 'range-endcontainer', { endContainer: endContainer, root: this.root } );
2784 }
2785 // %REMOVE_END%
2786 this.endContainer = endContainer;
2787 },
2788
2789 /**
2790 * Looks for elements matching the `query` selector within a range.
2791 *
2792 * @since 4.5.11
2793 * @private
2794 * @param {String} query
2795 * @param {Boolean} [includeNonEditables=false] Whether elements with `contenteditable` set to `false` should
2796 * be included.
2797 * @returns {CKEDITOR.dom.element[]}
2798 */
2799 _find: function( query, includeNonEditables ) {
2800 var ancestor = this.getCommonAncestor(),
2801 boundaries = this.getBoundaryNodes(),
2802 // Contrary to CKEDITOR.dom.element#find we're returning array, that's because NodeList is immutable, and we need
2803 // to do some filtering in returned list.
2804 ret = [],
2805 curItem,
2806 i,
2807 initialMatches,
2808 isStartGood,
2809 isEndGood;
2810
2811 if ( ancestor && ancestor.find ) {
2812 initialMatches = ancestor.find( query );
2813
2814 for ( i = 0; i < initialMatches.count(); i++ ) {
2815 curItem = initialMatches.getItem( i );
2816
2817 // Using isReadOnly() method to filterout non editables. It checks isContentEditable including all browser quirks.
2818 if ( !includeNonEditables && curItem.isReadOnly() ) {
2819 continue;
2820 }
2821
2822 // It's not enough to get elements from common ancestor, because it migth contain too many matches.
2823 // We need to ensure that returned items are between boundary points.
2824 isStartGood = ( curItem.getPosition( boundaries.startNode ) & CKEDITOR.POSITION_FOLLOWING ) || boundaries.startNode.equals( curItem );
2825 isEndGood = ( curItem.getPosition( boundaries.endNode ) & ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IS_CONTAINED ) );
2826
2827 if ( isStartGood && isEndGood ) {
2828 ret.push( curItem );
2829 }
2830 }
2831 }
2832
2833 return ret;
2834 }
2835 };
2836
2837
2838} )();
2839
2840/**
2841 * Indicates a position after start of a node.
2842 *
2843 * // When used according to an element:
2844 * // <element>^contents</element>
2845 *
2846 * // When used according to a text node:
2847 * // "^text" (range is anchored in the text node)
2848 *
2849 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2850 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2851 *
2852 * @readonly
2853 * @member CKEDITOR
2854 * @property {Number} [=1]
2855 */
2856CKEDITOR.POSITION_AFTER_START = 1;
2857
2858/**
2859 * Indicates a position before end of a node.
2860 *
2861 * // When used according to an element:
2862 * // <element>contents^</element>
2863 *
2864 * // When used according to a text node:
2865 * // "text^" (range is anchored in the text node)
2866 *
2867 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2868 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2869 *
2870 * @readonly
2871 * @member CKEDITOR
2872 * @property {Number} [=2]
2873 */
2874CKEDITOR.POSITION_BEFORE_END = 2;
2875
2876/**
2877 * Indicates a position before start of a node.
2878 *
2879 * // When used according to an element:
2880 * // ^<element>contents</element> (range is anchored in element's parent)
2881 *
2882 * // When used according to a text node:
2883 * // ^"text" (range is anchored in text node's parent)
2884 *
2885 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2886 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2887 *
2888 * @readonly
2889 * @member CKEDITOR
2890 * @property {Number} [=3]
2891 */
2892CKEDITOR.POSITION_BEFORE_START = 3;
2893
2894/**
2895 * Indicates a position after end of a node.
2896 *
2897 * // When used according to an element:
2898 * // <element>contents</element>^ (range is anchored in element's parent)
2899 *
2900 * // When used according to a text node:
2901 * // "text"^ (range is anchored in text node's parent)
2902 *
2903 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2904 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2905 *
2906 * @readonly
2907 * @member CKEDITOR
2908 * @property {Number} [=4]
2909 */
2910CKEDITOR.POSITION_AFTER_END = 4;
2911
2912/**
2913 * @readonly
2914 * @member CKEDITOR
2915 * @property {Number} [=1]
2916 */
2917CKEDITOR.ENLARGE_ELEMENT = 1;
2918
2919/**
2920 * @readonly
2921 * @member CKEDITOR
2922 * @property {Number} [=2]
2923 */
2924CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
2925
2926/**
2927 * @readonly
2928 * @member CKEDITOR
2929 * @property {Number} [=3]
2930 */
2931CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
2932
2933/**
2934 * @readonly
2935 * @member CKEDITOR
2936 * @property {Number} [=4]
2937 */
2938CKEDITOR.ENLARGE_INLINE = 4;
2939
2940// Check boundary types.
2941
2942/**
2943 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2944 *
2945 * @readonly
2946 * @member CKEDITOR
2947 * @property {Number} [=1]
2948 */
2949CKEDITOR.START = 1;
2950
2951/**
2952 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2953 *
2954 * @readonly
2955 * @member CKEDITOR
2956 * @property {Number} [=2]
2957 */
2958CKEDITOR.END = 2;
2959
2960// Shrink range types.
2961
2962/**
2963 * See {@link CKEDITOR.dom.range#shrink}.
2964 *
2965 * @readonly
2966 * @member CKEDITOR
2967 * @property {Number} [=1]
2968 */
2969CKEDITOR.SHRINK_ELEMENT = 1;
2970
2971/**
2972 * See {@link CKEDITOR.dom.range#shrink}.
2973 *
2974 * @readonly
2975 * @member CKEDITOR
2976 * @property {Number} [=2]
2977 */
2978CKEDITOR.SHRINK_TEXT = 2;
diff --git a/sources/core/dom/rangelist.js b/sources/core/dom/rangelist.js
new file mode 100644
index 0000000..250dfd9
--- /dev/null
+++ b/sources/core/dom/rangelist.js
@@ -0,0 +1,199 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /**
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 0000000..ce20ffe
--- /dev/null
+++ b/sources/core/dom/text.js
@@ -0,0 +1,135 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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 0000000..cec4574
--- /dev/null
+++ b/sources/core/dom/walker.js
@@ -0,0 +1,652 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 // 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 Filling Char Sequence text node used in Webkit. (#9384, #13816)
392 isWhitespace = !CKEDITOR.tools.trim( node.getText() ) ||
393 CKEDITOR.env.webkit && node.getText() == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE;
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 0000000..ceeaeff
--- /dev/null
+++ b/sources/core/dom/window.js
@@ -0,0 +1,95 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
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} );