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