diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-02-19 23:38:52 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-02-19 23:38:52 +0100 |
commit | 3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5 (patch) | |
tree | a4f77655fe55b79606e7d3416504686a1ab8b058 /sources/core | |
download | piedsjaloux-ckeditor-component-3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5.tar.gz piedsjaloux-ckeditor-component-3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5.tar.zst piedsjaloux-ckeditor-component-3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5.zip |
Initial commit4.5.7
Diffstat (limited to 'sources/core')
59 files changed, 31187 insertions, 0 deletions
diff --git a/sources/core/_bootstrap.js b/sources/core/_bootstrap.js new file mode 100644 index 0000000..9fcbe25 --- /dev/null +++ b/sources/core/_bootstrap.js | |||
@@ -0,0 +1,74 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview API initialization code. | ||
8 | */ | ||
9 | |||
10 | ( function() { | ||
11 | // Disable HC detection in WebKit. (#5429) | ||
12 | if ( CKEDITOR.env.webkit ) | ||
13 | CKEDITOR.env.hc = false; | ||
14 | else { | ||
15 | // Check whether high contrast is active by creating a colored border. | ||
16 | var hcDetect = CKEDITOR.dom.element.createFromHtml( '<div style="width:0;height:0;position:absolute;left:-10000px;' + | ||
17 | 'border:1px solid;border-color:red blue"></div>', CKEDITOR.document ); | ||
18 | |||
19 | hcDetect.appendTo( CKEDITOR.document.getHead() ); | ||
20 | |||
21 | // Update CKEDITOR.env. | ||
22 | // Catch exception needed sometimes for FF. (#4230) | ||
23 | try { | ||
24 | var top = hcDetect.getComputedStyle( 'border-top-color' ), | ||
25 | right = hcDetect.getComputedStyle( 'border-right-color' ); | ||
26 | |||
27 | // We need to check if getComputedStyle returned any value, because on FF | ||
28 | // it returnes empty string if CKEditor is loaded in hidden iframe. (#11121) | ||
29 | CKEDITOR.env.hc = !!( top && top == right ); | ||
30 | } catch ( e ) { | ||
31 | CKEDITOR.env.hc = false; | ||
32 | } | ||
33 | |||
34 | hcDetect.remove(); | ||
35 | } | ||
36 | |||
37 | if ( CKEDITOR.env.hc ) | ||
38 | CKEDITOR.env.cssClass += ' cke_hc'; | ||
39 | |||
40 | // Initially hide UI spaces when relevant skins are loading, later restored by skin css. | ||
41 | CKEDITOR.document.appendStyleText( '.cke{visibility:hidden;}' ); | ||
42 | |||
43 | // Mark the editor as fully loaded. | ||
44 | CKEDITOR.status = 'loaded'; | ||
45 | CKEDITOR.fireOnce( 'loaded' ); | ||
46 | |||
47 | // Process all instances created by the "basic" implementation. | ||
48 | var pending = CKEDITOR._.pending; | ||
49 | if ( pending ) { | ||
50 | delete CKEDITOR._.pending; | ||
51 | |||
52 | for ( var i = 0; i < pending.length; i++ ) { | ||
53 | CKEDITOR.editor.prototype.constructor.apply( pending[ i ][ 0 ], pending[ i ][ 1 ] ); | ||
54 | CKEDITOR.add( pending[ i ][ 0 ] ); | ||
55 | } | ||
56 | } | ||
57 | } )(); | ||
58 | |||
59 | /** | ||
60 | * Indicates that CKEditor is running on a High Contrast environment. | ||
61 | * | ||
62 | * if ( CKEDITOR.env.hc ) | ||
63 | * alert( 'You\'re running on High Contrast mode. The editor interface will get adapted to provide you a better experience.' ); | ||
64 | * | ||
65 | * @property {Boolean} hc | ||
66 | * @member CKEDITOR.env | ||
67 | */ | ||
68 | |||
69 | /** | ||
70 | * Fired when a CKEDITOR core object is fully loaded and ready for interaction. | ||
71 | * | ||
72 | * @event loaded | ||
73 | * @member CKEDITOR | ||
74 | */ | ||
diff --git a/sources/core/ckeditor.js b/sources/core/ckeditor.js new file mode 100644 index 0000000..2b3e5cd --- /dev/null +++ b/sources/core/ckeditor.js | |||
@@ -0,0 +1,204 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Contains the third and last part of the {@link CKEDITOR} object | ||
8 | * definition. | ||
9 | */ | ||
10 | |||
11 | /** @class CKEDITOR */ | ||
12 | |||
13 | // Remove the CKEDITOR.loadFullCore reference defined on ckeditor_basic. | ||
14 | delete CKEDITOR.loadFullCore; | ||
15 | |||
16 | /** | ||
17 | * Stores references to all editor instances created. The name of the properties | ||
18 | * in this object correspond to instance names, and their values contain the | ||
19 | * {@link CKEDITOR.editor} object representing them. | ||
20 | * | ||
21 | * alert( CKEDITOR.instances.editor1.name ); // 'editor1' | ||
22 | * | ||
23 | * @property {Object} | ||
24 | */ | ||
25 | CKEDITOR.instances = {}; | ||
26 | |||
27 | /** | ||
28 | * The document of the window storing the CKEDITOR object. | ||
29 | * | ||
30 | * alert( CKEDITOR.document.getBody().getName() ); // 'body' | ||
31 | * | ||
32 | * @property {CKEDITOR.dom.document} | ||
33 | */ | ||
34 | CKEDITOR.document = new CKEDITOR.dom.document( document ); | ||
35 | |||
36 | /** | ||
37 | * Adds an editor instance to the global {@link CKEDITOR} object. This function | ||
38 | * is available for internal use mainly. | ||
39 | * | ||
40 | * @param {CKEDITOR.editor} editor The editor instance to be added. | ||
41 | */ | ||
42 | CKEDITOR.add = function( editor ) { | ||
43 | CKEDITOR.instances[ editor.name ] = editor; | ||
44 | |||
45 | editor.on( 'focus', function() { | ||
46 | if ( CKEDITOR.currentInstance != editor ) { | ||
47 | CKEDITOR.currentInstance = editor; | ||
48 | CKEDITOR.fire( 'currentInstance' ); | ||
49 | } | ||
50 | } ); | ||
51 | |||
52 | editor.on( 'blur', function() { | ||
53 | if ( CKEDITOR.currentInstance == editor ) { | ||
54 | CKEDITOR.currentInstance = null; | ||
55 | CKEDITOR.fire( 'currentInstance' ); | ||
56 | } | ||
57 | } ); | ||
58 | |||
59 | CKEDITOR.fire( 'instance', null, editor ); | ||
60 | }; | ||
61 | |||
62 | /** | ||
63 | * Removes an editor instance from the global {@link CKEDITOR} object. This function | ||
64 | * is available for internal use only. External code must use {@link CKEDITOR.editor#method-destroy}. | ||
65 | * | ||
66 | * @private | ||
67 | * @param {CKEDITOR.editor} editor The editor instance to be removed. | ||
68 | */ | ||
69 | CKEDITOR.remove = function( editor ) { | ||
70 | delete CKEDITOR.instances[ editor.name ]; | ||
71 | }; | ||
72 | |||
73 | ( function() { | ||
74 | var tpls = {}; | ||
75 | |||
76 | /** | ||
77 | * Adds a named {@link CKEDITOR.template} instance to be reused among all editors. | ||
78 | * This will return the existing one if a template with same name is already | ||
79 | * defined. Additionally, it fires the "template" event to allow template source customization. | ||
80 | * | ||
81 | * @param {String} name The name which identifies a UI template. | ||
82 | * @param {String} source The source string for constructing this template. | ||
83 | * @returns {CKEDITOR.template} The created template instance. | ||
84 | */ | ||
85 | CKEDITOR.addTemplate = function( name, source ) { | ||
86 | var tpl = tpls[ name ]; | ||
87 | if ( tpl ) | ||
88 | return tpl; | ||
89 | |||
90 | // Make it possible to customize the template through event. | ||
91 | var params = { name: name, source: source }; | ||
92 | CKEDITOR.fire( 'template', params ); | ||
93 | |||
94 | return ( tpls[ name ] = new CKEDITOR.template( params.source ) ); | ||
95 | }; | ||
96 | |||
97 | /** | ||
98 | * Retrieves a defined template created with {@link CKEDITOR#addTemplate}. | ||
99 | * | ||
100 | * @param {String} name The template name. | ||
101 | */ | ||
102 | CKEDITOR.getTemplate = function( name ) { | ||
103 | return tpls[ name ]; | ||
104 | }; | ||
105 | } )(); | ||
106 | |||
107 | ( function() { | ||
108 | var styles = []; | ||
109 | |||
110 | /** | ||
111 | * Adds CSS rules to be appended to the editor document. | ||
112 | * This method is mostly used by plugins to add custom styles to the editor | ||
113 | * document. For basic content styling the `contents.css` file should be | ||
114 | * used instead. | ||
115 | * | ||
116 | * **Note:** This function should be called before the creation of editor instances. | ||
117 | * | ||
118 | * // Add styles for all headings inside editable contents. | ||
119 | * CKEDITOR.addCss( '.cke_editable h1,.cke_editable h2,.cke_editable h3 { border-bottom: 1px dotted red }' ); | ||
120 | * | ||
121 | * @param {String} css The style rules to be appended. | ||
122 | * @see CKEDITOR.config#contentsCss | ||
123 | */ | ||
124 | CKEDITOR.addCss = function( css ) { | ||
125 | styles.push( css ); | ||
126 | }; | ||
127 | |||
128 | /** | ||
129 | * Returns a string will all CSS rules passed to the {@link CKEDITOR#addCss} method. | ||
130 | * | ||
131 | * @returns {String} A string containing CSS rules. | ||
132 | */ | ||
133 | CKEDITOR.getCss = function() { | ||
134 | return styles.join( '\n' ); | ||
135 | }; | ||
136 | } )(); | ||
137 | |||
138 | // Perform global clean up to free as much memory as possible | ||
139 | // when there are no instances left | ||
140 | CKEDITOR.on( 'instanceDestroyed', function() { | ||
141 | if ( CKEDITOR.tools.isEmpty( this.instances ) ) | ||
142 | CKEDITOR.fire( 'reset' ); | ||
143 | } ); | ||
144 | |||
145 | // Load the bootstrap script. | ||
146 | CKEDITOR.loader.load( '_bootstrap' ); // %REMOVE_LINE% | ||
147 | |||
148 | // Tri-state constants. | ||
149 | /** | ||
150 | * Used to indicate the ON or ACTIVE state. | ||
151 | * | ||
152 | * @readonly | ||
153 | * @property {Number} [=1] | ||
154 | */ | ||
155 | CKEDITOR.TRISTATE_ON = 1; | ||
156 | |||
157 | /** | ||
158 | * Used to indicate the OFF or INACTIVE state. | ||
159 | * | ||
160 | * @readonly | ||
161 | * @property {Number} [=2] | ||
162 | */ | ||
163 | CKEDITOR.TRISTATE_OFF = 2; | ||
164 | |||
165 | /** | ||
166 | * Used to indicate the DISABLED state. | ||
167 | * | ||
168 | * @readonly | ||
169 | * @property {Number} [=0] | ||
170 | */ | ||
171 | CKEDITOR.TRISTATE_DISABLED = 0; | ||
172 | |||
173 | /** | ||
174 | * The editor which is currently active (has user focus). | ||
175 | * | ||
176 | * function showCurrentEditorName() { | ||
177 | * if ( CKEDITOR.currentInstance ) | ||
178 | * alert( CKEDITOR.currentInstance.name ); | ||
179 | * else | ||
180 | * alert( 'Please focus an editor first.' ); | ||
181 | * } | ||
182 | * | ||
183 | * @property {CKEDITOR.editor} currentInstance | ||
184 | * @see CKEDITOR#event-currentInstance | ||
185 | */ | ||
186 | |||
187 | /** | ||
188 | * Fired when the CKEDITOR.currentInstance object reference changes. This may | ||
189 | * happen when setting the focus on different editor instances in the page. | ||
190 | * | ||
191 | * var editor; // A variable to store a reference to the current editor. | ||
192 | * CKEDITOR.on( 'currentInstance', function() { | ||
193 | * editor = CKEDITOR.currentInstance; | ||
194 | * } ); | ||
195 | * | ||
196 | * @event currentInstance | ||
197 | */ | ||
198 | |||
199 | /** | ||
200 | * Fired when the last instance has been destroyed. This event is used to perform | ||
201 | * global memory cleanup. | ||
202 | * | ||
203 | * @event reset | ||
204 | */ | ||
diff --git a/sources/core/ckeditor_base.js b/sources/core/ckeditor_base.js new file mode 100644 index 0000000..1e90d72 --- /dev/null +++ b/sources/core/ckeditor_base.js | |||
@@ -0,0 +1,314 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Contains the first and essential part of the {@link CKEDITOR} | ||
8 | * object definition. | ||
9 | */ | ||
10 | |||
11 | // #### Compressed Code | ||
12 | // Compressed code in ckeditor.js must be be updated on changes in the script. | ||
13 | // The Closure Compiler online service should be used when updating this manually: | ||
14 | // http://closure-compiler.appspot.com/ | ||
15 | |||
16 | // #### Raw code | ||
17 | // ATTENTION: read the above "Compressed Code" notes when changing this code. | ||
18 | |||
19 | if ( !window.CKEDITOR ) { | ||
20 | /** | ||
21 | * This is the API entry point. The entire CKEditor code runs under this object. | ||
22 | * @class CKEDITOR | ||
23 | * @singleton | ||
24 | */ | ||
25 | window.CKEDITOR = ( function() { | ||
26 | var basePathSrcPattern = /(^|.*[\\\/])ckeditor\.js(?:\?.*|;.*)?$/i; | ||
27 | |||
28 | var CKEDITOR = { | ||
29 | |||
30 | /** | ||
31 | * A constant string unique for each release of CKEditor. Its value | ||
32 | * is used, by default, to build the URL for all resources loaded | ||
33 | * by the editor code, guaranteeing clean cache results when | ||
34 | * upgrading. | ||
35 | * | ||
36 | * alert( CKEDITOR.timestamp ); // e.g. '87dm' | ||
37 | */ | ||
38 | timestamp: '', // %REMOVE_LINE% | ||
39 | /* // %REMOVE_LINE% | ||
40 | // The production implementation contains a fixed timestamp, unique | ||
41 | // for each release and generated by the releaser. | ||
42 | // (Base 36 value of each component of YYMMDDHH - 4 chars total - e.g. 87bm == 08071122) | ||
43 | timestamp: '%TIMESTAMP%', | ||
44 | // %REMOVE_LINE% */ | ||
45 | |||
46 | /** | ||
47 | * Contains the CKEditor version number. | ||
48 | * | ||
49 | * alert( CKEDITOR.version ); // e.g. 'CKEditor 3.4.1' | ||
50 | */ | ||
51 | version: '%VERSION%', | ||
52 | |||
53 | /** | ||
54 | * Contains the CKEditor revision number. | ||
55 | * The revision number is incremented automatically, following each | ||
56 | * modification to the CKEditor source code. | ||
57 | * | ||
58 | * alert( CKEDITOR.revision ); // e.g. '3975' | ||
59 | */ | ||
60 | revision: '%REV%', | ||
61 | |||
62 | /** | ||
63 | * A 3-digit random integer, valid for the entire life of the CKEDITOR object. | ||
64 | * | ||
65 | * alert( CKEDITOR.rnd ); // e.g. 319 | ||
66 | * | ||
67 | * @property {Number} | ||
68 | */ | ||
69 | rnd: Math.floor( Math.random() * ( 999 /*Max*/ - 100 /*Min*/ + 1 ) ) + 100 /*Min*/, | ||
70 | |||
71 | /** | ||
72 | * Private object used to hold core stuff. It should not be used outside of | ||
73 | * the API code as properties defined here may change at any time | ||
74 | * without notice. | ||
75 | * | ||
76 | * @private | ||
77 | */ | ||
78 | _: { | ||
79 | pending: [], | ||
80 | basePathSrcPattern: basePathSrcPattern | ||
81 | }, | ||
82 | |||
83 | /** | ||
84 | * Indicates the API loading status. The following statuses are available: | ||
85 | * | ||
86 | * * **unloaded**: the API is not yet loaded. | ||
87 | * * **basic_loaded**: the basic API features are available. | ||
88 | * * **basic_ready**: the basic API is ready to load the full core code. | ||
89 | * * **loaded**: the API can be fully used. | ||
90 | * | ||
91 | * Example: | ||
92 | * | ||
93 | * if ( CKEDITOR.status == 'loaded' ) { | ||
94 | * // The API can now be fully used. | ||
95 | * doSomething(); | ||
96 | * } else { | ||
97 | * // Wait for the full core to be loaded and fire its loading. | ||
98 | * CKEDITOR.on( 'load', doSomething ); | ||
99 | * CKEDITOR.loadFullCore && CKEDITOR.loadFullCore(); | ||
100 | * } | ||
101 | */ | ||
102 | status: 'unloaded', | ||
103 | |||
104 | /** | ||
105 | * The full URL for the CKEditor installation directory. | ||
106 | * It is possible to manually provide the base path by setting a | ||
107 | * global variable named `CKEDITOR_BASEPATH`. This global variable | ||
108 | * must be set **before** the editor script loading. | ||
109 | * | ||
110 | * alert( CKEDITOR.basePath ); // e.g. 'http://www.example.com/ckeditor/' | ||
111 | * | ||
112 | * @property {String} | ||
113 | */ | ||
114 | basePath: ( function() { | ||
115 | // Find out the editor directory path, based on its <script> tag. | ||
116 | var path = window.CKEDITOR_BASEPATH || ''; | ||
117 | |||
118 | if ( !path ) { | ||
119 | var scripts = document.getElementsByTagName( 'script' ); | ||
120 | |||
121 | for ( var i = 0; i < scripts.length; i++ ) { | ||
122 | var match = scripts[ i ].src.match( basePathSrcPattern ); | ||
123 | |||
124 | if ( match ) { | ||
125 | path = match[ 1 ]; | ||
126 | break; | ||
127 | } | ||
128 | } | ||
129 | } | ||
130 | |||
131 | // In IE (only) the script.src string is the raw value entered in the | ||
132 | // HTML source. Other browsers return the full resolved URL instead. | ||
133 | if ( path.indexOf( ':/' ) == -1 && path.slice( 0, 2 ) != '//' ) { | ||
134 | // Absolute path. | ||
135 | if ( path.indexOf( '/' ) === 0 ) | ||
136 | path = location.href.match( /^.*?:\/\/[^\/]*/ )[ 0 ] + path; | ||
137 | // Relative path. | ||
138 | else | ||
139 | path = location.href.match( /^[^\?]*\/(?:)/ )[ 0 ] + path; | ||
140 | } | ||
141 | |||
142 | if ( !path ) | ||
143 | throw 'The CKEditor installation path could not be automatically detected. Please set the global variable "CKEDITOR_BASEPATH" before creating editor instances.'; | ||
144 | |||
145 | return path; | ||
146 | } )(), | ||
147 | |||
148 | /** | ||
149 | * Gets the full URL for CKEditor resources. By default, URLs | ||
150 | * returned by this function contain a querystring parameter ("t") | ||
151 | * set to the {@link CKEDITOR#timestamp} value. | ||
152 | * | ||
153 | * It is possible to provide a custom implementation of this | ||
154 | * function by setting a global variable named `CKEDITOR_GETURL`. | ||
155 | * This global variable must be set **before** the editor script | ||
156 | * loading. If the custom implementation returns nothing (`==null`), the | ||
157 | * default implementation is used. | ||
158 | * | ||
159 | * // e.g. 'http://www.example.com/ckeditor/skins/default/editor.css?t=87dm' | ||
160 | * alert( CKEDITOR.getUrl( 'skins/default/editor.css' ) ); | ||
161 | * | ||
162 | * // e.g. 'http://www.example.com/skins/default/editor.css?t=87dm' | ||
163 | * alert( CKEDITOR.getUrl( '/skins/default/editor.css' ) ); | ||
164 | * | ||
165 | * // e.g. 'http://www.somesite.com/skins/default/editor.css?t=87dm' | ||
166 | * alert( CKEDITOR.getUrl( 'http://www.somesite.com/skins/default/editor.css' ) ); | ||
167 | * | ||
168 | * @param {String} resource The resource whose full URL we want to get. | ||
169 | * It may be a full, absolute, or relative URL. | ||
170 | * @returns {String} The full URL. | ||
171 | */ | ||
172 | getUrl: function( resource ) { | ||
173 | // If this is not a full or absolute path. | ||
174 | if ( resource.indexOf( ':/' ) == -1 && resource.indexOf( '/' ) !== 0 ) | ||
175 | resource = this.basePath + resource; | ||
176 | |||
177 | // Add the timestamp, except for directories. | ||
178 | if ( this.timestamp && resource.charAt( resource.length - 1 ) != '/' && !( /[&?]t=/ ).test( resource ) ) | ||
179 | resource += ( resource.indexOf( '?' ) >= 0 ? '&' : '?' ) + 't=' + this.timestamp; | ||
180 | |||
181 | return resource; | ||
182 | }, | ||
183 | |||
184 | /** | ||
185 | * Specify a function to execute when the DOM is fully loaded. | ||
186 | * | ||
187 | * If called after the DOM has been initialized, the function passed in will | ||
188 | * be executed immediately. | ||
189 | * | ||
190 | * @method | ||
191 | * @todo | ||
192 | */ | ||
193 | domReady: ( function() { | ||
194 | // Based on the original jQuery code (available under the MIT license, see LICENSE.md). | ||
195 | |||
196 | var callbacks = []; | ||
197 | |||
198 | function onReady() { | ||
199 | try { | ||
200 | // Cleanup functions for the document ready method | ||
201 | if ( document.addEventListener ) { | ||
202 | document.removeEventListener( 'DOMContentLoaded', onReady, false ); | ||
203 | executeCallbacks(); | ||
204 | } | ||
205 | // Make sure body exists, at least, in case IE gets a little overzealous. | ||
206 | else if ( document.attachEvent && document.readyState === 'complete' ) { | ||
207 | document.detachEvent( 'onreadystatechange', onReady ); | ||
208 | executeCallbacks(); | ||
209 | } | ||
210 | } catch ( er ) {} | ||
211 | } | ||
212 | |||
213 | function executeCallbacks() { | ||
214 | var i; | ||
215 | while ( ( i = callbacks.shift() ) ) | ||
216 | i(); | ||
217 | } | ||
218 | |||
219 | return function( fn ) { | ||
220 | callbacks.push( fn ); | ||
221 | |||
222 | // Catch cases where this is called after the | ||
223 | // browser event has already occurred. | ||
224 | if ( document.readyState === 'complete' ) | ||
225 | // Handle it asynchronously to allow scripts the opportunity to delay ready | ||
226 | setTimeout( onReady, 1 ); | ||
227 | |||
228 | // Run below once on demand only. | ||
229 | if ( callbacks.length != 1 ) | ||
230 | return; | ||
231 | |||
232 | // For IE>8, Firefox, Opera and Webkit. | ||
233 | if ( document.addEventListener ) { | ||
234 | // Use the handy event callback | ||
235 | document.addEventListener( 'DOMContentLoaded', onReady, false ); | ||
236 | |||
237 | // A fallback to window.onload, that will always work | ||
238 | window.addEventListener( 'load', onReady, false ); | ||
239 | |||
240 | } | ||
241 | // If old IE event model is used | ||
242 | else if ( document.attachEvent ) { | ||
243 | // ensure firing before onload, | ||
244 | // maybe late but safe also for iframes | ||
245 | document.attachEvent( 'onreadystatechange', onReady ); | ||
246 | |||
247 | // A fallback to window.onload, that will always work | ||
248 | window.attachEvent( 'onload', onReady ); | ||
249 | |||
250 | // If IE and not a frame | ||
251 | // continually check to see if the document is ready | ||
252 | // use the trick by Diego Perini | ||
253 | // http://javascript.nwbox.com/IEContentLoaded/ | ||
254 | var toplevel = false; | ||
255 | |||
256 | try { | ||
257 | toplevel = !window.frameElement; | ||
258 | } catch ( e ) {} | ||
259 | |||
260 | if ( document.documentElement.doScroll && toplevel ) { | ||
261 | scrollCheck(); | ||
262 | } | ||
263 | } | ||
264 | |||
265 | function scrollCheck() { | ||
266 | try { | ||
267 | document.documentElement.doScroll( 'left' ); | ||
268 | } catch ( e ) { | ||
269 | setTimeout( scrollCheck, 1 ); | ||
270 | return; | ||
271 | } | ||
272 | onReady(); | ||
273 | } | ||
274 | }; | ||
275 | |||
276 | } )() | ||
277 | }; | ||
278 | |||
279 | // Make it possible to override the "url" function with a custom | ||
280 | // implementation pointing to a global named CKEDITOR_GETURL. | ||
281 | var newGetUrl = window.CKEDITOR_GETURL; | ||
282 | if ( newGetUrl ) { | ||
283 | var originalGetUrl = CKEDITOR.getUrl; | ||
284 | CKEDITOR.getUrl = function( resource ) { | ||
285 | return newGetUrl.call( CKEDITOR, resource ) || originalGetUrl.call( CKEDITOR, resource ); | ||
286 | }; | ||
287 | } | ||
288 | |||
289 | return CKEDITOR; | ||
290 | } )(); | ||
291 | } | ||
292 | |||
293 | /** | ||
294 | * Function called upon loading a custom configuration file that can | ||
295 | * modify the editor instance configuration ({@link CKEDITOR.editor#config}). | ||
296 | * It is usually defined inside the custom configuration files that can | ||
297 | * include developer defined settings. | ||
298 | * | ||
299 | * // This is supposed to be placed in the config.js file. | ||
300 | * CKEDITOR.editorConfig = function( config ) { | ||
301 | * // Define changes to default configuration here. For example: | ||
302 | * config.language = 'fr'; | ||
303 | * config.uiColor = '#AADC6E'; | ||
304 | * }; | ||
305 | * | ||
306 | * @method editorConfig | ||
307 | * @param {CKEDITOR.config} config A configuration object containing the | ||
308 | * settings defined for a {@link CKEDITOR.editor} instance up to this | ||
309 | * function call. Note that not all settings may still be available. See | ||
310 | * [Configuration Loading Order](http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Setting_Configurations#Configuration_Loading_Order) | ||
311 | * for details. | ||
312 | */ | ||
313 | |||
314 | // PACKAGER_RENAME( CKEDITOR ) | ||
diff --git a/sources/core/ckeditor_basic.js b/sources/core/ckeditor_basic.js new file mode 100644 index 0000000..847d661 --- /dev/null +++ b/sources/core/ckeditor_basic.js | |||
@@ -0,0 +1,94 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Contains the second part of the {@link CKEDITOR} object | ||
8 | * definition, which defines the basic editor features to be available in | ||
9 | * the root ckeditor_basic.js file. | ||
10 | */ | ||
11 | |||
12 | if ( CKEDITOR.status == 'unloaded' ) { | ||
13 | ( function() { | ||
14 | CKEDITOR.event.implementOn( CKEDITOR ); | ||
15 | |||
16 | /** | ||
17 | * Forces the full CKEditor core code, in the case only the basic code has been | ||
18 | * loaded (`ckeditor_basic.js`). This method self-destroys (becomes undefined) in | ||
19 | * the first call or as soon as the full code is available. | ||
20 | * | ||
21 | * // Check if the full core code has been loaded and load it. | ||
22 | * if ( CKEDITOR.loadFullCore ) | ||
23 | * CKEDITOR.loadFullCore(); | ||
24 | * | ||
25 | * @member CKEDITOR | ||
26 | */ | ||
27 | CKEDITOR.loadFullCore = function() { | ||
28 | // If the basic code is not ready, just mark it to be loaded. | ||
29 | if ( CKEDITOR.status != 'basic_ready' ) { | ||
30 | CKEDITOR.loadFullCore._load = 1; | ||
31 | return; | ||
32 | } | ||
33 | |||
34 | // Destroy this function. | ||
35 | delete CKEDITOR.loadFullCore; | ||
36 | |||
37 | // Append the script to the head. | ||
38 | var script = document.createElement( 'script' ); | ||
39 | script.type = 'text/javascript'; | ||
40 | script.src = CKEDITOR.basePath + 'ckeditor.js'; | ||
41 | script.src = CKEDITOR.basePath + 'ckeditor_source.js'; // %REMOVE_LINE% | ||
42 | |||
43 | document.getElementsByTagName( 'head' )[ 0 ].appendChild( script ); | ||
44 | }; | ||
45 | |||
46 | /** | ||
47 | * The time to wait (in seconds) to load the full editor code after the | ||
48 | * page load, if the "ckeditor_basic" file is used. If set to zero, the | ||
49 | * editor is loaded on demand, as soon as an instance is created. | ||
50 | * | ||
51 | * This value must be set on the page before the page load completion. | ||
52 | * | ||
53 | * // Loads the full source after five seconds. | ||
54 | * CKEDITOR.loadFullCoreTimeout = 5; | ||
55 | * | ||
56 | * @property | ||
57 | * @member CKEDITOR | ||
58 | */ | ||
59 | CKEDITOR.loadFullCoreTimeout = 0; | ||
60 | |||
61 | // Documented at ckeditor.js. | ||
62 | CKEDITOR.add = function( editor ) { | ||
63 | // For now, just put the editor in the pending list. It will be | ||
64 | // processed as soon as the full code gets loaded. | ||
65 | var pending = this._.pending || ( this._.pending = [] ); | ||
66 | pending.push( editor ); | ||
67 | }; | ||
68 | |||
69 | ( function() { | ||
70 | var onload = function() { | ||
71 | var loadFullCore = CKEDITOR.loadFullCore, | ||
72 | loadFullCoreTimeout = CKEDITOR.loadFullCoreTimeout; | ||
73 | |||
74 | if ( !loadFullCore ) | ||
75 | return; | ||
76 | |||
77 | CKEDITOR.status = 'basic_ready'; | ||
78 | |||
79 | if ( loadFullCore && loadFullCore._load ) | ||
80 | loadFullCore(); | ||
81 | else if ( loadFullCoreTimeout ) { | ||
82 | setTimeout( function() { | ||
83 | if ( CKEDITOR.loadFullCore ) | ||
84 | CKEDITOR.loadFullCore(); | ||
85 | }, loadFullCoreTimeout * 1000 ); | ||
86 | } | ||
87 | }; | ||
88 | |||
89 | CKEDITOR.domReady( onload ); | ||
90 | } )(); | ||
91 | |||
92 | CKEDITOR.status = 'basic_loaded'; | ||
93 | } )(); | ||
94 | } | ||
diff --git a/sources/core/command.js b/sources/core/command.js new file mode 100644 index 0000000..a0e07b5 --- /dev/null +++ b/sources/core/command.js | |||
@@ -0,0 +1,275 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * Represents a command that can be executed on an editor instance. | ||
8 | * | ||
9 | * var command = new CKEDITOR.command( editor, { | ||
10 | * exec: function( editor ) { | ||
11 | * alert( editor.document.getBody().getHtml() ); | ||
12 | * } | ||
13 | * } ); | ||
14 | * | ||
15 | * @class | ||
16 | * @extends CKEDITOR.commandDefinition | ||
17 | * @mixins CKEDITOR.event | ||
18 | * @constructor Creates a command class instance. | ||
19 | * @param {CKEDITOR.editor} editor The editor instance this command will be | ||
20 | * related to. | ||
21 | * @param {CKEDITOR.commandDefinition} commandDefinition The command | ||
22 | * definition. | ||
23 | */ | ||
24 | CKEDITOR.command = function( editor, commandDefinition ) { | ||
25 | /** | ||
26 | * Lists UI items that are associated to this command. This list can be | ||
27 | * used to interact with the UI on command execution (by the execution code | ||
28 | * itself, for example). | ||
29 | * | ||
30 | * alert( 'Number of UI items associated to this command: ' + command.uiItems.length ); | ||
31 | */ | ||
32 | this.uiItems = []; | ||
33 | |||
34 | /** | ||
35 | * Executes the command. | ||
36 | * | ||
37 | * command.exec(); // The command gets executed. | ||
38 | * | ||
39 | * **Note:** You should use the {@link CKEDITOR.editor#execCommand} method instead of calling | ||
40 | * `command.exec()` directly. | ||
41 | * | ||
42 | * @param {Object} [data] Any data to pass to the command. Depends on the | ||
43 | * command implementation and requirements. | ||
44 | * @returns {Boolean} A boolean indicating that the command has been successfully executed. | ||
45 | */ | ||
46 | this.exec = function( data ) { | ||
47 | if ( this.state == CKEDITOR.TRISTATE_DISABLED || !this.checkAllowed() ) | ||
48 | return false; | ||
49 | |||
50 | if ( this.editorFocus ) // Give editor focus if necessary (#4355). | ||
51 | editor.focus(); | ||
52 | |||
53 | if ( this.fire( 'exec' ) === false ) | ||
54 | return true; | ||
55 | |||
56 | return ( commandDefinition.exec.call( this, editor, data ) !== false ); | ||
57 | }; | ||
58 | |||
59 | /** | ||
60 | * Explicitly update the status of the command, by firing the {@link CKEDITOR.command#event-refresh} event, | ||
61 | * as well as invoke the {@link CKEDITOR.commandDefinition#refresh} method if defined, this method | ||
62 | * is to allow different parts of the editor code to contribute in command status resolution. | ||
63 | * | ||
64 | * @param {CKEDITOR.editor} editor The editor instance. | ||
65 | * @param {CKEDITOR.dom.elementPath} path | ||
66 | */ | ||
67 | this.refresh = function( editor, path ) { | ||
68 | // Do nothing is we're on read-only and this command doesn't support it. | ||
69 | // We don't need to disabled the command explicitely here, because this | ||
70 | // is already done by the "readOnly" event listener. | ||
71 | if ( !this.readOnly && editor.readOnly ) | ||
72 | return true; | ||
73 | |||
74 | // Disable commands that are not allowed in the current selection path context. | ||
75 | if ( this.context && !path.isContextFor( this.context ) ) { | ||
76 | this.disable(); | ||
77 | return true; | ||
78 | } | ||
79 | |||
80 | // Disable commands that are not allowed by the active filter. | ||
81 | if ( !this.checkAllowed( true ) ) { | ||
82 | this.disable(); | ||
83 | return true; | ||
84 | } | ||
85 | |||
86 | // Make the "enabled" state a default for commands enabled from start. | ||
87 | if ( !this.startDisabled ) | ||
88 | this.enable(); | ||
89 | |||
90 | // Disable commands which shouldn't be enabled in this mode. | ||
91 | if ( this.modes && !this.modes[ editor.mode ] ) | ||
92 | this.disable(); | ||
93 | |||
94 | if ( this.fire( 'refresh', { editor: editor, path: path } ) === false ) | ||
95 | return true; | ||
96 | |||
97 | return ( commandDefinition.refresh && commandDefinition.refresh.apply( this, arguments ) !== false ); | ||
98 | }; | ||
99 | |||
100 | var allowed; | ||
101 | |||
102 | /** | ||
103 | * Checks whether this command is allowed by the active allowed | ||
104 | * content filter ({@link CKEDITOR.editor#activeFilter}). This means | ||
105 | * that if command implements {@link CKEDITOR.feature} interface it will be tested | ||
106 | * by the {@link CKEDITOR.filter#checkFeature} method. | ||
107 | * | ||
108 | * @since 4.1 | ||
109 | * @param {Boolean} [noCache] Skip cache for example due to active filter change. Since CKEditor 4.2. | ||
110 | * @returns {Boolean} Whether this command is allowed. | ||
111 | */ | ||
112 | this.checkAllowed = function( noCache ) { | ||
113 | if ( !noCache && typeof allowed == 'boolean' ) | ||
114 | return allowed; | ||
115 | |||
116 | return allowed = editor.activeFilter.checkFeature( this ); | ||
117 | }; | ||
118 | |||
119 | CKEDITOR.tools.extend( this, commandDefinition, { | ||
120 | /** | ||
121 | * The editor modes within which the command can be executed. The | ||
122 | * execution will have no action if the current mode is not listed | ||
123 | * in this property. | ||
124 | * | ||
125 | * // Enable the command in both WYSIWYG and Source modes. | ||
126 | * command.modes = { wysiwyg:1,source:1 }; | ||
127 | * | ||
128 | * // Enable the command in Source mode only. | ||
129 | * command.modes = { source:1 }; | ||
130 | * | ||
131 | * @see CKEDITOR.editor#mode | ||
132 | */ | ||
133 | modes: { wysiwyg: 1 }, | ||
134 | |||
135 | /** | ||
136 | * Indicates that the editor will get the focus before executing | ||
137 | * the command. | ||
138 | * | ||
139 | * // Do not force the editor to have focus when executing the command. | ||
140 | * command.editorFocus = false; | ||
141 | * | ||
142 | * @property {Boolean} [=true] | ||
143 | */ | ||
144 | editorFocus: 1, | ||
145 | |||
146 | /** | ||
147 | * Indicates that this command is sensible to the selection context. | ||
148 | * If `true`, the {@link CKEDITOR.command#method-refresh} method will be | ||
149 | * called for this command on the {@link CKEDITOR.editor#event-selectionChange} event. | ||
150 | * | ||
151 | * @property {Boolean} [=false] | ||
152 | */ | ||
153 | contextSensitive: !!commandDefinition.context, | ||
154 | |||
155 | /** | ||
156 | * Indicates the editor state. Possible values are: | ||
157 | * | ||
158 | * * {@link CKEDITOR#TRISTATE_DISABLED}: the command is | ||
159 | * disabled. It's execution will have no effect. Same as {@link #disable}. | ||
160 | * * {@link CKEDITOR#TRISTATE_ON}: the command is enabled | ||
161 | * and currently active in the editor (for context sensitive commands, for example). | ||
162 | * * {@link CKEDITOR#TRISTATE_OFF}: the command is enabled | ||
163 | * and currently inactive in the editor (for context sensitive commands, for example). | ||
164 | * | ||
165 | * Do not set this property directly, using the {@link #setState} method instead. | ||
166 | * | ||
167 | * if ( command.state == CKEDITOR.TRISTATE_DISABLED ) | ||
168 | * alert( 'This command is disabled' ); | ||
169 | * | ||
170 | * @property {Number} [=CKEDITOR.TRISTATE_DISABLED] | ||
171 | */ | ||
172 | state: CKEDITOR.TRISTATE_DISABLED | ||
173 | } ); | ||
174 | |||
175 | // Call the CKEDITOR.event constructor to initialize this instance. | ||
176 | CKEDITOR.event.call( this ); | ||
177 | }; | ||
178 | |||
179 | CKEDITOR.command.prototype = { | ||
180 | /** | ||
181 | * Enables the command for execution. The command state (see | ||
182 | * {@link CKEDITOR.command#property-state}) available before disabling it is restored. | ||
183 | * | ||
184 | * command.enable(); | ||
185 | * command.exec(); // Execute the command. | ||
186 | */ | ||
187 | enable: function() { | ||
188 | if ( this.state == CKEDITOR.TRISTATE_DISABLED && this.checkAllowed() ) | ||
189 | this.setState( ( !this.preserveState || ( typeof this.previousState == 'undefined' ) ) ? CKEDITOR.TRISTATE_OFF : this.previousState ); | ||
190 | }, | ||
191 | |||
192 | /** | ||
193 | * Disables the command for execution. The command state (see | ||
194 | * {@link CKEDITOR.command#property-state}) will be set to {@link CKEDITOR#TRISTATE_DISABLED}. | ||
195 | * | ||
196 | * command.disable(); | ||
197 | * command.exec(); // "false" - Nothing happens. | ||
198 | */ | ||
199 | disable: function() { | ||
200 | this.setState( CKEDITOR.TRISTATE_DISABLED ); | ||
201 | }, | ||
202 | |||
203 | /** | ||
204 | * Sets the command state. | ||
205 | * | ||
206 | * command.setState( CKEDITOR.TRISTATE_ON ); | ||
207 | * command.exec(); // Execute the command. | ||
208 | * command.setState( CKEDITOR.TRISTATE_DISABLED ); | ||
209 | * command.exec(); // 'false' - Nothing happens. | ||
210 | * command.setState( CKEDITOR.TRISTATE_OFF ); | ||
211 | * command.exec(); // Execute the command. | ||
212 | * | ||
213 | * @param {Number} newState The new state. See {@link #property-state}. | ||
214 | * @returns {Boolean} Returns `true` if the command state changed. | ||
215 | */ | ||
216 | setState: function( newState ) { | ||
217 | // Do nothing if there is no state change. | ||
218 | if ( this.state == newState ) | ||
219 | return false; | ||
220 | |||
221 | if ( newState != CKEDITOR.TRISTATE_DISABLED && !this.checkAllowed() ) | ||
222 | return false; | ||
223 | |||
224 | this.previousState = this.state; | ||
225 | |||
226 | // Set the new state. | ||
227 | this.state = newState; | ||
228 | |||
229 | // Fire the "state" event, so other parts of the code can react to the | ||
230 | // change. | ||
231 | this.fire( 'state' ); | ||
232 | |||
233 | return true; | ||
234 | }, | ||
235 | |||
236 | /** | ||
237 | * Toggles the on/off (active/inactive) state of the command. This is | ||
238 | * mainly used internally by context sensitive commands. | ||
239 | * | ||
240 | * command.toggleState(); | ||
241 | */ | ||
242 | toggleState: function() { | ||
243 | if ( this.state == CKEDITOR.TRISTATE_OFF ) | ||
244 | this.setState( CKEDITOR.TRISTATE_ON ); | ||
245 | else if ( this.state == CKEDITOR.TRISTATE_ON ) | ||
246 | this.setState( CKEDITOR.TRISTATE_OFF ); | ||
247 | } | ||
248 | }; | ||
249 | |||
250 | CKEDITOR.event.implementOn( CKEDITOR.command.prototype ); | ||
251 | |||
252 | /** | ||
253 | * Indicates the previous command state. | ||
254 | * | ||
255 | * alert( command.previousState ); | ||
256 | * | ||
257 | * @property {Number} previousState | ||
258 | * @see #state | ||
259 | */ | ||
260 | |||
261 | /** | ||
262 | * Fired when the command state changes. | ||
263 | * | ||
264 | * command.on( 'state', function() { | ||
265 | * // Alerts the new state. | ||
266 | * alert( this.state ); | ||
267 | * } ); | ||
268 | * | ||
269 | * @event state | ||
270 | */ | ||
271 | |||
272 | /** | ||
273 | * @event refresh | ||
274 | * @todo | ||
275 | */ | ||
diff --git a/sources/core/commanddefinition.js b/sources/core/commanddefinition.js new file mode 100644 index 0000000..68b2253 --- /dev/null +++ b/sources/core/commanddefinition.js | |||
@@ -0,0 +1,148 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Defines the "virtual" {@link CKEDITOR.commandDefinition} class, | ||
8 | * which contains the defintion of a command. This file is for | ||
9 | * documentation purposes only. | ||
10 | */ | ||
11 | |||
12 | /** | ||
13 | * Virtual class that illustrates the features of command objects to be | ||
14 | * passed to the {@link CKEDITOR.editor#addCommand} function. | ||
15 | * | ||
16 | * @class CKEDITOR.commandDefinition | ||
17 | * @abstract | ||
18 | */ | ||
19 | |||
20 | /** | ||
21 | * The function to be fired when the commend is executed. | ||
22 | * | ||
23 | * editorInstance.addCommand( 'sample', { | ||
24 | * exec: function( editor ) { | ||
25 | * alert( 'Executing a command for the editor name "' + editor.name + '"!' ); | ||
26 | * } | ||
27 | * } ); | ||
28 | * | ||
29 | * @method exec | ||
30 | * @param {CKEDITOR.editor} editor The editor within which run the command. | ||
31 | * @param {Object} [data] Additional data to be used to execute the command. | ||
32 | * @returns {Boolean} Whether the command has been successfully executed. | ||
33 | * Defaults to `true`, if nothing is returned. | ||
34 | */ | ||
35 | |||
36 | /** | ||
37 | * Whether the command need to be hooked into the redo/undo system. | ||
38 | * | ||
39 | * editorInstance.addCommand( 'alertName', { | ||
40 | * exec: function( editor ) { | ||
41 | * alert( editor.name ); | ||
42 | * }, | ||
43 | * canUndo: false // No support for undo/redo. | ||
44 | * } ); | ||
45 | * | ||
46 | * @property {Boolean} [canUndo=true] | ||
47 | */ | ||
48 | |||
49 | /** | ||
50 | * Whether the command is asynchronous, which means that the | ||
51 | * {@link CKEDITOR.editor#event-afterCommandExec} event will be fired by the | ||
52 | * command itself manually, and that the return value of this command is not to | ||
53 | * be returned by the {@link #exec} function. | ||
54 | * | ||
55 | * editorInstance.addCommand( 'loadOptions', { | ||
56 | * exec: function( editor ) { | ||
57 | * // Asynchronous operation below. | ||
58 | * CKEDITOR.ajax.loadXml( 'data.xml', function() { | ||
59 | * editor.fire( 'afterCommandExec' ); | ||
60 | * } ); | ||
61 | * }, | ||
62 | * async: true // The command need some time to complete after exec function returns. | ||
63 | * } ); | ||
64 | * | ||
65 | * @property {Boolean} [async=false] | ||
66 | */ | ||
67 | |||
68 | /** | ||
69 | * Whether the command should give focus to the editor before execution. | ||
70 | * | ||
71 | * editorInstance.addCommand( 'maximize', { | ||
72 | * exec: function( editor ) { | ||
73 | * // ... | ||
74 | * }, | ||
75 | * editorFocus: false // The command doesn't require focusing the editing document. | ||
76 | * } ); | ||
77 | * | ||
78 | * See also {@link CKEDITOR.command#editorFocus}. | ||
79 | * | ||
80 | * @property {Boolean} [editorFocus=true] | ||
81 | */ | ||
82 | |||
83 | |||
84 | /** | ||
85 | * Whether the command state should be set to {@link CKEDITOR#TRISTATE_DISABLED} on startup. | ||
86 | * | ||
87 | * editorInstance.addCommand( 'unlink', { | ||
88 | * exec: function( editor ) { | ||
89 | * // ... | ||
90 | * }, | ||
91 | * startDisabled: true // Command is unavailable until selection is inside a link. | ||
92 | * } ); | ||
93 | * | ||
94 | * @property {Boolean} [startDisabled=false] | ||
95 | */ | ||
96 | |||
97 | /** | ||
98 | * Indicates that this command is sensible to the selection context. | ||
99 | * If `true`, the {@link CKEDITOR.command#method-refresh} method will be | ||
100 | * called for this command on selection changes, with a single parameter | ||
101 | * representing the current elements path. | ||
102 | * | ||
103 | * @property {Boolean} [contextSensitive=true] | ||
104 | */ | ||
105 | |||
106 | /** | ||
107 | * Defined by command definition a function to determinate the command state, it will be invoked | ||
108 | * when editor has it's `states` or `selection` changed. | ||
109 | * | ||
110 | * **Note:** The function provided must be calling {@link CKEDITOR.command#setState} in all circumstance, | ||
111 | * if it is intended to update the command state. | ||
112 | * | ||
113 | * @method refresh | ||
114 | * @param {CKEDITOR.editor} editor | ||
115 | * @param {CKEDITOR.dom.elementPath} path | ||
116 | */ | ||
117 | |||
118 | /** | ||
119 | * Sets the element name used to reflect the command state on selection changes. | ||
120 | * If the selection is in a place where the element is not allowed, the command | ||
121 | * will be disabled. | ||
122 | * Setting this property overrides {@link #contextSensitive} to `true`. | ||
123 | * | ||
124 | * @property {Boolean} [context=true] | ||
125 | */ | ||
126 | |||
127 | /** | ||
128 | * The editor modes within which the command can be executed. The execution | ||
129 | * will have no action if the current mode is not listed in this property. | ||
130 | * | ||
131 | * editorInstance.addCommand( 'link', { | ||
132 | * exec: function( editor ) { | ||
133 | * // ... | ||
134 | * }, | ||
135 | * modes: { wysiwyg:1 } // Command is available in wysiwyg mode only. | ||
136 | * } ); | ||
137 | * | ||
138 | * See also {@link CKEDITOR.command#modes}. | ||
139 | * | ||
140 | * @property {Object} [modes={ wysiwyg:1 }] | ||
141 | */ | ||
142 | |||
143 | /** | ||
144 | * Whether the command should be enabled in the {@link CKEDITOR.editor#setReadOnly read-only mode}. | ||
145 | * | ||
146 | * @since 4.0 | ||
147 | * @property {Boolean} [readOnly=false] | ||
148 | */ | ||
diff --git a/sources/core/config.js b/sources/core/config.js new file mode 100644 index 0000000..4ce98d7 --- /dev/null +++ b/sources/core/config.js | |||
@@ -0,0 +1,451 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.config} object that stores the | ||
8 | * default configuration settings. | ||
9 | */ | ||
10 | |||
11 | /** | ||
12 | * Used in conjunction with the {@link CKEDITOR.config#enterMode} | ||
13 | * and {@link CKEDITOR.config#shiftEnterMode} configuration | ||
14 | * settings to make the editor produce `<p>` tags when | ||
15 | * using the <kbd>Enter</kbd> key. | ||
16 | * | ||
17 | * Read more in the [documentation](#!/guide/dev_enterkey) and see the | ||
18 | * [SDK sample](http://sdk.ckeditor.com/samples/enterkey.html). | ||
19 | * | ||
20 | * @readonly | ||
21 | * @property {Number} [=1] | ||
22 | * @member CKEDITOR | ||
23 | */ | ||
24 | CKEDITOR.ENTER_P = 1; | ||
25 | |||
26 | /** | ||
27 | * Used in conjunction with the {@link CKEDITOR.config#enterMode} | ||
28 | * and {@link CKEDITOR.config#shiftEnterMode} configuration | ||
29 | * settings to make the editor produce `<br>` tags when | ||
30 | * using the <kbd>Enter</kbd> key. | ||
31 | * | ||
32 | * Read more in the [documentation](#!/guide/dev_enterkey) and see the | ||
33 | * [SDK sample](http://sdk.ckeditor.com/samples/enterkey.html). | ||
34 | * | ||
35 | * @readonly | ||
36 | * @property {Number} [=2] | ||
37 | * @member CKEDITOR | ||
38 | */ | ||
39 | CKEDITOR.ENTER_BR = 2; | ||
40 | |||
41 | /** | ||
42 | * Used in conjunction with the {@link CKEDITOR.config#enterMode} | ||
43 | * and {@link CKEDITOR.config#shiftEnterMode} configuration | ||
44 | * settings to make the editor produce `<div>` tags when | ||
45 | * using the <kbd>Enter</kbd> key. | ||
46 | * | ||
47 | * Read more in the [documentation](#!/guide/dev_enterkey) and see the | ||
48 | * [SDK sample](http://sdk.ckeditor.com/samples/enterkey.html). | ||
49 | * | ||
50 | * @readonly | ||
51 | * @property {Number} [=3] | ||
52 | * @member CKEDITOR | ||
53 | */ | ||
54 | CKEDITOR.ENTER_DIV = 3; | ||
55 | |||
56 | /** | ||
57 | * Stores default configuration settings. Changes to this object are | ||
58 | * reflected in all editor instances, if not specified otherwise for a particular | ||
59 | * instance. | ||
60 | * | ||
61 | * Read more about setting CKEditor configuration in the | ||
62 | * [documentation](#!/guide/dev_configuration). | ||
63 | * | ||
64 | * @class | ||
65 | * @singleton | ||
66 | */ | ||
67 | CKEDITOR.config = { | ||
68 | /** | ||
69 | * The URL path to the custom configuration file to be loaded. If not | ||
70 | * overwritten with inline configuration, it defaults to the `config.js` | ||
71 | * file present in the root of the CKEditor installation directory. | ||
72 | * | ||
73 | * CKEditor will recursively load custom configuration files defined inside | ||
74 | * other custom configuration files. | ||
75 | * | ||
76 | * Read more about setting CKEditor configuration in the | ||
77 | * [documentation](#!/guide/dev_configuration). | ||
78 | * | ||
79 | * // Load a specific configuration file. | ||
80 | * CKEDITOR.replace( 'myfield', { customConfig: '/myconfig.js' } ); | ||
81 | * | ||
82 | * // Do not load any custom configuration file. | ||
83 | * CKEDITOR.replace( 'myfield', { customConfig: '' } ); | ||
84 | * | ||
85 | * @cfg {String} [="<CKEditor folder>/config.js"] | ||
86 | */ | ||
87 | customConfig: 'config.js', | ||
88 | |||
89 | /** | ||
90 | * Whether the element replaced by the editor (usually a `<textarea>`) | ||
91 | * is to be updated automatically when posting the form containing the editor. | ||
92 | * | ||
93 | * @cfg | ||
94 | */ | ||
95 | autoUpdateElement: true, | ||
96 | |||
97 | /** | ||
98 | * The user interface language localization to use. If left empty, the editor | ||
99 | * will automatically be localized to the user language. If the user language is not supported, | ||
100 | * the language specified in the {@link CKEDITOR.config#defaultLanguage} | ||
101 | * configuration setting is used. | ||
102 | * | ||
103 | * Read more in the [documentation](#!/guide/dev_uilanguage) and see the | ||
104 | * [SDK sample](http://sdk.ckeditor.com/samples/uilanguages.html). | ||
105 | * | ||
106 | * // Load the German interface. | ||
107 | * config.language = 'de'; | ||
108 | * | ||
109 | * @cfg | ||
110 | */ | ||
111 | language: '', | ||
112 | |||
113 | /** | ||
114 | * The language to be used if the {@link CKEDITOR.config#language} | ||
115 | * setting is left empty and it is not possible to localize the editor to the user language. | ||
116 | * | ||
117 | * Read more in the [documentation](#!/guide/dev_uilanguage) and see the | ||
118 | * [SDK sample](http://sdk.ckeditor.com/samples/uilanguages.html). | ||
119 | * | ||
120 | * config.defaultLanguage = 'it'; | ||
121 | * | ||
122 | * @cfg | ||
123 | */ | ||
124 | defaultLanguage: 'en', | ||
125 | |||
126 | /** | ||
127 | * The writing direction of the language which is used to create editor content. | ||
128 | * Allowed values are: | ||
129 | * | ||
130 | * * `''` (an empty string) – Indicates that content direction will be the same as either | ||
131 | * the editor UI direction or the page element direction depending on the editor type: | ||
132 | * * [Classic editor](#!/guide/dev_framed) – The same as the user interface language direction. | ||
133 | * * [Inline editor](#!/guide/dev_inline)– The same as the editable element text direction. | ||
134 | * * `'ltr'` – Indicates a Left-To-Right text direction (like in English). | ||
135 | * * `'rtl'` – Indicates a Right-To-Left text direction (like in Arabic). | ||
136 | * | ||
137 | * See the [SDK sample](http://sdk.ckeditor.com/samples/language.html). | ||
138 | * | ||
139 | * Example: | ||
140 | * | ||
141 | * config.contentsLangDirection = 'rtl'; | ||
142 | * | ||
143 | * @cfg | ||
144 | */ | ||
145 | contentsLangDirection: '', | ||
146 | |||
147 | /** | ||
148 | * Sets the behavior of the <kbd>Enter</kbd> key. It also determines other behavior | ||
149 | * rules of the editor, like whether the `<br>` element is to be used | ||
150 | * as a paragraph separator when indenting text. | ||
151 | * The allowed values are the following constants that cause the behavior outlined below: | ||
152 | * | ||
153 | * * {@link CKEDITOR#ENTER_P} (1) – New `<p>` paragraphs are created. | ||
154 | * * {@link CKEDITOR#ENTER_BR} (2) – Lines are broken with `<br>` elements. | ||
155 | * * {@link CKEDITOR#ENTER_DIV} (3) – New `<div>` blocks are created. | ||
156 | * | ||
157 | * **Note**: It is recommended to use the {@link CKEDITOR#ENTER_P} setting because of | ||
158 | * its semantic value and correctness. The editor is optimized for this setting. | ||
159 | * | ||
160 | * Read more in the [documentation](#!/guide/dev_enterkey) and see the | ||
161 | * [SDK sample](http://sdk.ckeditor.com/samples/enterkey.html). | ||
162 | * | ||
163 | * // Not recommended. | ||
164 | * config.enterMode = CKEDITOR.ENTER_BR; | ||
165 | * | ||
166 | * @cfg {Number} [=CKEDITOR.ENTER_P] | ||
167 | */ | ||
168 | enterMode: CKEDITOR.ENTER_P, | ||
169 | |||
170 | /** | ||
171 | * Forces the use of {@link CKEDITOR.config#enterMode} as line break regardless | ||
172 | * of the context. If, for example, {@link CKEDITOR.config#enterMode} is set | ||
173 | * to {@link CKEDITOR#ENTER_P}, pressing the <kbd>Enter</kbd> key inside a | ||
174 | * `<div>` element will create a new paragraph with a `<p>` | ||
175 | * instead of a `<div>`. | ||
176 | * | ||
177 | * Read more in the [documentation](#!/guide/dev_enterkey) and see the | ||
178 | * [SDK sample](http://sdk.ckeditor.com/samples/enterkey.html). | ||
179 | * | ||
180 | * // Not recommended. | ||
181 | * config.forceEnterMode = true; | ||
182 | * | ||
183 | * @since 3.2.1 | ||
184 | * @cfg | ||
185 | */ | ||
186 | forceEnterMode: false, | ||
187 | |||
188 | /** | ||
189 | * Similarly to the {@link CKEDITOR.config#enterMode} setting, it defines the behavior | ||
190 | * of the <kbd>Shift+Enter</kbd> key combination. | ||
191 | * | ||
192 | * The allowed values are the following constants that cause the behavior outlined below: | ||
193 | * | ||
194 | * * {@link CKEDITOR#ENTER_P} (1) – New `<p>` paragraphs are created. | ||
195 | * * {@link CKEDITOR#ENTER_BR} (2) – Lines are broken with `<br>` elements. | ||
196 | * * {@link CKEDITOR#ENTER_DIV} (3) – New `<div>` blocks are created. | ||
197 | * | ||
198 | * Read more in the [documentation](#!/guide/dev_enterkey) and see the | ||
199 | * [SDK sample](http://sdk.ckeditor.com/samples/enterkey.html). | ||
200 | * | ||
201 | * Example: | ||
202 | * | ||
203 | * config.shiftEnterMode = CKEDITOR.ENTER_P; | ||
204 | * | ||
205 | * @cfg {Number} [=CKEDITOR.ENTER_BR] | ||
206 | */ | ||
207 | shiftEnterMode: CKEDITOR.ENTER_BR, | ||
208 | |||
209 | /** | ||
210 | * Sets the `DOCTYPE` to be used when loading the editor content as HTML. | ||
211 | * | ||
212 | * // Set the DOCTYPE to the HTML 4 (Quirks) mode. | ||
213 | * config.docType = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">'; | ||
214 | * | ||
215 | * @cfg | ||
216 | */ | ||
217 | docType: '<!DOCTYPE html>', | ||
218 | |||
219 | /** | ||
220 | * Sets the `id` attribute to be used on the `body` element | ||
221 | * of the editing area. This can be useful when you intend to reuse the original CSS | ||
222 | * file you are using on your live website and want to assign the editor the same ID | ||
223 | * as the section that will include the contents. In this way ID-specific CSS rules will | ||
224 | * be enabled. | ||
225 | * | ||
226 | * config.bodyId = 'contents_id'; | ||
227 | * | ||
228 | * @since 3.1 | ||
229 | * @cfg | ||
230 | */ | ||
231 | bodyId: '', | ||
232 | |||
233 | /** | ||
234 | * Sets the `class` attribute to be used on the `body` element | ||
235 | * of the editing area. This can be useful when you intend to reuse the original CSS | ||
236 | * file you are using on your live website and want to assign the editor the same class | ||
237 | * as the section that will include the contents. In this way class-specific CSS rules will | ||
238 | * be enabled. | ||
239 | * | ||
240 | * config.bodyClass = 'contents'; | ||
241 | * | ||
242 | * **Note:** The editor needs to load stylesheets containing contents styles. You can either | ||
243 | * copy them to the `contents.css` file that the editor loads by default or set the {@link #contentsCss} | ||
244 | * option. | ||
245 | * | ||
246 | * **Note:** This setting only applies to [classic editor](#!/guide/dev_framed) (the one that uses `iframe`). | ||
247 | * | ||
248 | * @since 3.1 | ||
249 | * @cfg | ||
250 | */ | ||
251 | bodyClass: '', | ||
252 | |||
253 | /** | ||
254 | * Indicates whether the content to be edited is being input as a full HTML page. | ||
255 | * A full page includes the `<html>`, `<head>`, and `<body>` elements. | ||
256 | * The final output will also reflect this setting, including the | ||
257 | * `<body>` content only if this setting is disabled. | ||
258 | * | ||
259 | * Read more in the [documentation](#!/guide/dev_fullpage) and see the | ||
260 | * [SDK sample](http://sdk.ckeditor.com/samples/fullpage.html). | ||
261 | * | ||
262 | * config.fullPage = true; | ||
263 | * | ||
264 | * @since 3.1 | ||
265 | * @cfg | ||
266 | */ | ||
267 | fullPage: false, | ||
268 | |||
269 | /** | ||
270 | * The height of the editing area that includes the editor content. This configuration | ||
271 | * option accepts an integer (to denote a value in pixels) or any CSS-defined length unit | ||
272 | * except percent (`%`) values which are not supported. | ||
273 | * | ||
274 | * **Note:** This configuration option is ignored by [inline editor](#!/guide/dev_inline). | ||
275 | * | ||
276 | * Read more in the [documentation](#!/guide/dev_size) and see the | ||
277 | * [SDK sample](http://sdk.ckeditor.com/samples/size.html). | ||
278 | * | ||
279 | * config.height = 500; // 500 pixels. | ||
280 | * config.height = '25em'; // CSS length. | ||
281 | * config.height = '300px'; // CSS length. | ||
282 | * | ||
283 | * @cfg {Number/String} | ||
284 | */ | ||
285 | height: 200, | ||
286 | |||
287 | /** | ||
288 | * The CSS file(s) to be used to apply style to editor content. It should | ||
289 | * reflect the CSS used in the target pages where the content is to be | ||
290 | * displayed. | ||
291 | * | ||
292 | * **Note:** This configuration value is ignored by [inline editor](#!/guide/dev_inline) | ||
293 | * as it uses the styles that come directly from the page that CKEditor is | ||
294 | * rendered on. It is also ignored in the {@link #fullPage full page mode} in | ||
295 | * which the developer has full control over the page HTML code. | ||
296 | * | ||
297 | * Read more in the [documentation](#!/guide/dev_styles) and see the | ||
298 | * [SDK sample](http://sdk.ckeditor.com/samples/styles.html). | ||
299 | * | ||
300 | * config.contentsCss = '/css/mysitestyles.css'; | ||
301 | * config.contentsCss = [ '/css/mysitestyles.css', '/css/anotherfile.css' ]; | ||
302 | * | ||
303 | * @cfg {String/Array} [contentsCss=CKEDITOR.getUrl( 'contents.css' )] | ||
304 | */ | ||
305 | contentsCss: CKEDITOR.getUrl( 'contents.css' ), | ||
306 | |||
307 | /** | ||
308 | * Comma-separated list of plugins to be used in an editor instance. Note that | ||
309 | * the actual plugins that are to be loaded could still be affected by two other settings: | ||
310 | * {@link CKEDITOR.config#extraPlugins} and {@link CKEDITOR.config#removePlugins}. | ||
311 | * | ||
312 | * @cfg {String} [="<default list of plugins>"] | ||
313 | */ | ||
314 | plugins: '', // %REMOVE_LINE% | ||
315 | |||
316 | /** | ||
317 | * A list of additional plugins to be loaded. This setting makes it easier | ||
318 | * to add new plugins without having to touch the {@link CKEDITOR.config#plugins} setting. | ||
319 | * | ||
320 | * **Note:** The most recommended way to | ||
321 | * [add CKEditor plugins](http://docs.ckeditor.com/#!/guide/dev_plugins) is through | ||
322 | * [CKEditor Builder](http://ckeditor.com/builder). Read more in the | ||
323 | * [documentation](#!/guide/dev_plugins). | ||
324 | * | ||
325 | * config.extraPlugins = 'myplugin,anotherplugin'; | ||
326 | * | ||
327 | * @cfg | ||
328 | */ | ||
329 | extraPlugins: '', | ||
330 | |||
331 | /** | ||
332 | * A list of plugins that must not be loaded. This setting makes it possible | ||
333 | * to avoid loading some plugins defined in the {@link CKEDITOR.config#plugins} | ||
334 | * setting without having to touch it. | ||
335 | * | ||
336 | * **Note:** A plugin required by another plugin cannot be removed and will cause | ||
337 | * an error to be thrown. So for example if `contextmenu` is required by `tabletools`, | ||
338 | * it can only be removed if `tabletools` is not loaded. | ||
339 | * | ||
340 | * config.removePlugins = 'elementspath,save,font'; | ||
341 | * | ||
342 | * @cfg | ||
343 | */ | ||
344 | removePlugins: '', | ||
345 | |||
346 | /** | ||
347 | * A list of regular expressions to be executed on input HTML, | ||
348 | * indicating HTML source code that when matched, must **not** be available in the WYSIWYG | ||
349 | * mode for editing. | ||
350 | * | ||
351 | * config.protectedSource.push( /<\?[\s\S]*?\?>/g ); // PHP code | ||
352 | * config.protectedSource.push( /<%[\s\S]*?%>/g ); // ASP code | ||
353 | * config.protectedSource.push( /(<asp:[^\>]+>[\s|\S]*?<\/asp:[^\>]+>)|(<asp:[^\>]+\/>)/gi ); // ASP.NET code | ||
354 | * | ||
355 | * @cfg | ||
356 | */ | ||
357 | protectedSource: [], | ||
358 | |||
359 | /** | ||
360 | * The editor `tabindex` value. | ||
361 | * | ||
362 | * Read more in the [documentation](#!/guide/dev_tabindex) and see the | ||
363 | * [SDK sample](http://sdk.ckeditor.com/samples/tabindex.html). | ||
364 | * | ||
365 | * config.tabIndex = 1; | ||
366 | * | ||
367 | * @cfg | ||
368 | */ | ||
369 | tabIndex: 0, | ||
370 | |||
371 | /** | ||
372 | * The editor UI outer width. This configuration option accepts an integer | ||
373 | * (to denote a value in pixels) or any CSS-defined length unit. | ||
374 | * | ||
375 | * Unlike the {@link CKEDITOR.config#height} setting, this | ||
376 | * one will set the outer width of the entire editor UI, not for the | ||
377 | * editing area only. | ||
378 | * | ||
379 | * **Note:** This configuration option is ignored by [inline editor](#!/guide/dev_inline). | ||
380 | * | ||
381 | * Read more in the [documentation](#!/guide/dev_size) and see the | ||
382 | * [SDK sample](http://sdk.ckeditor.com/samples/size.html). | ||
383 | * | ||
384 | * config.width = 850; // 850 pixels wide. | ||
385 | * config.width = '75%'; // CSS unit. | ||
386 | * | ||
387 | * @cfg {String/Number} | ||
388 | */ | ||
389 | width: '', | ||
390 | |||
391 | /** | ||
392 | * The base Z-index for floating dialog windows and popups. | ||
393 | * | ||
394 | * config.baseFloatZIndex = 2000; | ||
395 | * | ||
396 | * @cfg | ||
397 | */ | ||
398 | baseFloatZIndex: 10000, | ||
399 | |||
400 | /** | ||
401 | * The keystrokes that are blocked by default as the browser implementation | ||
402 | * is buggy. These default keystrokes are handled by the editor. | ||
403 | * | ||
404 | * // Default setting. | ||
405 | * config.blockedKeystrokes = [ | ||
406 | * CKEDITOR.CTRL + 66, // Ctrl+B | ||
407 | * CKEDITOR.CTRL + 73, // Ctrl+I | ||
408 | * CKEDITOR.CTRL + 85 // Ctrl+U | ||
409 | * ]; | ||
410 | * | ||
411 | * @cfg {Array} [blockedKeystrokes=see example] | ||
412 | */ | ||
413 | blockedKeystrokes: [ | ||
414 | CKEDITOR.CTRL + 66, // Ctrl+B | ||
415 | CKEDITOR.CTRL + 73, // Ctrl+I | ||
416 | CKEDITOR.CTRL + 85 // Ctrl+U | ||
417 | ] | ||
418 | }; | ||
419 | |||
420 | /** | ||
421 | * Indicates that some of the editor features, like alignment and text | ||
422 | * direction, should use the "computed value" of the feature to indicate its | ||
423 | * on/off state instead of using the "real value". | ||
424 | * | ||
425 | * If enabled in a Left-To-Right written document, the "Left Justify" | ||
426 | * alignment button will be shown as active, even if the alignment style is not | ||
427 | * explicitly applied to the current paragraph in the editor. | ||
428 | * | ||
429 | * config.useComputedState = false; | ||
430 | * | ||
431 | * @since 3.4 | ||
432 | * @cfg {Boolean} [useComputedState=true] | ||
433 | */ | ||
434 | |||
435 | /** | ||
436 | * The base user interface color to be used by the editor. Not all skins are | ||
437 | * [compatible with this setting](#!/guide/skin_sdk_chameleon). | ||
438 | * | ||
439 | * Read more in the [documentation](#!/guide/dev_uicolor) and see the | ||
440 | * [SDK sample](http://sdk.ckeditor.com/samples/uicolor.html). | ||
441 | * | ||
442 | * // Using a color code. | ||
443 | * config.uiColor = '#AADC6E'; | ||
444 | * | ||
445 | * // Using an HTML color name. | ||
446 | * config.uiColor = 'Gold'; | ||
447 | * | ||
448 | * @cfg {String} uiColor | ||
449 | */ | ||
450 | |||
451 | // PACKAGER_RENAME( CKEDITOR.config ) | ||
diff --git a/sources/core/creators/inline.js b/sources/core/creators/inline.js new file mode 100644 index 0000000..a1a653c --- /dev/null +++ b/sources/core/creators/inline.js | |||
@@ -0,0 +1,157 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | ( function() { | ||
7 | /** @class CKEDITOR */ | ||
8 | |||
9 | /** | ||
10 | * Turns a DOM element with the `contenteditable` attribute set to `true` into a | ||
11 | * CKEditor instance. Check {@link CKEDITOR.dtd#$editable} for a list of | ||
12 | * allowed element names. | ||
13 | * | ||
14 | * **Note:** If the DOM element for which inline editing is being enabled does not have | ||
15 | * the `contenteditable` attribute set to `true`, the editor will start in read-only mode. | ||
16 | * | ||
17 | * <div contenteditable="true" id="content">...</div> | ||
18 | * ... | ||
19 | * CKEDITOR.inline( 'content' ); | ||
20 | * | ||
21 | * It is also possible to create an inline editor from the `<textarea>` element. | ||
22 | * If you do so, an additional `<div>` element with editable content will be created | ||
23 | * directly after the `<textarea>` element and the `<textarea>` element will be hidden. | ||
24 | * | ||
25 | * @param {Object/String} element The DOM element or its ID. | ||
26 | * @param {Object} [instanceConfig] The specific configurations to apply to this editor instance. | ||
27 | * See {@link CKEDITOR.config}. | ||
28 | * @returns {CKEDITOR.editor} The editor instance created. | ||
29 | */ | ||
30 | CKEDITOR.inline = function( element, instanceConfig ) { | ||
31 | if ( !CKEDITOR.env.isCompatible ) | ||
32 | return null; | ||
33 | |||
34 | element = CKEDITOR.dom.element.get( element ); | ||
35 | |||
36 | // Avoid multiple inline editor instances on the same element. | ||
37 | if ( element.getEditor() ) | ||
38 | throw 'The editor instance "' + element.getEditor().name + '" is already attached to the provided element.'; | ||
39 | |||
40 | var editor = new CKEDITOR.editor( instanceConfig, element, CKEDITOR.ELEMENT_MODE_INLINE ), | ||
41 | textarea = element.is( 'textarea' ) ? element : null; | ||
42 | |||
43 | if ( textarea ) { | ||
44 | editor.setData( textarea.getValue(), null, true ); | ||
45 | |||
46 | //Change element from textarea to div | ||
47 | element = CKEDITOR.dom.element.createFromHtml( | ||
48 | '<div contenteditable="' + !!editor.readOnly + '" class="cke_textarea_inline">' + | ||
49 | textarea.getValue() + | ||
50 | '</div>', | ||
51 | CKEDITOR.document ); | ||
52 | |||
53 | element.insertAfter( textarea ); | ||
54 | textarea.hide(); | ||
55 | |||
56 | // Attaching the concrete form. | ||
57 | if ( textarea.$.form ) | ||
58 | editor._attachToForm(); | ||
59 | } else { | ||
60 | // Initial editor data is simply loaded from the page element content to make | ||
61 | // data retrieval possible immediately after the editor creation. | ||
62 | editor.setData( element.getHtml(), null, true ); | ||
63 | } | ||
64 | |||
65 | // Once the editor is loaded, start the UI. | ||
66 | editor.on( 'loaded', function() { | ||
67 | editor.fire( 'uiReady' ); | ||
68 | |||
69 | // Enable editing on the element. | ||
70 | editor.editable( element ); | ||
71 | |||
72 | // Editable itself is the outermost element. | ||
73 | editor.container = element; | ||
74 | editor.ui.contentsElement = element; | ||
75 | |||
76 | // Load and process editor data. | ||
77 | editor.setData( editor.getData( 1 ) ); | ||
78 | |||
79 | // Clean on startup. | ||
80 | editor.resetDirty(); | ||
81 | |||
82 | editor.fire( 'contentDom' ); | ||
83 | |||
84 | // Inline editing defaults to "wysiwyg" mode, so plugins don't | ||
85 | // need to make special handling for this "mode-less" environment. | ||
86 | editor.mode = 'wysiwyg'; | ||
87 | editor.fire( 'mode' ); | ||
88 | |||
89 | // The editor is completely loaded for interaction. | ||
90 | editor.status = 'ready'; | ||
91 | editor.fireOnce( 'instanceReady' ); | ||
92 | CKEDITOR.fire( 'instanceReady', null, editor ); | ||
93 | |||
94 | // give priority to plugins that relay on editor#loaded for bootstrapping. | ||
95 | }, null, null, 10000 ); | ||
96 | |||
97 | // Handle editor destroying. | ||
98 | editor.on( 'destroy', function() { | ||
99 | // Remove container from DOM if inline-textarea editor. | ||
100 | // Show <textarea> back again. | ||
101 | if ( textarea ) { | ||
102 | editor.container.clearCustomData(); | ||
103 | editor.container.remove(); | ||
104 | textarea.show(); | ||
105 | } | ||
106 | |||
107 | editor.element.clearCustomData(); | ||
108 | |||
109 | delete editor.element; | ||
110 | } ); | ||
111 | |||
112 | return editor; | ||
113 | }; | ||
114 | |||
115 | /** | ||
116 | * Calls {@link CKEDITOR#inline} for all page elements with | ||
117 | * the `contenteditable` attribute set to `true`. | ||
118 | * | ||
119 | */ | ||
120 | CKEDITOR.inlineAll = function() { | ||
121 | var el, data; | ||
122 | |||
123 | for ( var name in CKEDITOR.dtd.$editable ) { | ||
124 | var elements = CKEDITOR.document.getElementsByTag( name ); | ||
125 | |||
126 | for ( var i = 0, len = elements.count(); i < len; i++ ) { | ||
127 | el = elements.getItem( i ); | ||
128 | |||
129 | if ( el.getAttribute( 'contenteditable' ) == 'true' ) { | ||
130 | // Fire the "inline" event, making it possible to customize | ||
131 | // the instance settings and eventually cancel the creation. | ||
132 | |||
133 | data = { | ||
134 | element: el, | ||
135 | config: {} | ||
136 | }; | ||
137 | |||
138 | if ( CKEDITOR.fire( 'inline', data ) !== false ) | ||
139 | CKEDITOR.inline( el, data.config ); | ||
140 | } | ||
141 | } | ||
142 | } | ||
143 | }; | ||
144 | |||
145 | CKEDITOR.domReady( function() { | ||
146 | !CKEDITOR.disableAutoInline && CKEDITOR.inlineAll(); | ||
147 | } ); | ||
148 | } )(); | ||
149 | |||
150 | /** | ||
151 | * Disables creating the inline editor automatically for elements with | ||
152 | * the `contenteditable` attribute set to `true`. | ||
153 | * | ||
154 | * CKEDITOR.disableAutoInline = true; | ||
155 | * | ||
156 | * @cfg {Boolean} [disableAutoInline=false] | ||
157 | */ | ||
diff --git a/sources/core/creators/themedui.js b/sources/core/creators/themedui.js new file mode 100644 index 0000000..c87d079 --- /dev/null +++ b/sources/core/creators/themedui.js | |||
@@ -0,0 +1,541 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** @class CKEDITOR */ | ||
7 | |||
8 | /** | ||
9 | * The class name used to identify `<textarea>` elements to be replaced | ||
10 | * by CKEditor instances. Set it to empty/`null` to disable this feature. | ||
11 | * | ||
12 | * CKEDITOR.replaceClass = 'rich_editor'; | ||
13 | * | ||
14 | * @cfg {String} [replaceClass='ckeditor'] | ||
15 | */ | ||
16 | CKEDITOR.replaceClass = 'ckeditor'; | ||
17 | |||
18 | ( function() { | ||
19 | /** | ||
20 | * Replaces a `<textarea>` or a DOM element (`<div>`) with a CKEditor | ||
21 | * instance. For textareas, the initial value in the editor will be the | ||
22 | * textarea value. For DOM elements, their `innerHTML` will be used | ||
23 | * instead. We recommend using `<textarea>` and `<div>` elements only. | ||
24 | * | ||
25 | * <textarea id="myfield" name="myfield"></textarea> | ||
26 | * ... | ||
27 | * CKEDITOR.replace( 'myfield' ); | ||
28 | * | ||
29 | * var textarea = document.body.appendChild( document.createElement( 'textarea' ) ); | ||
30 | * CKEDITOR.replace( textarea ); | ||
31 | * | ||
32 | * @param {Object/String} element The DOM element (textarea), its ID, or name. | ||
33 | * @param {Object} [config] The specific configuration to apply to this | ||
34 | * editor instance. Configuration set here will override the global CKEditor settings | ||
35 | * (see {@link CKEDITOR.config}). | ||
36 | * @returns {CKEDITOR.editor} The editor instance created. | ||
37 | */ | ||
38 | CKEDITOR.replace = function( element, config ) { | ||
39 | return createInstance( element, config, null, CKEDITOR.ELEMENT_MODE_REPLACE ); | ||
40 | }; | ||
41 | |||
42 | /** | ||
43 | * Creates a new editor instance at the end of a specific DOM element. | ||
44 | * | ||
45 | * <!DOCTYPE html> | ||
46 | * <html> | ||
47 | * <head> | ||
48 | * <meta charset="utf-8"> | ||
49 | * <title>CKEditor</title> | ||
50 | * <!-- Make sure the path to CKEditor is correct. --> | ||
51 | * <script src="/ckeditor/ckeditor.js"></script> | ||
52 | * </head> | ||
53 | * <body> | ||
54 | * <div id="editorSpace"></div> | ||
55 | * <script> | ||
56 | * CKEDITOR.appendTo( 'editorSpace' ); | ||
57 | * </script> | ||
58 | * </body> | ||
59 | * </html> | ||
60 | * | ||
61 | * @param {Object/String} element The DOM element, its ID, or name. | ||
62 | * @param {Object} [config] The specific configuration to apply to this | ||
63 | * editor instance. Configuration set here will override the global CKEditor settings | ||
64 | * (see {@link CKEDITOR.config}). | ||
65 | * @param {String} [data] Since 3.3. Initial value for the instance. | ||
66 | * @returns {CKEDITOR.editor} The editor instance created. | ||
67 | */ | ||
68 | CKEDITOR.appendTo = function( element, config, data ) { | ||
69 | return createInstance( element, config, data, CKEDITOR.ELEMENT_MODE_APPENDTO ); | ||
70 | }; | ||
71 | |||
72 | /** | ||
73 | * Replaces all `<textarea>` elements available in the document with | ||
74 | * editor instances. | ||
75 | * | ||
76 | * // Replace all <textarea> elements in the page. | ||
77 | * CKEDITOR.replaceAll(); | ||
78 | * | ||
79 | * // Replace all <textarea class="myClassName"> elements in the page. | ||
80 | * CKEDITOR.replaceAll( 'myClassName' ); | ||
81 | * | ||
82 | * // Selectively replace <textarea> elements, based on custom assertions. | ||
83 | * CKEDITOR.replaceAll( function( textarea, config ) { | ||
84 | * // An assertion function that needs to be evaluated for the <textarea> | ||
85 | * // to be replaced. It must explicitely return "false" to ignore a | ||
86 | * // specific <textarea>. | ||
87 | * // You can also customize the editor instance by having the function | ||
88 | * // modify the "config" parameter. | ||
89 | * } ); | ||
90 | * | ||
91 | * // Full page example where three <textarea> elements are replaced. | ||
92 | * <!DOCTYPE html> | ||
93 | * <html> | ||
94 | * <head> | ||
95 | * <meta charset="utf-8"> | ||
96 | * <title>CKEditor</title> | ||
97 | * <!-- Make sure the path to CKEditor is correct. --> | ||
98 | * <script src="/ckeditor/ckeditor.js"></script> | ||
99 | * </head> | ||
100 | * <body> | ||
101 | * <textarea name="editor1"></textarea> | ||
102 | * <textarea name="editor2"></textarea> | ||
103 | * <textarea name="editor3"></textarea> | ||
104 | * <script> | ||
105 | * // Replace all three <textarea> elements above with CKEditor instances. | ||
106 | * CKEDITOR.replaceAll(); | ||
107 | * </script> | ||
108 | * </body> | ||
109 | * </html> | ||
110 | * | ||
111 | * @param {String} [className] The `<textarea>` class name. | ||
112 | * @param {Function} [function] An assertion function that must return `true` for a `<textarea>` | ||
113 | * to be replaced with the editor. If the function returns `false`, the `<textarea>` element | ||
114 | * will not be replaced. | ||
115 | */ | ||
116 | CKEDITOR.replaceAll = function() { | ||
117 | var textareas = document.getElementsByTagName( 'textarea' ); | ||
118 | |||
119 | for ( var i = 0; i < textareas.length; i++ ) { | ||
120 | var config = null, | ||
121 | textarea = textareas[ i ]; | ||
122 | |||
123 | // The "name" and/or "id" attribute must exist. | ||
124 | if ( !textarea.name && !textarea.id ) | ||
125 | continue; | ||
126 | |||
127 | if ( typeof arguments[ 0 ] == 'string' ) { | ||
128 | // The textarea class name could be passed as the function | ||
129 | // parameter. | ||
130 | |||
131 | var classRegex = new RegExp( '(?:^|\\s)' + arguments[ 0 ] + '(?:$|\\s)' ); | ||
132 | |||
133 | if ( !classRegex.test( textarea.className ) ) | ||
134 | continue; | ||
135 | } else if ( typeof arguments[ 0 ] == 'function' ) { | ||
136 | // An assertion function could be passed as the function parameter. | ||
137 | // It must explicitly return "false" to ignore a specific <textarea>. | ||
138 | config = {}; | ||
139 | if ( arguments[ 0 ]( textarea, config ) === false ) | ||
140 | continue; | ||
141 | } | ||
142 | |||
143 | this.replace( textarea, config ); | ||
144 | } | ||
145 | }; | ||
146 | |||
147 | /** @class CKEDITOR.editor */ | ||
148 | |||
149 | /** | ||
150 | * Registers an editing mode. This function is to be used mainly by plugins. | ||
151 | * | ||
152 | * @param {String} mode The mode name. | ||
153 | * @param {Function} exec The function that performs the actual mode change. | ||
154 | */ | ||
155 | CKEDITOR.editor.prototype.addMode = function( mode, exec ) { | ||
156 | ( this._.modes || ( this._.modes = {} ) )[ mode ] = exec; | ||
157 | }; | ||
158 | |||
159 | /** | ||
160 | * Changes the editing mode of this editor instance. | ||
161 | * | ||
162 | * **Note:** The mode switch could be asynchronous depending on the mode provider. | ||
163 | * Use the `callback` to hook subsequent code. | ||
164 | * | ||
165 | * // Switch to "source" view. | ||
166 | * CKEDITOR.instances.editor1.setMode( 'source' ); | ||
167 | * // Switch to "wysiwyg" view and be notified on completion. | ||
168 | * CKEDITOR.instances.editor1.setMode( 'wysiwyg', function() { alert( 'wysiwyg mode loaded!' ); } ); | ||
169 | * | ||
170 | * @param {String} [newMode] If not specified, the {@link CKEDITOR.config#startupMode} will be used. | ||
171 | * @param {Function} [callback] Optional callback function which is invoked once the mode switch has succeeded. | ||
172 | */ | ||
173 | CKEDITOR.editor.prototype.setMode = function( newMode, callback ) { | ||
174 | var editor = this; | ||
175 | |||
176 | var modes = this._.modes; | ||
177 | |||
178 | // Mode loading quickly fails. | ||
179 | if ( newMode == editor.mode || !modes || !modes[ newMode ] ) | ||
180 | return; | ||
181 | |||
182 | editor.fire( 'beforeSetMode', newMode ); | ||
183 | |||
184 | if ( editor.mode ) { | ||
185 | var isDirty = editor.checkDirty(), | ||
186 | previousModeData = editor._.previousModeData, | ||
187 | currentData, | ||
188 | unlockSnapshot = 0; | ||
189 | |||
190 | editor.fire( 'beforeModeUnload' ); | ||
191 | |||
192 | // Detach the current editable. While detaching editable will set | ||
193 | // cached editor's data (with internal setData call). We use this | ||
194 | // data below to avoid two getData() calls in a row. | ||
195 | editor.editable( 0 ); | ||
196 | |||
197 | editor._.previousMode = editor.mode; | ||
198 | // Get cached data, which was set while detaching editable. | ||
199 | editor._.previousModeData = currentData = editor.getData( 1 ); | ||
200 | |||
201 | // If data has not been modified in the mode which we are currently leaving, | ||
202 | // avoid making snapshot right after initializing new mode. | ||
203 | // http://dev.ckeditor.com/ticket/5217#comment:20 | ||
204 | // Tested by: | ||
205 | // 'test switch mode with unrecoreded, inner HTML specific content (boguses)' | ||
206 | // 'test switch mode with unrecoreded, inner HTML specific content (boguses) plus changes in source mode' | ||
207 | if ( editor.mode == 'source' && previousModeData == currentData ) { | ||
208 | // We need to make sure that unlockSnapshot will update the last snapshot | ||
209 | // (will not create new one) if lockSnapshot is not called on outdated snapshots stack. | ||
210 | // Additionally, forceUpdate prevents from making content image now, which is useless | ||
211 | // (because it equals editor data not inner HTML). | ||
212 | editor.fire( 'lockSnapshot', { forceUpdate: true } ); | ||
213 | unlockSnapshot = 1; | ||
214 | } | ||
215 | |||
216 | // Clear up the mode space. | ||
217 | editor.ui.space( 'contents' ).setHtml( '' ); | ||
218 | |||
219 | editor.mode = ''; | ||
220 | } else { | ||
221 | editor._.previousModeData = editor.getData( 1 ); | ||
222 | } | ||
223 | |||
224 | // Fire the mode handler. | ||
225 | this._.modes[ newMode ]( function() { | ||
226 | // Set the current mode. | ||
227 | editor.mode = newMode; | ||
228 | |||
229 | if ( isDirty !== undefined ) | ||
230 | !isDirty && editor.resetDirty(); | ||
231 | |||
232 | if ( unlockSnapshot ) | ||
233 | editor.fire( 'unlockSnapshot' ); | ||
234 | // Since snapshot made on dataReady (which normally catches changes done by setData) | ||
235 | // won't work because editor.mode was not set yet (it's set in this function), we need | ||
236 | // to make special snapshot for changes done in source mode here. | ||
237 | else if ( newMode == 'wysiwyg' ) | ||
238 | editor.fire( 'saveSnapshot' ); | ||
239 | |||
240 | // Delay to avoid race conditions (setMode inside setMode). | ||
241 | setTimeout( function() { | ||
242 | editor.fire( 'mode' ); | ||
243 | callback && callback.call( editor ); | ||
244 | }, 0 ); | ||
245 | } ); | ||
246 | }; | ||
247 | |||
248 | /** | ||
249 | * Resizes the editor interface. | ||
250 | * | ||
251 | * editor.resize( 900, 300 ); | ||
252 | * | ||
253 | * editor.resize( '100%', 450, true ); | ||
254 | * | ||
255 | * @param {Number/String} width The new width. It can be an integer denoting a value | ||
256 | * in pixels or a CSS size value with unit. | ||
257 | * @param {Number/String} height The new height. It can be an integer denoting a value | ||
258 | * in pixels or a CSS size value with unit. | ||
259 | * @param {Boolean} [isContentHeight] Indicates that the provided height is to | ||
260 | * be applied to the editor content area, and not to the entire editor | ||
261 | * interface. Defaults to `false`. | ||
262 | * @param {Boolean} [resizeInner] Indicates that it is the inner interface | ||
263 | * element that must be resized, not the outer element. The default theme | ||
264 | * defines the editor interface inside a pair of `<span>` elements | ||
265 | * (`<span><span>...</span></span>`). By default the first, | ||
266 | * outer `<span>` element receives the sizes. If this parameter is set to | ||
267 | * `true`, the second, inner `<span>` is resized instead. | ||
268 | */ | ||
269 | CKEDITOR.editor.prototype.resize = function( width, height, isContentHeight, resizeInner ) { | ||
270 | var container = this.container, | ||
271 | contents = this.ui.space( 'contents' ), | ||
272 | contentsFrame = CKEDITOR.env.webkit && this.document && this.document.getWindow().$.frameElement, | ||
273 | outer; | ||
274 | |||
275 | if ( resizeInner ) { | ||
276 | outer = this.container.getFirst( function( node ) { | ||
277 | return node.type == CKEDITOR.NODE_ELEMENT && node.hasClass( 'cke_inner' ); | ||
278 | } ); | ||
279 | } else { | ||
280 | outer = container; | ||
281 | } | ||
282 | |||
283 | // Set as border box width. (#5353) | ||
284 | outer.setSize( 'width', width, true ); | ||
285 | |||
286 | // WebKit needs to refresh the iframe size to avoid rendering issues. (1/2) (#8348) | ||
287 | contentsFrame && ( contentsFrame.style.width = '1%' ); | ||
288 | |||
289 | // Get the height delta between the outer table and the content area. | ||
290 | var contentsOuterDelta = ( outer.$.offsetHeight || 0 ) - ( contents.$.clientHeight || 0 ), | ||
291 | |||
292 | // If we're setting the content area's height, then we don't need the delta. | ||
293 | resultContentsHeight = Math.max( height - ( isContentHeight ? 0 : contentsOuterDelta ), 0 ), | ||
294 | resultOuterHeight = ( isContentHeight ? height + contentsOuterDelta : height ); | ||
295 | |||
296 | contents.setStyle( 'height', resultContentsHeight + 'px' ); | ||
297 | |||
298 | // WebKit needs to refresh the iframe size to avoid rendering issues. (2/2) (#8348) | ||
299 | contentsFrame && ( contentsFrame.style.width = '100%' ); | ||
300 | |||
301 | // Emit a resize event. | ||
302 | this.fire( 'resize', { | ||
303 | outerHeight: resultOuterHeight, | ||
304 | contentsHeight: resultContentsHeight, | ||
305 | // Sometimes width is not provided. | ||
306 | outerWidth: width || outer.getSize( 'width' ) | ||
307 | } ); | ||
308 | }; | ||
309 | |||
310 | /** | ||
311 | * Gets the element that can be used to check the editor size. This method | ||
312 | * is mainly used by the `resize` plugin, which adds a UI handle that can be used | ||
313 | * to resize the editor. | ||
314 | * | ||
315 | * @param {Boolean} forContents Whether to return the "contents" part of the theme instead of the container. | ||
316 | * @returns {CKEDITOR.dom.element} The resizable element. | ||
317 | */ | ||
318 | CKEDITOR.editor.prototype.getResizable = function( forContents ) { | ||
319 | return forContents ? this.ui.space( 'contents' ) : this.container; | ||
320 | }; | ||
321 | |||
322 | function createInstance( element, config, data, mode ) { | ||
323 | if ( !CKEDITOR.env.isCompatible ) | ||
324 | return null; | ||
325 | |||
326 | element = CKEDITOR.dom.element.get( element ); | ||
327 | |||
328 | // Avoid multiple inline editor instances on the same element. | ||
329 | if ( element.getEditor() ) | ||
330 | throw 'The editor instance "' + element.getEditor().name + '" is already attached to the provided element.'; | ||
331 | |||
332 | // Create the editor instance. | ||
333 | var editor = new CKEDITOR.editor( config, element, mode ); | ||
334 | |||
335 | if ( mode == CKEDITOR.ELEMENT_MODE_REPLACE ) { | ||
336 | // Do not replace the textarea right now, just hide it. The effective | ||
337 | // replacement will be done later in the editor creation lifecycle. | ||
338 | element.setStyle( 'visibility', 'hidden' ); | ||
339 | |||
340 | // #8031 Remember if textarea was required and remove the attribute. | ||
341 | editor._.required = element.hasAttribute( 'required' ); | ||
342 | element.removeAttribute( 'required' ); | ||
343 | } | ||
344 | |||
345 | data && editor.setData( data, null, true ); | ||
346 | |||
347 | // Once the editor is loaded, start the UI. | ||
348 | editor.on( 'loaded', function() { | ||
349 | loadTheme( editor ); | ||
350 | |||
351 | if ( mode == CKEDITOR.ELEMENT_MODE_REPLACE && editor.config.autoUpdateElement && element.$.form ) | ||
352 | editor._attachToForm(); | ||
353 | |||
354 | editor.setMode( editor.config.startupMode, function() { | ||
355 | // Clean on startup. | ||
356 | editor.resetDirty(); | ||
357 | |||
358 | // Editor is completely loaded for interaction. | ||
359 | editor.status = 'ready'; | ||
360 | editor.fireOnce( 'instanceReady' ); | ||
361 | CKEDITOR.fire( 'instanceReady', null, editor ); | ||
362 | } ); | ||
363 | } ); | ||
364 | |||
365 | editor.on( 'destroy', destroy ); | ||
366 | return editor; | ||
367 | } | ||
368 | |||
369 | function destroy() { | ||
370 | var editor = this, | ||
371 | container = editor.container, | ||
372 | element = editor.element; | ||
373 | |||
374 | if ( container ) { | ||
375 | container.clearCustomData(); | ||
376 | container.remove(); | ||
377 | } | ||
378 | |||
379 | if ( element ) { | ||
380 | element.clearCustomData(); | ||
381 | if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) { | ||
382 | element.show(); | ||
383 | if ( editor._.required ) | ||
384 | element.setAttribute( 'required', 'required' ); | ||
385 | } | ||
386 | delete editor.element; | ||
387 | } | ||
388 | } | ||
389 | |||
390 | function loadTheme( editor ) { | ||
391 | var name = editor.name, | ||
392 | element = editor.element, | ||
393 | elementMode = editor.elementMode; | ||
394 | |||
395 | // Get the HTML for the predefined spaces. | ||
396 | var topHtml = editor.fire( 'uiSpace', { space: 'top', html: '' } ).html; | ||
397 | var bottomHtml = editor.fire( 'uiSpace', { space: 'bottom', html: '' } ).html; | ||
398 | |||
399 | var themedTpl = new CKEDITOR.template( | ||
400 | '<{outerEl}' + | ||
401 | ' id="cke_{name}"' + | ||
402 | ' class="{id} cke cke_reset cke_chrome cke_editor_{name} cke_{langDir} ' + CKEDITOR.env.cssClass + '" ' + | ||
403 | ' dir="{langDir}"' + | ||
404 | ' lang="{langCode}"' + | ||
405 | ' role="application"' + | ||
406 | ( editor.title ? ' aria-labelledby="cke_{name}_arialbl"' : '' ) + | ||
407 | '>' + | ||
408 | ( editor.title ? '<span id="cke_{name}_arialbl" class="cke_voice_label">{voiceLabel}</span>' : '' ) + | ||
409 | '<{outerEl} class="cke_inner cke_reset" role="presentation">' + | ||
410 | '{topHtml}' + | ||
411 | '<{outerEl} id="{contentId}" class="cke_contents cke_reset" role="presentation"></{outerEl}>' + | ||
412 | '{bottomHtml}' + | ||
413 | '</{outerEl}>' + | ||
414 | '</{outerEl}>' ); | ||
415 | |||
416 | var container = CKEDITOR.dom.element.createFromHtml( themedTpl.output( { | ||
417 | id: editor.id, | ||
418 | name: name, | ||
419 | langDir: editor.lang.dir, | ||
420 | langCode: editor.langCode, | ||
421 | voiceLabel: editor.title, | ||
422 | topHtml: topHtml ? '<span id="' + editor.ui.spaceId( 'top' ) + '" class="cke_top cke_reset_all" role="presentation" style="height:auto">' + topHtml + '</span>' : '', | ||
423 | contentId: editor.ui.spaceId( 'contents' ), | ||
424 | bottomHtml: bottomHtml ? '<span id="' + editor.ui.spaceId( 'bottom' ) + '" class="cke_bottom cke_reset_all" role="presentation">' + bottomHtml + '</span>' : '', | ||
425 | outerEl: CKEDITOR.env.ie ? 'span' : 'div' // #9571 | ||
426 | } ) ); | ||
427 | |||
428 | if ( elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) { | ||
429 | element.hide(); | ||
430 | container.insertAfter( element ); | ||
431 | } else { | ||
432 | element.append( container ); | ||
433 | } | ||
434 | |||
435 | editor.container = container; | ||
436 | editor.ui.contentsElement = editor.ui.space( 'contents' ); | ||
437 | |||
438 | // Make top and bottom spaces unelectable, but not content space, | ||
439 | // otherwise the editable area would be affected. | ||
440 | topHtml && editor.ui.space( 'top' ).unselectable(); | ||
441 | bottomHtml && editor.ui.space( 'bottom' ).unselectable(); | ||
442 | |||
443 | var width = editor.config.width, height = editor.config.height; | ||
444 | if ( width ) | ||
445 | container.setStyle( 'width', CKEDITOR.tools.cssLength( width ) ); | ||
446 | |||
447 | // The editor height is applied to the contents space. | ||
448 | if ( height ) | ||
449 | editor.ui.space( 'contents' ).setStyle( 'height', CKEDITOR.tools.cssLength( height ) ); | ||
450 | |||
451 | // Disable browser context menu for editor's chrome. | ||
452 | container.disableContextMenu(); | ||
453 | |||
454 | // Redirect the focus into editor for webkit. (#5713) | ||
455 | CKEDITOR.env.webkit && container.on( 'focus', function() { | ||
456 | editor.focus(); | ||
457 | } ); | ||
458 | |||
459 | editor.fireOnce( 'uiReady' ); | ||
460 | } | ||
461 | |||
462 | // Replace all textareas with the default class name. | ||
463 | CKEDITOR.domReady( function() { | ||
464 | CKEDITOR.replaceClass && CKEDITOR.replaceAll( CKEDITOR.replaceClass ); | ||
465 | } ); | ||
466 | } )(); | ||
467 | |||
468 | /** | ||
469 | * The current editing mode. An editing mode basically provides | ||
470 | * different ways of editing or viewing the contents. | ||
471 | * | ||
472 | * alert( CKEDITOR.instances.editor1.mode ); // (e.g.) 'wysiwyg' | ||
473 | * | ||
474 | * @readonly | ||
475 | * @property {String} mode | ||
476 | */ | ||
477 | |||
478 | /** | ||
479 | * The mode to load at the editor startup. It depends on the plugins | ||
480 | * loaded. By default, the `wysiwyg` and `source` modes are available. | ||
481 | * | ||
482 | * config.startupMode = 'source'; | ||
483 | * | ||
484 | * @cfg {String} [startupMode='wysiwyg'] | ||
485 | * @member CKEDITOR.config | ||
486 | */ | ||
487 | CKEDITOR.config.startupMode = 'wysiwyg'; | ||
488 | |||
489 | /** | ||
490 | * Fired after the editor instance is resized through | ||
491 | * the {@link CKEDITOR.editor#method-resize CKEDITOR.resize} method. | ||
492 | * | ||
493 | * @event resize | ||
494 | * @param {CKEDITOR.editor} editor This editor instance. | ||
495 | * @param {Object} data Available since CKEditor 4.5. | ||
496 | * @param {Number} data.outerHeight The height of the entire area that the editor covers. | ||
497 | * @param {Number} data.contentsHeight Editable area height in pixels. | ||
498 | * @param {Number} data.outerWidth The width of the entire area that the editor covers. | ||
499 | */ | ||
500 | |||
501 | /** | ||
502 | * Fired before changing the editing mode. See also | ||
503 | * {@link #beforeSetMode} and {@link #event-mode}. | ||
504 | * | ||
505 | * @event beforeModeUnload | ||
506 | * @param {CKEDITOR.editor} editor This editor instance. | ||
507 | */ | ||
508 | |||
509 | /** | ||
510 | * Fired before the editor mode is set. See also | ||
511 | * {@link #event-mode} and {@link #beforeModeUnload}. | ||
512 | * | ||
513 | * @since 3.5.3 | ||
514 | * @event beforeSetMode | ||
515 | * @param {CKEDITOR.editor} editor This editor instance. | ||
516 | * @param {String} data The name of the mode which is about to be set. | ||
517 | */ | ||
518 | |||
519 | /** | ||
520 | * Fired after setting the editing mode. See also {@link #beforeSetMode} and {@link #beforeModeUnload} | ||
521 | * | ||
522 | * @event mode | ||
523 | * @param {CKEDITOR.editor} editor This editor instance. | ||
524 | */ | ||
525 | |||
526 | /** | ||
527 | * Fired when the editor (replacing a `<textarea>` which has a `required` attribute) is empty during form submission. | ||
528 | * | ||
529 | * This event replaces native required fields validation that the browsers cannot | ||
530 | * perform when CKEditor replaces `<textarea>` elements. | ||
531 | * | ||
532 | * You can cancel this event to prevent the page from submitting data. | ||
533 | * | ||
534 | * editor.on( 'required', function( evt ) { | ||
535 | * alert( 'Article content is required.' ); | ||
536 | * evt.cancel(); | ||
537 | * } ); | ||
538 | * | ||
539 | * @event required | ||
540 | * @param {CKEDITOR.editor} editor This editor instance. | ||
541 | */ | ||
diff --git a/sources/core/dataprocessor.js b/sources/core/dataprocessor.js new file mode 100644 index 0000000..ebc4d1c --- /dev/null +++ b/sources/core/dataprocessor.js | |||
@@ -0,0 +1,70 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Defines the "virtual" {@link CKEDITOR.dataProcessor} class, which | ||
8 | * defines the basic structure of data processor objects to be | ||
9 | * set to {@link CKEDITOR.editor.dataProcessor}. | ||
10 | */ | ||
11 | |||
12 | /** | ||
13 | * If defined, points to the data processor which is responsible for translating | ||
14 | * and transforming the editor data on input and output. | ||
15 | * Generally it will point to an instance of {@link CKEDITOR.htmlDataProcessor}, | ||
16 | * which handles HTML data. The editor may also handle other data formats by | ||
17 | * using different data processors provided by specific plugins. | ||
18 | * | ||
19 | * @property {CKEDITOR.dataProcessor} dataProcessor | ||
20 | * @member CKEDITOR.editor | ||
21 | */ | ||
22 | |||
23 | /** | ||
24 | * Represents a data processor which is responsible for translating and | ||
25 | * transforming the editor data on input and output. | ||
26 | * | ||
27 | * This class is here for documentation purposes only and is not really part of | ||
28 | * the API. It serves as the base ("interface") for data processor implementations. | ||
29 | * | ||
30 | * @class CKEDITOR.dataProcessor | ||
31 | * @abstract | ||
32 | */ | ||
33 | |||
34 | /** | ||
35 | * Transforms input data into HTML to be loaded into the editor. | ||
36 | * While the editor is able to handle non-HTML data (like BBCode), it can only | ||
37 | * handle HTML data at runtime. The role of the data processor is to transform | ||
38 | * the input data into HTML through this function. | ||
39 | * | ||
40 | * // Tranforming BBCode data, with a custom BBCode data processor available. | ||
41 | * var data = 'This is [b]an example[/b].'; | ||
42 | * var html = editor.dataProcessor.toHtml( data ); // '<p>This is <b>an example</b>.</p>' | ||
43 | * | ||
44 | * @method toHtml | ||
45 | * @param {String} data The input data to be transformed. | ||
46 | * @param {String} [fixForBody] The tag name to be used if the data must be | ||
47 | * fixed because it is supposed to be loaded direcly into the `<body>` | ||
48 | * tag. This is generally not used by non-HTML data processors. | ||
49 | * @todo fixForBody type - compare to htmlDataProcessor. | ||
50 | */ | ||
51 | |||
52 | /** | ||
53 | * Transforms HTML into data to be output by the editor, in the format | ||
54 | * expected by the data processor. | ||
55 | * | ||
56 | * While the editor is able to handle non-HTML data (like BBCode), it can only | ||
57 | * handle HTML data at runtime. The role of the data processor is to transform | ||
58 | * the HTML data containined by the editor into a specific data format through | ||
59 | * this function. | ||
60 | * | ||
61 | * // Tranforming into BBCode data, with a custom BBCode data processor available. | ||
62 | * var html = '<p>This is <b>an example</b>.</p>'; | ||
63 | * var data = editor.dataProcessor.toDataFormat( html ); // 'This is [b]an example[/b].' | ||
64 | * | ||
65 | * @method toDataFormat | ||
66 | * @param {String} html The HTML to be transformed. | ||
67 | * @param {String} fixForBody The tag name to be used if the output data is | ||
68 | * coming from the `<body>` element and may be eventually fixed for it. This is | ||
69 | * generally not used by non-HTML data processors. | ||
70 | */ | ||
diff --git a/sources/core/dom.js b/sources/core/dom.js new file mode 100644 index 0000000..a806a74 --- /dev/null +++ b/sources/core/dom.js | |||
@@ -0,0 +1,13 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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} object, which contains DOM | ||
8 | * manipulation objects and function. | ||
9 | */ | ||
10 | |||
11 | CKEDITOR.dom = {}; | ||
12 | |||
13 | // PACKAGER_RENAME( CKEDITOR.dom ) | ||
diff --git a/sources/core/dom/comment.js b/sources/core/dom/comment.js new file mode 100644 index 0000000..69828c2 --- /dev/null +++ b/sources/core/dom/comment.js | |||
@@ -0,0 +1,53 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 0000000..f287245 --- /dev/null +++ b/sources/core/dom/document.js | |||
@@ -0,0 +1,326 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 0000000..ffca9e5 --- /dev/null +++ b/sources/core/dom/documentfragment.js | |||
@@ -0,0 +1,62 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 0000000..607e9f3 --- /dev/null +++ b/sources/core/dom/domobject.js | |||
@@ -0,0 +1,266 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 0000000..b586b02 --- /dev/null +++ b/sources/core/dom/element.js | |||
@@ -0,0 +1,2107 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 0000000..1ee551b --- /dev/null +++ b/sources/core/dom/elementpath.js | |||
@@ -0,0 +1,251 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 0000000..7cc1bd8 --- /dev/null +++ b/sources/core/dom/event.js | |||
@@ -0,0 +1,208 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 0000000..41a823c --- /dev/null +++ b/sources/core/dom/iterator.js | |||
@@ -0,0 +1,565 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 0000000..7818b07 --- /dev/null +++ b/sources/core/dom/node.js | |||
@@ -0,0 +1,902 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 && isEmpty( current ) ) { | ||
377 | var adjacent = getAdjacentNonEmptyTextNode( current ) || getAdjacentNonEmptyTextNode( current, true ); | ||
378 | |||
379 | if ( !adjacent ) | ||
380 | return -1; | ||
381 | } | ||
382 | |||
383 | do { | ||
384 | // Bypass blank node and adjacent text nodes. | ||
385 | if ( normalized && current != this.$ && current.nodeType == CKEDITOR.NODE_TEXT && ( isNormalizing || isEmpty( current ) ) ) | ||
386 | continue; | ||
387 | |||
388 | index++; | ||
389 | isNormalizing = current.nodeType == CKEDITOR.NODE_TEXT; | ||
390 | } | ||
391 | while ( ( current = current.previousSibling ) ); | ||
392 | |||
393 | return index; | ||
394 | |||
395 | function getAdjacentNonEmptyTextNode( node, lookForward ) { | ||
396 | var sibling = lookForward ? node.nextSibling : node.previousSibling; | ||
397 | |||
398 | if ( !sibling || sibling.nodeType != CKEDITOR.NODE_TEXT ) { | ||
399 | return null; | ||
400 | } | ||
401 | |||
402 | // If found a non-empty text node, then return it. | ||
403 | // If not, then continue search. | ||
404 | return isEmpty( sibling ) ? getAdjacentNonEmptyTextNode( sibling, lookForward ) : sibling; | ||
405 | } | ||
406 | |||
407 | // Checks whether a text node is empty or is FCSeq string (which will be totally removed when normalizing). | ||
408 | function isEmpty( textNode ) { | ||
409 | return !textNode.nodeValue || textNode.nodeValue == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE; | ||
410 | } | ||
411 | }, | ||
412 | |||
413 | /** | ||
414 | * @todo | ||
415 | */ | ||
416 | getNextSourceNode: function( startFromSibling, nodeType, guard ) { | ||
417 | // If "guard" is a node, transform it in a function. | ||
418 | if ( guard && !guard.call ) { | ||
419 | var guardNode = guard; | ||
420 | guard = function( node ) { | ||
421 | return !node.equals( guardNode ); | ||
422 | }; | ||
423 | } | ||
424 | |||
425 | var node = ( !startFromSibling && this.getFirst && this.getFirst() ), | ||
426 | parent; | ||
427 | |||
428 | // Guarding when we're skipping the current element( no children or 'startFromSibling' ). | ||
429 | // send the 'moving out' signal even we don't actually dive into. | ||
430 | if ( !node ) { | ||
431 | if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) | ||
432 | return null; | ||
433 | node = this.getNext(); | ||
434 | } | ||
435 | |||
436 | while ( !node && ( parent = ( parent || this ).getParent() ) ) { | ||
437 | // The guard check sends the "true" paramenter to indicate that | ||
438 | // we are moving "out" of the element. | ||
439 | if ( guard && guard( parent, true ) === false ) | ||
440 | return null; | ||
441 | |||
442 | node = parent.getNext(); | ||
443 | } | ||
444 | |||
445 | if ( !node ) | ||
446 | return null; | ||
447 | |||
448 | if ( guard && guard( node ) === false ) | ||
449 | return null; | ||
450 | |||
451 | if ( nodeType && nodeType != node.type ) | ||
452 | return node.getNextSourceNode( false, nodeType, guard ); | ||
453 | |||
454 | return node; | ||
455 | }, | ||
456 | |||
457 | /** | ||
458 | * @todo | ||
459 | */ | ||
460 | getPreviousSourceNode: function( startFromSibling, nodeType, guard ) { | ||
461 | if ( guard && !guard.call ) { | ||
462 | var guardNode = guard; | ||
463 | guard = function( node ) { | ||
464 | return !node.equals( guardNode ); | ||
465 | }; | ||
466 | } | ||
467 | |||
468 | var node = ( !startFromSibling && this.getLast && this.getLast() ), | ||
469 | parent; | ||
470 | |||
471 | // Guarding when we're skipping the current element( no children or 'startFromSibling' ). | ||
472 | // send the 'moving out' signal even we don't actually dive into. | ||
473 | if ( !node ) { | ||
474 | if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) | ||
475 | return null; | ||
476 | node = this.getPrevious(); | ||
477 | } | ||
478 | |||
479 | while ( !node && ( parent = ( parent || this ).getParent() ) ) { | ||
480 | // The guard check sends the "true" paramenter to indicate that | ||
481 | // we are moving "out" of the element. | ||
482 | if ( guard && guard( parent, true ) === false ) | ||
483 | return null; | ||
484 | |||
485 | node = parent.getPrevious(); | ||
486 | } | ||
487 | |||
488 | if ( !node ) | ||
489 | return null; | ||
490 | |||
491 | if ( guard && guard( node ) === false ) | ||
492 | return null; | ||
493 | |||
494 | if ( nodeType && node.type != nodeType ) | ||
495 | return node.getPreviousSourceNode( false, nodeType, guard ); | ||
496 | |||
497 | return node; | ||
498 | }, | ||
499 | |||
500 | /** | ||
501 | * Gets the node that preceeds this element in its parent's child list. | ||
502 | * | ||
503 | * var element = CKEDITOR.dom.element.createFromHtml( '<div><i>prev</i><b>Example</b></div>' ); | ||
504 | * var first = element.getLast().getPrev(); | ||
505 | * alert( first.getName() ); // 'i' | ||
506 | * | ||
507 | * @param {Function} [evaluator] Filtering the result node. | ||
508 | * @returns {CKEDITOR.dom.node} The previous node or null if not available. | ||
509 | */ | ||
510 | getPrevious: function( evaluator ) { | ||
511 | var previous = this.$, | ||
512 | retval; | ||
513 | do { | ||
514 | previous = previous.previousSibling; | ||
515 | |||
516 | // Avoid returning the doc type node. | ||
517 | // http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927 | ||
518 | retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous ); | ||
519 | } | ||
520 | while ( retval && evaluator && !evaluator( retval ) ); | ||
521 | return retval; | ||
522 | }, | ||
523 | |||
524 | /** | ||
525 | * Gets the node that follows this element in its parent's child list. | ||
526 | * | ||
527 | * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b><i>next</i></div>' ); | ||
528 | * var last = element.getFirst().getNext(); | ||
529 | * alert( last.getName() ); // 'i' | ||
530 | * | ||
531 | * @param {Function} [evaluator] Filtering the result node. | ||
532 | * @returns {CKEDITOR.dom.node} The next node or null if not available. | ||
533 | */ | ||
534 | getNext: function( evaluator ) { | ||
535 | var next = this.$, | ||
536 | retval; | ||
537 | do { | ||
538 | next = next.nextSibling; | ||
539 | retval = next && new CKEDITOR.dom.node( next ); | ||
540 | } | ||
541 | while ( retval && evaluator && !evaluator( retval ) ); | ||
542 | return retval; | ||
543 | }, | ||
544 | |||
545 | /** | ||
546 | * Gets the parent element for this node. | ||
547 | * | ||
548 | * var node = editor.document.getBody().getFirst(); | ||
549 | * var parent = node.getParent(); | ||
550 | * alert( parent.getName() ); // 'body' | ||
551 | * | ||
552 | * @param {Boolean} [allowFragmentParent=false] Consider also parent node that is of | ||
553 | * fragment type {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}. | ||
554 | * @returns {CKEDITOR.dom.element} The parent element. | ||
555 | */ | ||
556 | getParent: function( allowFragmentParent ) { | ||
557 | var parent = this.$.parentNode; | ||
558 | return ( parent && ( parent.nodeType == CKEDITOR.NODE_ELEMENT || allowFragmentParent && parent.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) ? new CKEDITOR.dom.node( parent ) : null; | ||
559 | }, | ||
560 | |||
561 | /** | ||
562 | * Returns an array containing node parents and the node itself. By default nodes are in _descending_ order. | ||
563 | * | ||
564 | * // Assuming that body has paragraph as the first child. | ||
565 | * var node = editor.document.getBody().getFirst(); | ||
566 | * var parents = node.getParents(); | ||
567 | * alert( parents[ 0 ].getName() + ',' + parents[ 2 ].getName() ); // 'html,p' | ||
568 | * | ||
569 | * @param {Boolean} [closerFirst=false] Determines the order of returned nodes. | ||
570 | * @returns {Array} Returns an array of {@link CKEDITOR.dom.node}. | ||
571 | */ | ||
572 | getParents: function( closerFirst ) { | ||
573 | var node = this; | ||
574 | var parents = []; | ||
575 | |||
576 | do { | ||
577 | parents[ closerFirst ? 'push' : 'unshift' ]( node ); | ||
578 | } | ||
579 | while ( ( node = node.getParent() ) ); | ||
580 | |||
581 | return parents; | ||
582 | }, | ||
583 | |||
584 | /** | ||
585 | * @todo | ||
586 | */ | ||
587 | getCommonAncestor: function( node ) { | ||
588 | if ( node.equals( this ) ) | ||
589 | return this; | ||
590 | |||
591 | if ( node.contains && node.contains( this ) ) | ||
592 | return node; | ||
593 | |||
594 | var start = this.contains ? this : this.getParent(); | ||
595 | |||
596 | do { | ||
597 | if ( start.contains( node ) ) return start; | ||
598 | } | ||
599 | while ( ( start = start.getParent() ) ); | ||
600 | |||
601 | return null; | ||
602 | }, | ||
603 | |||
604 | /** | ||
605 | * Determines the position relation between this node and the given {@link CKEDITOR.dom.node} in the document. | ||
606 | * This node can be preceding ({@link CKEDITOR#POSITION_PRECEDING}) or following ({@link CKEDITOR#POSITION_FOLLOWING}) | ||
607 | * the given node. This node can also contain ({@link CKEDITOR#POSITION_CONTAINS}) or be contained by | ||
608 | * ({@link CKEDITOR#POSITION_IS_CONTAINED}) the given node. The function returns a bitmask of constants | ||
609 | * listed above or {@link CKEDITOR#POSITION_IDENTICAL} if the given node is the same as this node. | ||
610 | * | ||
611 | * @param {CKEDITOR.dom.node} otherNode A node to check relation with. | ||
612 | * @returns {Number} Position relation between this node and given node. | ||
613 | */ | ||
614 | getPosition: function( otherNode ) { | ||
615 | var $ = this.$; | ||
616 | var $other = otherNode.$; | ||
617 | |||
618 | if ( $.compareDocumentPosition ) | ||
619 | return $.compareDocumentPosition( $other ); | ||
620 | |||
621 | // IE and Safari have no support for compareDocumentPosition. | ||
622 | |||
623 | if ( $ == $other ) | ||
624 | return CKEDITOR.POSITION_IDENTICAL; | ||
625 | |||
626 | // Only element nodes support contains and sourceIndex. | ||
627 | if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) { | ||
628 | if ( $.contains ) { | ||
629 | if ( $.contains( $other ) ) | ||
630 | return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING; | ||
631 | |||
632 | if ( $other.contains( $ ) ) | ||
633 | return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; | ||
634 | } | ||
635 | |||
636 | if ( 'sourceIndex' in $ ) | ||
637 | return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; | ||
638 | |||
639 | } | ||
640 | |||
641 | // For nodes that don't support compareDocumentPosition, contains | ||
642 | // or sourceIndex, their "address" is compared. | ||
643 | |||
644 | var addressOfThis = this.getAddress(), | ||
645 | addressOfOther = otherNode.getAddress(), | ||
646 | minLevel = Math.min( addressOfThis.length, addressOfOther.length ); | ||
647 | |||
648 | // Determinate preceding/following relationship. | ||
649 | for ( var i = 0; i < minLevel; i++ ) { | ||
650 | if ( addressOfThis[ i ] != addressOfOther[ i ] ) { | ||
651 | return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; | ||
652 | } | ||
653 | } | ||
654 | |||
655 | // Determinate contains/contained relationship. | ||
656 | return ( addressOfThis.length < addressOfOther.length ) ? CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; | ||
657 | }, | ||
658 | |||
659 | /** | ||
660 | * Gets the closest ancestor node of this node, specified by its name or using an evaluator function. | ||
661 | * | ||
662 | * // Suppose we have the following HTML structure: | ||
663 | * // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div> | ||
664 | * // If node == <b> | ||
665 | * ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner"> | ||
666 | * ascendant = node.getAscendant( 'b' ); // ascendant == null | ||
667 | * ascendant = node.getAscendant( 'b', true ); // ascendant == <b> | ||
668 | * ascendant = node.getAscendant( { div:1, p:1 } ); // Searches for the first 'div' or 'p': ascendant == <div id="inner"> | ||
669 | * | ||
670 | * // Using custom evaluator: | ||
671 | * ascendant = node.getAscendant( function( el ) { | ||
672 | * return el.getId() == 'inner'; | ||
673 | * } ); | ||
674 | * // ascendant == <div id="inner"> | ||
675 | * | ||
676 | * @since 3.6.1 | ||
677 | * @param {String/Function/Object} query The name of the ancestor node to search or | ||
678 | * an object with the node names to search for or an evaluator function. | ||
679 | * @param {Boolean} [includeSelf] Whether to include the current | ||
680 | * node in the search. | ||
681 | * @returns {CKEDITOR.dom.node} The located ancestor node or `null` if not found. | ||
682 | */ | ||
683 | getAscendant: function( query, includeSelf ) { | ||
684 | var $ = this.$, | ||
685 | evaluator, | ||
686 | isCustomEvaluator; | ||
687 | |||
688 | if ( !includeSelf ) { | ||
689 | $ = $.parentNode; | ||
690 | } | ||
691 | |||
692 | // Custom checker provided in an argument. | ||
693 | if ( typeof query == 'function' ) { | ||
694 | isCustomEvaluator = true; | ||
695 | evaluator = query; | ||
696 | } else { | ||
697 | // Predefined tag name checker. | ||
698 | isCustomEvaluator = false; | ||
699 | evaluator = function( $ ) { | ||
700 | var name = ( typeof $.nodeName == 'string' ? $.nodeName.toLowerCase() : '' ); | ||
701 | |||
702 | return ( typeof query == 'string' ? name == query : name in query ); | ||
703 | }; | ||
704 | } | ||
705 | |||
706 | while ( $ ) { | ||
707 | // For user provided checker we use CKEDITOR.dom.node. | ||
708 | if ( evaluator( isCustomEvaluator ? new CKEDITOR.dom.node( $ ) : $ ) ) { | ||
709 | return new CKEDITOR.dom.node( $ ); | ||
710 | } | ||
711 | |||
712 | try { | ||
713 | $ = $.parentNode; | ||
714 | } catch ( e ) { | ||
715 | $ = null; | ||
716 | } | ||
717 | } | ||
718 | |||
719 | return null; | ||
720 | }, | ||
721 | |||
722 | /** | ||
723 | * @todo | ||
724 | */ | ||
725 | hasAscendant: function( name, includeSelf ) { | ||
726 | var $ = this.$; | ||
727 | |||
728 | if ( !includeSelf ) | ||
729 | $ = $.parentNode; | ||
730 | |||
731 | while ( $ ) { | ||
732 | if ( $.nodeName && $.nodeName.toLowerCase() == name ) | ||
733 | return true; | ||
734 | |||
735 | $ = $.parentNode; | ||
736 | } | ||
737 | return false; | ||
738 | }, | ||
739 | |||
740 | /** | ||
741 | * @todo | ||
742 | */ | ||
743 | move: function( target, toStart ) { | ||
744 | target.append( this.remove(), toStart ); | ||
745 | }, | ||
746 | |||
747 | /** | ||
748 | * Removes this node from the document DOM. | ||
749 | * | ||
750 | * var element = CKEDITOR.document.getById( 'MyElement' ); | ||
751 | * element.remove(); | ||
752 | * | ||
753 | * @param {Boolean} [preserveChildren=false] Indicates that the children | ||
754 | * elements must remain in the document, removing only the outer tags. | ||
755 | */ | ||
756 | remove: function( preserveChildren ) { | ||
757 | var $ = this.$; | ||
758 | var parent = $.parentNode; | ||
759 | |||
760 | if ( parent ) { | ||
761 | if ( preserveChildren ) { | ||
762 | // Move all children before the node. | ||
763 | for ( var child; | ||
764 | ( child = $.firstChild ); ) { | ||
765 | parent.insertBefore( $.removeChild( child ), $ ); | ||
766 | } | ||
767 | } | ||
768 | |||
769 | parent.removeChild( $ ); | ||
770 | } | ||
771 | |||
772 | return this; | ||
773 | }, | ||
774 | |||
775 | /** | ||
776 | * @todo | ||
777 | */ | ||
778 | replace: function( nodeToReplace ) { | ||
779 | this.insertBefore( nodeToReplace ); | ||
780 | nodeToReplace.remove(); | ||
781 | }, | ||
782 | |||
783 | /** | ||
784 | * @todo | ||
785 | */ | ||
786 | trim: function() { | ||
787 | this.ltrim(); | ||
788 | this.rtrim(); | ||
789 | }, | ||
790 | |||
791 | /** | ||
792 | * @todo | ||
793 | */ | ||
794 | ltrim: function() { | ||
795 | var child; | ||
796 | while ( this.getFirst && ( child = this.getFirst() ) ) { | ||
797 | if ( child.type == CKEDITOR.NODE_TEXT ) { | ||
798 | var trimmed = CKEDITOR.tools.ltrim( child.getText() ), | ||
799 | originalLength = child.getLength(); | ||
800 | |||
801 | if ( !trimmed ) { | ||
802 | child.remove(); | ||
803 | continue; | ||
804 | } else if ( trimmed.length < originalLength ) { | ||
805 | child.split( originalLength - trimmed.length ); | ||
806 | |||
807 | // IE BUG: child.remove() may raise JavaScript errors here. (#81) | ||
808 | this.$.removeChild( this.$.firstChild ); | ||
809 | } | ||
810 | } | ||
811 | break; | ||
812 | } | ||
813 | }, | ||
814 | |||
815 | /** | ||
816 | * @todo | ||
817 | */ | ||
818 | rtrim: function() { | ||
819 | var child; | ||
820 | while ( this.getLast && ( child = this.getLast() ) ) { | ||
821 | if ( child.type == CKEDITOR.NODE_TEXT ) { | ||
822 | var trimmed = CKEDITOR.tools.rtrim( child.getText() ), | ||
823 | originalLength = child.getLength(); | ||
824 | |||
825 | if ( !trimmed ) { | ||
826 | child.remove(); | ||
827 | continue; | ||
828 | } else if ( trimmed.length < originalLength ) { | ||
829 | child.split( trimmed.length ); | ||
830 | |||
831 | // IE BUG: child.getNext().remove() may raise JavaScript errors here. | ||
832 | // (#81) | ||
833 | this.$.lastChild.parentNode.removeChild( this.$.lastChild ); | ||
834 | } | ||
835 | } | ||
836 | break; | ||
837 | } | ||
838 | |||
839 | if ( CKEDITOR.env.needsBrFiller ) { | ||
840 | child = this.$.lastChild; | ||
841 | |||
842 | if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) { | ||
843 | // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324). | ||
844 | child.parentNode.removeChild( child ); | ||
845 | } | ||
846 | } | ||
847 | }, | ||
848 | |||
849 | /** | ||
850 | * Checks if this node is read-only (should not be changed). | ||
851 | * | ||
852 | * // For the following HTML: | ||
853 | * // <b>foo</b><div contenteditable="false"><i>bar</i></div> | ||
854 | * | ||
855 | * elB.isReadOnly(); // -> false | ||
856 | * foo.isReadOnly(); // -> false | ||
857 | * elDiv.isReadOnly(); // -> true | ||
858 | * elI.isReadOnly(); // -> true | ||
859 | * | ||
860 | * This method works in two modes depending on browser support for the `element.isContentEditable` property and | ||
861 | * the value of the `checkOnlyAttributes` parameter. The `element.isContentEditable` check is faster, but it is known | ||
862 | * to malfunction in hidden or detached nodes. Additionally, when processing some detached DOM tree you may want to imitate | ||
863 | * that this happens inside an editable container (like it would happen inside the {@link CKEDITOR.editable}). To do so, | ||
864 | * you can temporarily attach this tree to an element with the `data-cke-editable` attribute and use the | ||
865 | * `checkOnlyAttributes` mode. | ||
866 | * | ||
867 | * @since 3.5 | ||
868 | * @param {Boolean} [checkOnlyAttributes=false] If `true`, only attributes will be checked, native methods will not | ||
869 | * be used. This parameter needs to be `true` to check hidden or detached elements. Introduced in 4.5. | ||
870 | * @returns {Boolean} | ||
871 | */ | ||
872 | isReadOnly: function( checkOnlyAttributes ) { | ||
873 | var element = this; | ||
874 | if ( this.type != CKEDITOR.NODE_ELEMENT ) | ||
875 | element = this.getParent(); | ||
876 | |||
877 | // Prevent Edge crash (#13609, #13919). | ||
878 | if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) { | ||
879 | checkOnlyAttributes = true; | ||
880 | } | ||
881 | |||
882 | if ( !checkOnlyAttributes && element && typeof element.$.isContentEditable != 'undefined' ) { | ||
883 | return !( element.$.isContentEditable || element.data( 'cke-editable' ) ); | ||
884 | } | ||
885 | else { | ||
886 | // Degrade for old browsers which don't support "isContentEditable", e.g. FF3 | ||
887 | |||
888 | while ( element ) { | ||
889 | if ( element.data( 'cke-editable' ) ) { | ||
890 | return false; | ||
891 | } else if ( element.hasAttribute( 'contenteditable' ) ) { | ||
892 | return element.getAttribute( 'contenteditable' ) == 'false'; | ||
893 | } | ||
894 | |||
895 | element = element.getParent(); | ||
896 | } | ||
897 | |||
898 | // Reached the root of DOM tree, no editable found. | ||
899 | return true; | ||
900 | } | ||
901 | } | ||
902 | } ); | ||
diff --git a/sources/core/dom/nodelist.js b/sources/core/dom/nodelist.js new file mode 100644 index 0000000..0f91eaa --- /dev/null +++ b/sources/core/dom/nodelist.js | |||
@@ -0,0 +1,43 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 0000000..b5e8736 --- /dev/null +++ b/sources/core/dom/range.js | |||
@@ -0,0 +1,2907 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.getText().replace( CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE, '' ).length; | ||
808 | |||
809 | return sum; | ||
810 | } | ||
811 | |||
812 | function normalizeTextNodes( limit ) { | ||
813 | var container = limit.container, | ||
814 | offset = limit.offset; | ||
815 | |||
816 | // If limit is between text nodes move it to the end of preceding one, | ||
817 | // because they will be merged. | ||
818 | if ( betweenTextNodes( container, offset ) ) { | ||
819 | container = container.getChild( offset - 1 ); | ||
820 | offset = container.getLength(); | ||
821 | } | ||
822 | |||
823 | // Now, if limit is anchored in element and has at least one node before it, | ||
824 | // it may happen that some of them will be merged. Normalize the offset | ||
825 | // by setting it to normalized index of its preceding, safe node. | ||
826 | // (safe == one for which getIndex(true) does not return -1, so one which won't disappear). | ||
827 | if ( container.type == CKEDITOR.NODE_ELEMENT && offset > 0 ) { | ||
828 | offset = getPrecedingSafeNodeIndex( container, offset ) + 1; | ||
829 | } | ||
830 | |||
831 | // The last step - fix the offset inside text node by adding | ||
832 | // lengths of preceding text nodes which will be merged with container. | ||
833 | if ( container.type == CKEDITOR.NODE_TEXT ) { | ||
834 | var precedingLength = getLengthOfPrecedingTextNodes( container ); | ||
835 | |||
836 | // Normal case - text node is not empty. | ||
837 | if ( container.getText() ) { | ||
838 | offset += precedingLength; | ||
839 | |||
840 | // Awful case - the text node is empty and thus will be totally lost. | ||
841 | // In this case we are trying to normalize the limit to the left: | ||
842 | // * either to the preceding text node, | ||
843 | // * or to the "gap" after the preceding element. | ||
844 | } else { | ||
845 | // Find the closest non-text sibling. | ||
846 | var precedingContainer = container.getPrevious( isNotText ); | ||
847 | |||
848 | // If there are any characters on the left, that means that we can anchor | ||
849 | // there, because this text node will not be lost. | ||
850 | if ( precedingLength ) { | ||
851 | offset = precedingLength; | ||
852 | |||
853 | if ( precedingContainer ) { | ||
854 | // The text node is the first node after the closest non-text sibling. | ||
855 | container = precedingContainer.getNext(); | ||
856 | } else { | ||
857 | // But if there was no non-text sibling, then the text node is the first child. | ||
858 | container = container.getParent().getFirst(); | ||
859 | } | ||
860 | |||
861 | // If there are no characters on the left, then anchor after the previous non-text node. | ||
862 | // E.g. (see tests for a legend :D): | ||
863 | // <b>x</b>(foo)({}bar) -> <b>x</b>[](foo)(bar) | ||
864 | } else { | ||
865 | container = container.getParent(); | ||
866 | offset = precedingContainer ? ( precedingContainer.getIndex( true ) + 1 ) : 0; | ||
867 | } | ||
868 | } | ||
869 | } | ||
870 | |||
871 | limit.container = container; | ||
872 | limit.offset = offset; | ||
873 | } | ||
874 | |||
875 | function normalizeFCSeq( limit, root ) { | ||
876 | var fcseq = root.getCustomData( 'cke-fillingChar' ); | ||
877 | |||
878 | if ( !fcseq ) { | ||
879 | return; | ||
880 | } | ||
881 | |||
882 | var container = limit.container; | ||
883 | |||
884 | if ( fcseq.equals( container ) ) { | ||
885 | limit.offset -= CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE.length; | ||
886 | |||
887 | // == 0 handles case when limit was at the end of FCS. | ||
888 | // < 0 handles all cases where limit was somewhere in the middle or at the beginning. | ||
889 | // > 0 (the "else" case) means cases where there are some more characters in the FCS node (FCSabc^def). | ||
890 | if ( limit.offset <= 0 ) { | ||
891 | limit.offset = container.getIndex(); | ||
892 | limit.container = container.getParent(); | ||
893 | } | ||
894 | return; | ||
895 | } | ||
896 | |||
897 | // And here goes the funny part - all other cases are handled inside node.getAddress() and getIndex() thanks to | ||
898 | // node.getIndex() being aware of FCS (handling it as an empty node). | ||
899 | } | ||
900 | |||
901 | // Finds a normalized index of a safe node preceding this one. | ||
902 | // Safe == one that will not disappear, so one for which getIndex( true ) does not return -1. | ||
903 | // Return -1 if there's no safe preceding node. | ||
904 | function getPrecedingSafeNodeIndex( container, offset ) { | ||
905 | var index; | ||
906 | |||
907 | while ( offset-- ) { | ||
908 | index = container.getChild( offset ).getIndex( true ); | ||
909 | |||
910 | if ( index >= 0 ) | ||
911 | return index; | ||
912 | } | ||
913 | |||
914 | return -1; | ||
915 | } | ||
916 | |||
917 | return function( normalized ) { | ||
918 | var collapsed = this.collapsed, | ||
919 | bmStart = { | ||
920 | container: this.startContainer, | ||
921 | offset: this.startOffset | ||
922 | }, | ||
923 | bmEnd = { | ||
924 | container: this.endContainer, | ||
925 | offset: this.endOffset | ||
926 | }; | ||
927 | |||
928 | if ( normalized ) { | ||
929 | normalizeTextNodes( bmStart ); | ||
930 | normalizeFCSeq( bmStart, this.root ); | ||
931 | |||
932 | if ( !collapsed ) { | ||
933 | normalizeTextNodes( bmEnd ); | ||
934 | normalizeFCSeq( bmEnd, this.root ); | ||
935 | } | ||
936 | } | ||
937 | |||
938 | return { | ||
939 | start: bmStart.container.getAddress( normalized ), | ||
940 | end: collapsed ? null : bmEnd.container.getAddress( normalized ), | ||
941 | startOffset: bmStart.offset, | ||
942 | endOffset: bmEnd.offset, | ||
943 | normalized: normalized, | ||
944 | collapsed: collapsed, | ||
945 | is2: true // It's a createBookmark2 bookmark. | ||
946 | }; | ||
947 | }; | ||
948 | } )(), | ||
949 | |||
950 | /** | ||
951 | * Moves this range to the given bookmark. See {@link #createBookmark} and {@link #createBookmark2}. | ||
952 | * | ||
953 | * If serializable bookmark passed, then its `<span>` markers will be removed. | ||
954 | * | ||
955 | * @param {Object} bookmark | ||
956 | */ | ||
957 | moveToBookmark: function( bookmark ) { | ||
958 | // Created with createBookmark2(). | ||
959 | if ( bookmark.is2 ) { | ||
960 | // Get the start information. | ||
961 | var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ), | ||
962 | startOffset = bookmark.startOffset; | ||
963 | |||
964 | // Get the end information. | ||
965 | var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ), | ||
966 | endOffset = bookmark.endOffset; | ||
967 | |||
968 | // Set the start boundary. | ||
969 | this.setStart( startContainer, startOffset ); | ||
970 | |||
971 | // Set the end boundary. If not available, collapse it. | ||
972 | if ( endContainer ) | ||
973 | this.setEnd( endContainer, endOffset ); | ||
974 | else | ||
975 | this.collapse( true ); | ||
976 | } | ||
977 | // Created with createBookmark(). | ||
978 | else { | ||
979 | var serializable = bookmark.serializable, | ||
980 | startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode, | ||
981 | endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode; | ||
982 | |||
983 | // Set the range start at the bookmark start node position. | ||
984 | this.setStartBefore( startNode ); | ||
985 | |||
986 | // Remove it, because it may interfere in the setEndBefore call. | ||
987 | startNode.remove(); | ||
988 | |||
989 | // Set the range end at the bookmark end node position, or simply | ||
990 | // collapse it if it is not available. | ||
991 | if ( endNode ) { | ||
992 | this.setEndBefore( endNode ); | ||
993 | endNode.remove(); | ||
994 | } else { | ||
995 | this.collapse( true ); | ||
996 | } | ||
997 | } | ||
998 | }, | ||
999 | |||
1000 | /** | ||
1001 | * Returns two nodes which are on the boundaries of this range. | ||
1002 | * | ||
1003 | * @returns {Object} | ||
1004 | * @returns {CKEDITOR.dom.node} return.startNode | ||
1005 | * @returns {CKEDITOR.dom.node} return.endNode | ||
1006 | * @todo precise desc/algorithm | ||
1007 | */ | ||
1008 | getBoundaryNodes: function() { | ||
1009 | var startNode = this.startContainer, | ||
1010 | endNode = this.endContainer, | ||
1011 | startOffset = this.startOffset, | ||
1012 | endOffset = this.endOffset, | ||
1013 | childCount; | ||
1014 | |||
1015 | if ( startNode.type == CKEDITOR.NODE_ELEMENT ) { | ||
1016 | childCount = startNode.getChildCount(); | ||
1017 | if ( childCount > startOffset ) { | ||
1018 | startNode = startNode.getChild( startOffset ); | ||
1019 | } else if ( childCount < 1 ) { | ||
1020 | startNode = startNode.getPreviousSourceNode(); | ||
1021 | } | ||
1022 | // startOffset > childCount but childCount is not 0 | ||
1023 | else { | ||
1024 | // Try to take the node just after the current position. | ||
1025 | startNode = startNode.$; | ||
1026 | while ( startNode.lastChild ) | ||
1027 | startNode = startNode.lastChild; | ||
1028 | startNode = new CKEDITOR.dom.node( startNode ); | ||
1029 | |||
1030 | // Normally we should take the next node in DFS order. But it | ||
1031 | // is also possible that we've already reached the end of | ||
1032 | // document. | ||
1033 | startNode = startNode.getNextSourceNode() || startNode; | ||
1034 | } | ||
1035 | } | ||
1036 | |||
1037 | if ( endNode.type == CKEDITOR.NODE_ELEMENT ) { | ||
1038 | childCount = endNode.getChildCount(); | ||
1039 | if ( childCount > endOffset ) { | ||
1040 | endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true ); | ||
1041 | } else if ( childCount < 1 ) { | ||
1042 | endNode = endNode.getPreviousSourceNode(); | ||
1043 | } | ||
1044 | // endOffset > childCount but childCount is not 0. | ||
1045 | else { | ||
1046 | // Try to take the node just before the current position. | ||
1047 | endNode = endNode.$; | ||
1048 | while ( endNode.lastChild ) | ||
1049 | endNode = endNode.lastChild; | ||
1050 | endNode = new CKEDITOR.dom.node( endNode ); | ||
1051 | } | ||
1052 | } | ||
1053 | |||
1054 | // Sometimes the endNode will come right before startNode for collapsed | ||
1055 | // ranges. Fix it. (#3780) | ||
1056 | if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING ) | ||
1057 | startNode = endNode; | ||
1058 | |||
1059 | return { startNode: startNode, endNode: endNode }; | ||
1060 | }, | ||
1061 | |||
1062 | /** | ||
1063 | * Find the node which fully contains the range. | ||
1064 | * | ||
1065 | * @param {Boolean} [includeSelf=false] | ||
1066 | * @param {Boolean} [ignoreTextNode=false] Whether ignore {@link CKEDITOR#NODE_TEXT} type. | ||
1067 | * @returns {CKEDITOR.dom.element} | ||
1068 | */ | ||
1069 | getCommonAncestor: function( includeSelf, ignoreTextNode ) { | ||
1070 | var start = this.startContainer, | ||
1071 | end = this.endContainer, | ||
1072 | ancestor; | ||
1073 | |||
1074 | if ( start.equals( end ) ) { | ||
1075 | if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 ) | ||
1076 | ancestor = start.getChild( this.startOffset ); | ||
1077 | else | ||
1078 | ancestor = start; | ||
1079 | } else { | ||
1080 | ancestor = start.getCommonAncestor( end ); | ||
1081 | } | ||
1082 | |||
1083 | return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor; | ||
1084 | }, | ||
1085 | |||
1086 | /** | ||
1087 | * Transforms the {@link #startContainer} and {@link #endContainer} properties from text | ||
1088 | * nodes to element nodes, whenever possible. This is actually possible | ||
1089 | * if either of the boundary containers point to a text node, and its | ||
1090 | * offset is set to zero, or after the last char in the node. | ||
1091 | */ | ||
1092 | optimize: function() { | ||
1093 | var container = this.startContainer; | ||
1094 | var offset = this.startOffset; | ||
1095 | |||
1096 | if ( container.type != CKEDITOR.NODE_ELEMENT ) { | ||
1097 | if ( !offset ) | ||
1098 | this.setStartBefore( container ); | ||
1099 | else if ( offset >= container.getLength() ) | ||
1100 | this.setStartAfter( container ); | ||
1101 | } | ||
1102 | |||
1103 | container = this.endContainer; | ||
1104 | offset = this.endOffset; | ||
1105 | |||
1106 | if ( container.type != CKEDITOR.NODE_ELEMENT ) { | ||
1107 | if ( !offset ) | ||
1108 | this.setEndBefore( container ); | ||
1109 | else if ( offset >= container.getLength() ) | ||
1110 | this.setEndAfter( container ); | ||
1111 | } | ||
1112 | }, | ||
1113 | |||
1114 | /** | ||
1115 | * Move the range out of bookmark nodes if they'd been the container. | ||
1116 | */ | ||
1117 | optimizeBookmark: function() { | ||
1118 | var startNode = this.startContainer, | ||
1119 | endNode = this.endContainer; | ||
1120 | |||
1121 | if ( startNode.is && startNode.is( 'span' ) && startNode.data( 'cke-bookmark' ) ) | ||
1122 | this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START ); | ||
1123 | if ( endNode && endNode.is && endNode.is( 'span' ) && endNode.data( 'cke-bookmark' ) ) | ||
1124 | this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END ); | ||
1125 | }, | ||
1126 | |||
1127 | /** | ||
1128 | * @param {Boolean} [ignoreStart=false] | ||
1129 | * @param {Boolean} [ignoreEnd=false] | ||
1130 | * @todo precise desc/algorithm | ||
1131 | */ | ||
1132 | trim: function( ignoreStart, ignoreEnd ) { | ||
1133 | var startContainer = this.startContainer, | ||
1134 | startOffset = this.startOffset, | ||
1135 | collapsed = this.collapsed; | ||
1136 | if ( ( !ignoreStart || collapsed ) && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) { | ||
1137 | // If the offset is zero, we just insert the new node before | ||
1138 | // the start. | ||
1139 | if ( !startOffset ) { | ||
1140 | startOffset = startContainer.getIndex(); | ||
1141 | startContainer = startContainer.getParent(); | ||
1142 | } | ||
1143 | // If the offset is at the end, we'll insert it after the text | ||
1144 | // node. | ||
1145 | else if ( startOffset >= startContainer.getLength() ) { | ||
1146 | startOffset = startContainer.getIndex() + 1; | ||
1147 | startContainer = startContainer.getParent(); | ||
1148 | } | ||
1149 | // In other case, we split the text node and insert the new | ||
1150 | // node at the split point. | ||
1151 | else { | ||
1152 | var nextText = startContainer.split( startOffset ); | ||
1153 | |||
1154 | startOffset = startContainer.getIndex() + 1; | ||
1155 | startContainer = startContainer.getParent(); | ||
1156 | |||
1157 | // Check all necessity of updating the end boundary. | ||
1158 | if ( this.startContainer.equals( this.endContainer ) ) | ||
1159 | this.setEnd( nextText, this.endOffset - this.startOffset ); | ||
1160 | else if ( startContainer.equals( this.endContainer ) ) | ||
1161 | this.endOffset += 1; | ||
1162 | } | ||
1163 | |||
1164 | this.setStart( startContainer, startOffset ); | ||
1165 | |||
1166 | if ( collapsed ) { | ||
1167 | this.collapse( true ); | ||
1168 | return; | ||
1169 | } | ||
1170 | } | ||
1171 | |||
1172 | var endContainer = this.endContainer; | ||
1173 | var endOffset = this.endOffset; | ||
1174 | |||
1175 | if ( !( ignoreEnd || collapsed ) && endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) { | ||
1176 | // If the offset is zero, we just insert the new node before | ||
1177 | // the start. | ||
1178 | if ( !endOffset ) { | ||
1179 | endOffset = endContainer.getIndex(); | ||
1180 | endContainer = endContainer.getParent(); | ||
1181 | } | ||
1182 | // If the offset is at the end, we'll insert it after the text | ||
1183 | // node. | ||
1184 | else if ( endOffset >= endContainer.getLength() ) { | ||
1185 | endOffset = endContainer.getIndex() + 1; | ||
1186 | endContainer = endContainer.getParent(); | ||
1187 | } | ||
1188 | // In other case, we split the text node and insert the new | ||
1189 | // node at the split point. | ||
1190 | else { | ||
1191 | endContainer.split( endOffset ); | ||
1192 | |||
1193 | endOffset = endContainer.getIndex() + 1; | ||
1194 | endContainer = endContainer.getParent(); | ||
1195 | } | ||
1196 | |||
1197 | this.setEnd( endContainer, endOffset ); | ||
1198 | } | ||
1199 | }, | ||
1200 | |||
1201 | /** | ||
1202 | * Expands the range so that partial units are completely contained. | ||
1203 | * | ||
1204 | * @param unit {Number} The unit type to expand with. | ||
1205 | * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding. | ||
1206 | */ | ||
1207 | enlarge: function( unit, excludeBrs ) { | ||
1208 | var leadingWhitespaceRegex = new RegExp( /[^\s\ufeff]/ ); | ||
1209 | |||
1210 | switch ( unit ) { | ||
1211 | case CKEDITOR.ENLARGE_INLINE: | ||
1212 | var enlargeInlineOnly = 1; | ||
1213 | |||
1214 | /* falls through */ | ||
1215 | case CKEDITOR.ENLARGE_ELEMENT: | ||
1216 | |||
1217 | if ( this.collapsed ) | ||
1218 | return; | ||
1219 | |||
1220 | // Get the common ancestor. | ||
1221 | var commonAncestor = this.getCommonAncestor(); | ||
1222 | |||
1223 | var boundary = this.root; | ||
1224 | |||
1225 | // For each boundary | ||
1226 | // a. Depending on its position, find out the first node to be checked (a sibling) or, | ||
1227 | // if not available, to be enlarge. | ||
1228 | // b. Go ahead checking siblings and enlarging the boundary as much as possible until the | ||
1229 | // common ancestor is not reached. After reaching the common ancestor, just save the | ||
1230 | // enlargeable node to be used later. | ||
1231 | |||
1232 | var startTop, endTop; | ||
1233 | |||
1234 | var enlargeable, sibling, commonReached; | ||
1235 | |||
1236 | // Indicates that the node can be added only if whitespace | ||
1237 | // is available before it. | ||
1238 | var needsWhiteSpace = false; | ||
1239 | var isWhiteSpace; | ||
1240 | var siblingText; | ||
1241 | |||
1242 | // Process the start boundary. | ||
1243 | |||
1244 | var container = this.startContainer; | ||
1245 | var offset = this.startOffset; | ||
1246 | |||
1247 | if ( container.type == CKEDITOR.NODE_TEXT ) { | ||
1248 | if ( offset ) { | ||
1249 | // Check if there is any non-space text before the | ||
1250 | // offset. Otherwise, container is null. | ||
1251 | container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container; | ||
1252 | |||
1253 | // If we found only whitespace in the node, it | ||
1254 | // means that we'll need more whitespace to be able | ||
1255 | // to expand. For example, <i> can be expanded in | ||
1256 | // "A <i> [B]</i>", but not in "A<i> [B]</i>". | ||
1257 | needsWhiteSpace = !!container; | ||
1258 | } | ||
1259 | |||
1260 | if ( container ) { | ||
1261 | if ( !( sibling = container.getPrevious() ) ) | ||
1262 | enlargeable = container.getParent(); | ||
1263 | } | ||
1264 | } else { | ||
1265 | // If we have offset, get the node preceeding it as the | ||
1266 | // first sibling to be checked. | ||
1267 | if ( offset ) | ||
1268 | sibling = container.getChild( offset - 1 ) || container.getLast(); | ||
1269 | |||
1270 | // If there is no sibling, mark the container to be | ||
1271 | // enlarged. | ||
1272 | if ( !sibling ) | ||
1273 | enlargeable = container; | ||
1274 | } | ||
1275 | |||
1276 | // Ensures that enlargeable can be indeed enlarged, if not it will be nulled. | ||
1277 | enlargeable = getValidEnlargeable( enlargeable ); | ||
1278 | |||
1279 | while ( enlargeable || sibling ) { | ||
1280 | if ( enlargeable && !sibling ) { | ||
1281 | // If we reached the common ancestor, mark the flag | ||
1282 | // for it. | ||
1283 | if ( !commonReached && enlargeable.equals( commonAncestor ) ) | ||
1284 | commonReached = true; | ||
1285 | |||
1286 | if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) ) | ||
1287 | break; | ||
1288 | |||
1289 | // If we don't need space or this element breaks | ||
1290 | // the line, then enlarge it. | ||
1291 | if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) { | ||
1292 | needsWhiteSpace = false; | ||
1293 | |||
1294 | // If the common ancestor has been reached, | ||
1295 | // we'll not enlarge it immediately, but just | ||
1296 | // mark it to be enlarged later if the end | ||
1297 | // boundary also enlarges it. | ||
1298 | if ( commonReached ) | ||
1299 | startTop = enlargeable; | ||
1300 | else | ||
1301 | this.setStartBefore( enlargeable ); | ||
1302 | } | ||
1303 | |||
1304 | sibling = enlargeable.getPrevious(); | ||
1305 | } | ||
1306 | |||
1307 | // Check all sibling nodes preceeding the enlargeable | ||
1308 | // node. The node wil lbe enlarged only if none of them | ||
1309 | // blocks it. | ||
1310 | while ( sibling ) { | ||
1311 | // This flag indicates that this node has | ||
1312 | // whitespaces at the end. | ||
1313 | isWhiteSpace = false; | ||
1314 | |||
1315 | if ( sibling.type == CKEDITOR.NODE_COMMENT ) { | ||
1316 | sibling = sibling.getPrevious(); | ||
1317 | continue; | ||
1318 | } else if ( sibling.type == CKEDITOR.NODE_TEXT ) { | ||
1319 | siblingText = sibling.getText(); | ||
1320 | |||
1321 | if ( leadingWhitespaceRegex.test( siblingText ) ) | ||
1322 | sibling = null; | ||
1323 | |||
1324 | isWhiteSpace = /[\s\ufeff]$/.test( siblingText ); | ||
1325 | } else { | ||
1326 | // #12221 (Chrome) plus #11111 (Safari). | ||
1327 | var offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0; | ||
1328 | |||
1329 | // If this is a visible element. | ||
1330 | // We need to check for the bookmark attribute because IE insists on | ||
1331 | // rendering the display:none nodes we use for bookmarks. (#3363) | ||
1332 | // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041) | ||
1333 | if ( ( sibling.$.offsetWidth > offsetWidth0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) { | ||
1334 | // We'll accept it only if we need | ||
1335 | // whitespace, and this is an inline | ||
1336 | // element with whitespace only. | ||
1337 | if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) { | ||
1338 | // It must contains spaces and inline elements only. | ||
1339 | |||
1340 | siblingText = sibling.getText(); | ||
1341 | |||
1342 | if ( leadingWhitespaceRegex.test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF) | ||
1343 | sibling = null; | ||
1344 | else { | ||
1345 | var allChildren = sibling.$.getElementsByTagName( '*' ); | ||
1346 | for ( var i = 0, child; child = allChildren[ i++ ]; ) { | ||
1347 | if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) { | ||
1348 | sibling = null; | ||
1349 | break; | ||
1350 | } | ||
1351 | } | ||
1352 | } | ||
1353 | |||
1354 | if ( sibling ) | ||
1355 | isWhiteSpace = !!siblingText.length; | ||
1356 | } else { | ||
1357 | sibling = null; | ||
1358 | } | ||
1359 | } | ||
1360 | } | ||
1361 | |||
1362 | // A node with whitespaces has been found. | ||
1363 | if ( isWhiteSpace ) { | ||
1364 | // Enlarge the last enlargeable node, if we | ||
1365 | // were waiting for spaces. | ||
1366 | if ( needsWhiteSpace ) { | ||
1367 | if ( commonReached ) | ||
1368 | startTop = enlargeable; | ||
1369 | else if ( enlargeable ) | ||
1370 | this.setStartBefore( enlargeable ); | ||
1371 | } else { | ||
1372 | needsWhiteSpace = true; | ||
1373 | } | ||
1374 | } | ||
1375 | |||
1376 | if ( sibling ) { | ||
1377 | var next = sibling.getPrevious(); | ||
1378 | |||
1379 | if ( !enlargeable && !next ) { | ||
1380 | // Set the sibling as enlargeable, so it's | ||
1381 | // parent will be get later outside this while. | ||
1382 | enlargeable = sibling; | ||
1383 | sibling = null; | ||
1384 | break; | ||
1385 | } | ||
1386 | |||
1387 | sibling = next; | ||
1388 | } else { | ||
1389 | // If sibling has been set to null, then we | ||
1390 | // need to stop enlarging. | ||
1391 | enlargeable = null; | ||
1392 | } | ||
1393 | } | ||
1394 | |||
1395 | if ( enlargeable ) | ||
1396 | enlargeable = getValidEnlargeable( enlargeable.getParent() ); | ||
1397 | } | ||
1398 | |||
1399 | // Process the end boundary. This is basically the same | ||
1400 | // code used for the start boundary, with small changes to | ||
1401 | // make it work in the oposite side (to the right). This | ||
1402 | // makes it difficult to reuse the code here. So, fixes to | ||
1403 | // the above code are likely to be replicated here. | ||
1404 | |||
1405 | container = this.endContainer; | ||
1406 | offset = this.endOffset; | ||
1407 | |||
1408 | // Reset the common variables. | ||
1409 | enlargeable = sibling = null; | ||
1410 | commonReached = needsWhiteSpace = false; | ||
1411 | |||
1412 | // Function check if there are only whitespaces from the given starting point | ||
1413 | // (startContainer and startOffset) till the end on block. | ||
1414 | // Examples ("[" is the start point): | ||
1415 | // - <p>foo[ </p> - will return true, | ||
1416 | // - <p><b>foo[ </b> </p> - will return true, | ||
1417 | // - <p>foo[ bar</p> - will return false, | ||
1418 | // - <p><b>foo[ </b>bar</p> - will return false, | ||
1419 | // - <p>foo[ <b></b></p> - will return false. | ||
1420 | function onlyWhiteSpaces( startContainer, startOffset ) { | ||
1421 | // We need to enlarge range if there is white space at the end of the block, | ||
1422 | // because it is not displayed in WYSIWYG mode and user can not select it. So | ||
1423 | // "<p>foo[bar] </p>" should be changed to "<p>foo[bar ]</p>". On the other hand | ||
1424 | // we should do nothing if we are not at the end of the block, so this should not | ||
1425 | // be changed: "<p><i>[foo] </i>bar</p>". | ||
1426 | var walkerRange = new CKEDITOR.dom.range( boundary ); | ||
1427 | walkerRange.setStart( startContainer, startOffset ); | ||
1428 | // The guard will find the end of range so I put boundary here. | ||
1429 | walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END ); | ||
1430 | |||
1431 | var walker = new CKEDITOR.dom.walker( walkerRange ), | ||
1432 | node; | ||
1433 | |||
1434 | walker.guard = function( node ) { | ||
1435 | // Stop if you exit block. | ||
1436 | return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() ); | ||
1437 | }; | ||
1438 | |||
1439 | while ( ( node = walker.next() ) ) { | ||
1440 | if ( node.type != CKEDITOR.NODE_TEXT ) { | ||
1441 | // Stop if you enter to any node (walker.next() will return node only | ||
1442 | // it goes out, not if it is go into node). | ||
1443 | return false; | ||
1444 | } else { | ||
1445 | // Trim the first node to startOffset. | ||
1446 | if ( node != startContainer ) | ||
1447 | siblingText = node.getText(); | ||
1448 | else | ||
1449 | siblingText = node.substring( startOffset ); | ||
1450 | |||
1451 | // Check if it is white space. | ||
1452 | if ( leadingWhitespaceRegex.test( siblingText ) ) | ||
1453 | return false; | ||
1454 | } | ||
1455 | } | ||
1456 | |||
1457 | return true; | ||
1458 | } | ||
1459 | |||
1460 | if ( container.type == CKEDITOR.NODE_TEXT ) { | ||
1461 | // Check if there is only white space after the offset. | ||
1462 | if ( CKEDITOR.tools.trim( container.substring( offset ) ).length ) { | ||
1463 | // If we found only whitespace in the node, it | ||
1464 | // means that we'll need more whitespace to be able | ||
1465 | // to expand. For example, <i> can be expanded in | ||
1466 | // "A <i> [B]</i>", but not in "A<i> [B]</i>". | ||
1467 | needsWhiteSpace = true; | ||
1468 | } else { | ||
1469 | needsWhiteSpace = !container.getLength(); | ||
1470 | |||
1471 | if ( offset == container.getLength() ) { | ||
1472 | // If we are at the end of container and this is the last text node, | ||
1473 | // we should enlarge end to the parent. | ||
1474 | if ( !( sibling = container.getNext() ) ) | ||
1475 | enlargeable = container.getParent(); | ||
1476 | } else { | ||
1477 | // If we are in the middle on text node and there are only whitespaces | ||
1478 | // till the end of block, we should enlarge element. | ||
1479 | if ( onlyWhiteSpaces( container, offset ) ) | ||
1480 | enlargeable = container.getParent(); | ||
1481 | } | ||
1482 | } | ||
1483 | } else { | ||
1484 | // Get the node right after the boudary to be checked | ||
1485 | // first. | ||
1486 | sibling = container.getChild( offset ); | ||
1487 | |||
1488 | if ( !sibling ) | ||
1489 | enlargeable = container; | ||
1490 | } | ||
1491 | |||
1492 | while ( enlargeable || sibling ) { | ||
1493 | if ( enlargeable && !sibling ) { | ||
1494 | if ( !commonReached && enlargeable.equals( commonAncestor ) ) | ||
1495 | commonReached = true; | ||
1496 | |||
1497 | if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) ) | ||
1498 | break; | ||
1499 | |||
1500 | if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) { | ||
1501 | needsWhiteSpace = false; | ||
1502 | |||
1503 | if ( commonReached ) | ||
1504 | endTop = enlargeable; | ||
1505 | else if ( enlargeable ) | ||
1506 | this.setEndAfter( enlargeable ); | ||
1507 | } | ||
1508 | |||
1509 | sibling = enlargeable.getNext(); | ||
1510 | } | ||
1511 | |||
1512 | while ( sibling ) { | ||
1513 | isWhiteSpace = false; | ||
1514 | |||
1515 | if ( sibling.type == CKEDITOR.NODE_TEXT ) { | ||
1516 | siblingText = sibling.getText(); | ||
1517 | |||
1518 | // Check if there are not whitespace characters till the end of editable. | ||
1519 | // If so stop expanding. | ||
1520 | if ( !onlyWhiteSpaces( sibling, 0 ) ) | ||
1521 | sibling = null; | ||
1522 | |||
1523 | isWhiteSpace = /^[\s\ufeff]/.test( siblingText ); | ||
1524 | } else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) { | ||
1525 | // If this is a visible element. | ||
1526 | // We need to check for the bookmark attribute because IE insists on | ||
1527 | // rendering the display:none nodes we use for bookmarks. (#3363) | ||
1528 | // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041) | ||
1529 | if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) { | ||
1530 | // We'll accept it only if we need | ||
1531 | // whitespace, and this is an inline | ||
1532 | // element with whitespace only. | ||
1533 | if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) { | ||
1534 | // It must contains spaces and inline elements only. | ||
1535 | |||
1536 | siblingText = sibling.getText(); | ||
1537 | |||
1538 | if ( leadingWhitespaceRegex.test( siblingText ) ) | ||
1539 | sibling = null; | ||
1540 | else { | ||
1541 | allChildren = sibling.$.getElementsByTagName( '*' ); | ||
1542 | for ( i = 0; child = allChildren[ i++ ]; ) { | ||
1543 | if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) { | ||
1544 | sibling = null; | ||
1545 | break; | ||
1546 | } | ||
1547 | } | ||
1548 | } | ||
1549 | |||
1550 | if ( sibling ) | ||
1551 | isWhiteSpace = !!siblingText.length; | ||
1552 | } else { | ||
1553 | sibling = null; | ||
1554 | } | ||
1555 | } | ||
1556 | } else { | ||
1557 | isWhiteSpace = 1; | ||
1558 | } | ||
1559 | |||
1560 | if ( isWhiteSpace ) { | ||
1561 | if ( needsWhiteSpace ) { | ||
1562 | if ( commonReached ) | ||
1563 | endTop = enlargeable; | ||
1564 | else | ||
1565 | this.setEndAfter( enlargeable ); | ||
1566 | } | ||
1567 | } | ||
1568 | |||
1569 | if ( sibling ) { | ||
1570 | next = sibling.getNext(); | ||
1571 | |||
1572 | if ( !enlargeable && !next ) { | ||
1573 | enlargeable = sibling; | ||
1574 | sibling = null; | ||
1575 | break; | ||
1576 | } | ||
1577 | |||
1578 | sibling = next; | ||
1579 | } else { | ||
1580 | // If sibling has been set to null, then we | ||
1581 | // need to stop enlarging. | ||
1582 | enlargeable = null; | ||
1583 | } | ||
1584 | } | ||
1585 | |||
1586 | if ( enlargeable ) | ||
1587 | enlargeable = getValidEnlargeable( enlargeable.getParent() ); | ||
1588 | } | ||
1589 | |||
1590 | // If the common ancestor can be enlarged by both boundaries, then include it also. | ||
1591 | if ( startTop && endTop ) { | ||
1592 | commonAncestor = startTop.contains( endTop ) ? endTop : startTop; | ||
1593 | |||
1594 | this.setStartBefore( commonAncestor ); | ||
1595 | this.setEndAfter( commonAncestor ); | ||
1596 | } | ||
1597 | break; | ||
1598 | |||
1599 | case CKEDITOR.ENLARGE_BLOCK_CONTENTS: | ||
1600 | case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS: | ||
1601 | |||
1602 | // Enlarging the start boundary. | ||
1603 | var walkerRange = new CKEDITOR.dom.range( this.root ); | ||
1604 | |||
1605 | boundary = this.root; | ||
1606 | |||
1607 | walkerRange.setStartAt( boundary, CKEDITOR.POSITION_AFTER_START ); | ||
1608 | walkerRange.setEnd( this.startContainer, this.startOffset ); | ||
1609 | |||
1610 | var walker = new CKEDITOR.dom.walker( walkerRange ), | ||
1611 | blockBoundary, // The node on which the enlarging should stop. | ||
1612 | tailBr, // In case BR as block boundary. | ||
1613 | notBlockBoundary = CKEDITOR.dom.walker.blockBoundary( ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br: 1 } : null ), | ||
1614 | inNonEditable = null, | ||
1615 | // Record the encountered 'blockBoundary' for later use. | ||
1616 | boundaryGuard = function( node ) { | ||
1617 | // We should not check contents of non-editable elements. It may happen | ||
1618 | // that inline widget has display:table child which should not block range#enlarge. | ||
1619 | // When encoutered non-editable element... | ||
1620 | if ( node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'false' ) { | ||
1621 | if ( inNonEditable ) { | ||
1622 | // ... in which we already were, reset it (because we're leaving it) and return. | ||
1623 | if ( inNonEditable.equals( node ) ) { | ||
1624 | inNonEditable = null; | ||
1625 | return; | ||
1626 | } | ||
1627 | // ... which we're entering, remember it but check it (no return). | ||
1628 | } else { | ||
1629 | inNonEditable = node; | ||
1630 | } | ||
1631 | // When we are in non-editable element, do not check if current node is a block boundary. | ||
1632 | } else if ( inNonEditable ) { | ||
1633 | return; | ||
1634 | } | ||
1635 | |||
1636 | var retval = notBlockBoundary( node ); | ||
1637 | if ( !retval ) | ||
1638 | blockBoundary = node; | ||
1639 | return retval; | ||
1640 | }, | ||
1641 | // Record the encounted 'tailBr' for later use. | ||
1642 | tailBrGuard = function( node ) { | ||
1643 | var retval = boundaryGuard( node ); | ||
1644 | if ( !retval && node.is && node.is( 'br' ) ) | ||
1645 | tailBr = node; | ||
1646 | return retval; | ||
1647 | }; | ||
1648 | |||
1649 | walker.guard = boundaryGuard; | ||
1650 | |||
1651 | enlargeable = walker.lastBackward(); | ||
1652 | |||
1653 | // It's the body which stop the enlarging if no block boundary found. | ||
1654 | blockBoundary = blockBoundary || boundary; | ||
1655 | |||
1656 | // Start the range either after the end of found block (<p>...</p>[text) | ||
1657 | // or at the start of block (<p>[text...), by comparing the document position | ||
1658 | // with 'enlargeable' node. | ||
1659 | this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() || | ||
1660 | enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END ); | ||
1661 | |||
1662 | // Avoid enlarging the range further when end boundary spans right after the BR. (#7490) | ||
1663 | if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) { | ||
1664 | var theRange = this.clone(); | ||
1665 | walker = new CKEDITOR.dom.walker( theRange ); | ||
1666 | |||
1667 | var whitespaces = CKEDITOR.dom.walker.whitespaces(), | ||
1668 | bookmark = CKEDITOR.dom.walker.bookmark(); | ||
1669 | |||
1670 | walker.evaluator = function( node ) { | ||
1671 | return !whitespaces( node ) && !bookmark( node ); | ||
1672 | }; | ||
1673 | var previous = walker.previous(); | ||
1674 | if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) ) | ||
1675 | return; | ||
1676 | } | ||
1677 | |||
1678 | // Enlarging the end boundary. | ||
1679 | // Set up new range and reset all flags (blockBoundary, inNonEditable, tailBr). | ||
1680 | |||
1681 | walkerRange = this.clone(); | ||
1682 | walkerRange.collapse(); | ||
1683 | walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END ); | ||
1684 | walker = new CKEDITOR.dom.walker( walkerRange ); | ||
1685 | |||
1686 | // tailBrGuard only used for on range end. | ||
1687 | walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? tailBrGuard : boundaryGuard; | ||
1688 | blockBoundary = inNonEditable = tailBr = null; | ||
1689 | |||
1690 | // End the range right before the block boundary node. | ||
1691 | enlargeable = walker.lastForward(); | ||
1692 | |||
1693 | // It's the body which stop the enlarging if no block boundary found. | ||
1694 | blockBoundary = blockBoundary || boundary; | ||
1695 | |||
1696 | // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>) | ||
1697 | // by comparing the document position with 'enlargeable' node. | ||
1698 | this.setEndAt( blockBoundary, ( !enlargeable && this.checkEndOfBlock() || | ||
1699 | enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_BEFORE_START ); | ||
1700 | // We must include the <br> at the end of range if there's | ||
1701 | // one and we're expanding list item contents | ||
1702 | if ( tailBr ) { | ||
1703 | this.setEndAfter( tailBr ); | ||
1704 | } | ||
1705 | } | ||
1706 | |||
1707 | // Ensures that returned element can be enlarged by selection, null otherwise. | ||
1708 | // @param {CKEDITOR.dom.element} enlargeable | ||
1709 | // @returns {CKEDITOR.dom.element/null} | ||
1710 | function getValidEnlargeable( enlargeable ) { | ||
1711 | return enlargeable && enlargeable.type == CKEDITOR.NODE_ELEMENT && enlargeable.hasAttribute( 'contenteditable' ) ? | ||
1712 | null : enlargeable; | ||
1713 | } | ||
1714 | }, | ||
1715 | |||
1716 | /** | ||
1717 | * Descrease the range to make sure that boundaries | ||
1718 | * always anchor beside text nodes or innermost element. | ||
1719 | * | ||
1720 | * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}). | ||
1721 | * | ||
1722 | * * {@link CKEDITOR#SHRINK_ELEMENT} - Shrink the range boundaries to the edge of the innermost element. | ||
1723 | * * {@link CKEDITOR#SHRINK_TEXT} - Shrink the range boudaries to anchor by the side of enclosed text | ||
1724 | * node, range remains if there's no text nodes on boundaries at all. | ||
1725 | * | ||
1726 | * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node. | ||
1727 | */ | ||
1728 | shrink: function( mode, selectContents, shrinkOnBlockBoundary ) { | ||
1729 | // Unable to shrink a collapsed range. | ||
1730 | if ( !this.collapsed ) { | ||
1731 | mode = mode || CKEDITOR.SHRINK_TEXT; | ||
1732 | |||
1733 | var walkerRange = this.clone(); | ||
1734 | |||
1735 | var startContainer = this.startContainer, | ||
1736 | endContainer = this.endContainer, | ||
1737 | startOffset = this.startOffset, | ||
1738 | endOffset = this.endOffset; | ||
1739 | |||
1740 | // Whether the start/end boundary is moveable. | ||
1741 | var moveStart = 1, | ||
1742 | moveEnd = 1; | ||
1743 | |||
1744 | if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) { | ||
1745 | if ( !startOffset ) | ||
1746 | walkerRange.setStartBefore( startContainer ); | ||
1747 | else if ( startOffset >= startContainer.getLength() ) | ||
1748 | walkerRange.setStartAfter( startContainer ); | ||
1749 | else { | ||
1750 | // Enlarge the range properly to avoid walker making | ||
1751 | // DOM changes caused by triming the text nodes later. | ||
1752 | walkerRange.setStartBefore( startContainer ); | ||
1753 | moveStart = 0; | ||
1754 | } | ||
1755 | } | ||
1756 | |||
1757 | if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) { | ||
1758 | if ( !endOffset ) | ||
1759 | walkerRange.setEndBefore( endContainer ); | ||
1760 | else if ( endOffset >= endContainer.getLength() ) | ||
1761 | walkerRange.setEndAfter( endContainer ); | ||
1762 | else { | ||
1763 | walkerRange.setEndAfter( endContainer ); | ||
1764 | moveEnd = 0; | ||
1765 | } | ||
1766 | } | ||
1767 | |||
1768 | var walker = new CKEDITOR.dom.walker( walkerRange ), | ||
1769 | isBookmark = CKEDITOR.dom.walker.bookmark(); | ||
1770 | |||
1771 | walker.evaluator = function( node ) { | ||
1772 | return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT ); | ||
1773 | }; | ||
1774 | |||
1775 | var currentElement; | ||
1776 | walker.guard = function( node, movingOut ) { | ||
1777 | if ( isBookmark( node ) ) | ||
1778 | return true; | ||
1779 | |||
1780 | // Stop when we're shrink in element mode while encountering a text node. | ||
1781 | if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT ) | ||
1782 | return false; | ||
1783 | |||
1784 | // Stop when we've already walked "through" an element. | ||
1785 | if ( movingOut && node.equals( currentElement ) ) | ||
1786 | return false; | ||
1787 | |||
1788 | if ( shrinkOnBlockBoundary === false && node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() ) | ||
1789 | return false; | ||
1790 | |||
1791 | // Stop shrinking when encountering an editable border. | ||
1792 | if ( node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'contenteditable' ) ) | ||
1793 | return false; | ||
1794 | |||
1795 | if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT ) | ||
1796 | currentElement = node; | ||
1797 | |||
1798 | return true; | ||
1799 | }; | ||
1800 | |||
1801 | if ( moveStart ) { | ||
1802 | var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next' ](); | ||
1803 | textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START ); | ||
1804 | } | ||
1805 | |||
1806 | if ( moveEnd ) { | ||
1807 | walker.reset(); | ||
1808 | var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous' ](); | ||
1809 | textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END ); | ||
1810 | } | ||
1811 | |||
1812 | return !!( moveStart || moveEnd ); | ||
1813 | } | ||
1814 | }, | ||
1815 | |||
1816 | /** | ||
1817 | * Inserts a node at the start of the range. The range will be expanded | ||
1818 | * the contain the node. | ||
1819 | * | ||
1820 | * @param {CKEDITOR.dom.node} node | ||
1821 | */ | ||
1822 | insertNode: function( node ) { | ||
1823 | this.optimizeBookmark(); | ||
1824 | this.trim( false, true ); | ||
1825 | |||
1826 | var startContainer = this.startContainer; | ||
1827 | var startOffset = this.startOffset; | ||
1828 | |||
1829 | var nextNode = startContainer.getChild( startOffset ); | ||
1830 | |||
1831 | if ( nextNode ) | ||
1832 | node.insertBefore( nextNode ); | ||
1833 | else | ||
1834 | startContainer.append( node ); | ||
1835 | |||
1836 | // Check if we need to update the end boundary. | ||
1837 | if ( node.getParent() && node.getParent().equals( this.endContainer ) ) | ||
1838 | this.endOffset++; | ||
1839 | |||
1840 | // Expand the range to embrace the new node. | ||
1841 | this.setStartBefore( node ); | ||
1842 | }, | ||
1843 | |||
1844 | /** | ||
1845 | * Moves the range to given position according to specified node. | ||
1846 | * | ||
1847 | * // HTML: <p>Foo <b>bar</b></p> | ||
1848 | * range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START ); | ||
1849 | * // Range will be moved to: <p>Foo ^<b>bar</b></p> | ||
1850 | * | ||
1851 | * See also {@link #setStartAt} and {@link #setEndAt}. | ||
1852 | * | ||
1853 | * @param {CKEDITOR.dom.node} node The node according to which position will be set. | ||
1854 | * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START}, | ||
1855 | * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END}, | ||
1856 | * {@link CKEDITOR#POSITION_AFTER_END}. | ||
1857 | */ | ||
1858 | moveToPosition: function( node, position ) { | ||
1859 | this.setStartAt( node, position ); | ||
1860 | this.collapse( true ); | ||
1861 | }, | ||
1862 | |||
1863 | /** | ||
1864 | * Moves the range to the exact position of the specified range. | ||
1865 | * | ||
1866 | * @param {CKEDITOR.dom.range} range | ||
1867 | */ | ||
1868 | moveToRange: function( range ) { | ||
1869 | this.setStart( range.startContainer, range.startOffset ); | ||
1870 | this.setEnd( range.endContainer, range.endOffset ); | ||
1871 | }, | ||
1872 | |||
1873 | /** | ||
1874 | * Select nodes content. Range will start and end inside this node. | ||
1875 | * | ||
1876 | * @param {CKEDITOR.dom.node} node | ||
1877 | */ | ||
1878 | selectNodeContents: function( node ) { | ||
1879 | this.setStart( node, 0 ); | ||
1880 | this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() ); | ||
1881 | }, | ||
1882 | |||
1883 | /** | ||
1884 | * Sets the start position of a range. | ||
1885 | * | ||
1886 | * @param {CKEDITOR.dom.node} startNode The node to start the range. | ||
1887 | * @param {Number} startOffset An integer greater than or equal to zero | ||
1888 | * representing the offset for the start of the range from the start | ||
1889 | * of `startNode`. | ||
1890 | */ | ||
1891 | setStart: function( startNode, startOffset ) { | ||
1892 | // W3C requires a check for the new position. If it is after the end | ||
1893 | // boundary, the range should be collapsed to the new start. It seams | ||
1894 | // we will not need this check for our use of this class so we can | ||
1895 | // ignore it for now. | ||
1896 | |||
1897 | // Fixing invalid range start inside dtd empty elements. | ||
1898 | if ( startNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ startNode.getName() ] ) | ||
1899 | startOffset = startNode.getIndex(), startNode = startNode.getParent(); | ||
1900 | |||
1901 | this._setStartContainer( startNode ); | ||
1902 | this.startOffset = startOffset; | ||
1903 | |||
1904 | if ( !this.endContainer ) { | ||
1905 | this._setEndContainer( startNode ); | ||
1906 | this.endOffset = startOffset; | ||
1907 | } | ||
1908 | |||
1909 | updateCollapsed( this ); | ||
1910 | }, | ||
1911 | |||
1912 | /** | ||
1913 | * Sets the end position of a Range. | ||
1914 | * | ||
1915 | * @param {CKEDITOR.dom.node} endNode The node to end the range. | ||
1916 | * @param {Number} endOffset An integer greater than or equal to zero | ||
1917 | * representing the offset for the end of the range from the start | ||
1918 | * of `endNode`. | ||
1919 | */ | ||
1920 | setEnd: function( endNode, endOffset ) { | ||
1921 | // W3C requires a check for the new position. If it is before the start | ||
1922 | // boundary, the range should be collapsed to the new end. It seams we | ||
1923 | // will not need this check for our use of this class so we can ignore | ||
1924 | // it for now. | ||
1925 | |||
1926 | // Fixing invalid range end inside dtd empty elements. | ||
1927 | if ( endNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ endNode.getName() ] ) | ||
1928 | endOffset = endNode.getIndex() + 1, endNode = endNode.getParent(); | ||
1929 | |||
1930 | this._setEndContainer( endNode ); | ||
1931 | this.endOffset = endOffset; | ||
1932 | |||
1933 | if ( !this.startContainer ) { | ||
1934 | this._setStartContainer( endNode ); | ||
1935 | this.startOffset = endOffset; | ||
1936 | } | ||
1937 | |||
1938 | updateCollapsed( this ); | ||
1939 | }, | ||
1940 | |||
1941 | /** | ||
1942 | * Sets start of this range after the specified node. | ||
1943 | * | ||
1944 | * // Range: <p>foo<b>bar</b>^</p> | ||
1945 | * range.setStartAfter( textFoo ); | ||
1946 | * // The range will be changed to: | ||
1947 | * // <p>foo[<b>bar</b>]</p> | ||
1948 | * | ||
1949 | * @param {CKEDITOR.dom.node} node | ||
1950 | */ | ||
1951 | setStartAfter: function( node ) { | ||
1952 | this.setStart( node.getParent(), node.getIndex() + 1 ); | ||
1953 | }, | ||
1954 | |||
1955 | /** | ||
1956 | * Sets start of this range after the specified node. | ||
1957 | * | ||
1958 | * // Range: <p>foo<b>bar</b>^</p> | ||
1959 | * range.setStartBefore( elB ); | ||
1960 | * // The range will be changed to: | ||
1961 | * // <p>foo[<b>bar</b>]</p> | ||
1962 | * | ||
1963 | * @param {CKEDITOR.dom.node} node | ||
1964 | */ | ||
1965 | setStartBefore: function( node ) { | ||
1966 | this.setStart( node.getParent(), node.getIndex() ); | ||
1967 | }, | ||
1968 | |||
1969 | /** | ||
1970 | * Sets end of this range after the specified node. | ||
1971 | * | ||
1972 | * // Range: <p>foo^<b>bar</b></p> | ||
1973 | * range.setEndAfter( elB ); | ||
1974 | * // The range will be changed to: | ||
1975 | * // <p>foo[<b>bar</b>]</p> | ||
1976 | * | ||
1977 | * @param {CKEDITOR.dom.node} node | ||
1978 | */ | ||
1979 | setEndAfter: function( node ) { | ||
1980 | this.setEnd( node.getParent(), node.getIndex() + 1 ); | ||
1981 | }, | ||
1982 | |||
1983 | /** | ||
1984 | * Sets end of this range before the specified node. | ||
1985 | * | ||
1986 | * // Range: <p>^foo<b>bar</b></p> | ||
1987 | * range.setStartAfter( textBar ); | ||
1988 | * // The range will be changed to: | ||
1989 | * // <p>[foo<b>]bar</b></p> | ||
1990 | * | ||
1991 | * @param {CKEDITOR.dom.node} node | ||
1992 | */ | ||
1993 | setEndBefore: function( node ) { | ||
1994 | this.setEnd( node.getParent(), node.getIndex() ); | ||
1995 | }, | ||
1996 | |||
1997 | /** | ||
1998 | * Moves the start of this range to given position according to specified node. | ||
1999 | * | ||
2000 | * // HTML: <p>Foo <b>bar</b>^</p> | ||
2001 | * range.setStartAt( elB, CKEDITOR.POSITION_AFTER_START ); | ||
2002 | * // The range will be changed to: | ||
2003 | * // <p>Foo <b>[bar</b>]</p> | ||
2004 | * | ||
2005 | * See also {@link #setEndAt} and {@link #moveToPosition}. | ||
2006 | * | ||
2007 | * @param {CKEDITOR.dom.node} node The node according to which position will be set. | ||
2008 | * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START}, | ||
2009 | * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END}, | ||
2010 | * {@link CKEDITOR#POSITION_AFTER_END}. | ||
2011 | */ | ||
2012 | setStartAt: function( node, position ) { | ||
2013 | switch ( position ) { | ||
2014 | case CKEDITOR.POSITION_AFTER_START: | ||
2015 | this.setStart( node, 0 ); | ||
2016 | break; | ||
2017 | |||
2018 | case CKEDITOR.POSITION_BEFORE_END: | ||
2019 | if ( node.type == CKEDITOR.NODE_TEXT ) | ||
2020 | this.setStart( node, node.getLength() ); | ||
2021 | else | ||
2022 | this.setStart( node, node.getChildCount() ); | ||
2023 | break; | ||
2024 | |||
2025 | case CKEDITOR.POSITION_BEFORE_START: | ||
2026 | this.setStartBefore( node ); | ||
2027 | break; | ||
2028 | |||
2029 | case CKEDITOR.POSITION_AFTER_END: | ||
2030 | this.setStartAfter( node ); | ||
2031 | } | ||
2032 | |||
2033 | updateCollapsed( this ); | ||
2034 | }, | ||
2035 | |||
2036 | /** | ||
2037 | * Moves the end of this range to given position according to specified node. | ||
2038 | * | ||
2039 | * // HTML: <p>^Foo <b>bar</b></p> | ||
2040 | * range.setEndAt( textBar, CKEDITOR.POSITION_BEFORE_START ); | ||
2041 | * // The range will be changed to: | ||
2042 | * // <p>[Foo <b>]bar</b></p> | ||
2043 | * | ||
2044 | * See also {@link #setStartAt} and {@link #moveToPosition}. | ||
2045 | * | ||
2046 | * @param {CKEDITOR.dom.node} node The node according to which position will be set. | ||
2047 | * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START}, | ||
2048 | * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END}, | ||
2049 | * {@link CKEDITOR#POSITION_AFTER_END}. | ||
2050 | */ | ||
2051 | setEndAt: function( node, position ) { | ||
2052 | switch ( position ) { | ||
2053 | case CKEDITOR.POSITION_AFTER_START: | ||
2054 | this.setEnd( node, 0 ); | ||
2055 | break; | ||
2056 | |||
2057 | case CKEDITOR.POSITION_BEFORE_END: | ||
2058 | if ( node.type == CKEDITOR.NODE_TEXT ) | ||
2059 | this.setEnd( node, node.getLength() ); | ||
2060 | else | ||
2061 | this.setEnd( node, node.getChildCount() ); | ||
2062 | break; | ||
2063 | |||
2064 | case CKEDITOR.POSITION_BEFORE_START: | ||
2065 | this.setEndBefore( node ); | ||
2066 | break; | ||
2067 | |||
2068 | case CKEDITOR.POSITION_AFTER_END: | ||
2069 | this.setEndAfter( node ); | ||
2070 | } | ||
2071 | |||
2072 | updateCollapsed( this ); | ||
2073 | }, | ||
2074 | |||
2075 | /** | ||
2076 | * Wraps inline content found around the range's start or end boundary | ||
2077 | * with a block element. | ||
2078 | * | ||
2079 | * // Assuming the following range: | ||
2080 | * // <h1>foo</h1>ba^r<br />bom<p>foo</p> | ||
2081 | * // The result of executing: | ||
2082 | * range.fixBlock( true, 'p' ); | ||
2083 | * // will be: | ||
2084 | * // <h1>foo</h1><p>ba^r<br />bom</p><p>foo</p> | ||
2085 | * | ||
2086 | * Non-collapsed range: | ||
2087 | * | ||
2088 | * // Assuming the following range: | ||
2089 | * // ba[r<p>foo</p>bo]m | ||
2090 | * // The result of executing: | ||
2091 | * range.fixBlock( false, 'p' ); | ||
2092 | * // will be: | ||
2093 | * // ba[r<p>foo</p><p>bo]m</p> | ||
2094 | * | ||
2095 | * @param {Boolean} isStart Whether the start or end boundary of a range should be checked. | ||
2096 | * @param {String} blockTag The name of a block element in which content will be wrapped. | ||
2097 | * For example: `'p'`. | ||
2098 | * @returns {CKEDITOR.dom.element} Created block wrapper. | ||
2099 | */ | ||
2100 | fixBlock: function( isStart, blockTag ) { | ||
2101 | var bookmark = this.createBookmark(), | ||
2102 | fixedBlock = this.document.createElement( blockTag ); | ||
2103 | |||
2104 | this.collapse( isStart ); | ||
2105 | |||
2106 | this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS ); | ||
2107 | |||
2108 | this.extractContents().appendTo( fixedBlock ); | ||
2109 | fixedBlock.trim(); | ||
2110 | |||
2111 | this.insertNode( fixedBlock ); | ||
2112 | |||
2113 | // Bogus <br> could already exist in the range's container before fixBlock() was called. In such case it was | ||
2114 | // extracted and appended to the fixBlock. However, we are not sure that it's at the end of | ||
2115 | // the fixedBlock, because of FF's terrible bug. When creating a bookmark in an empty editable | ||
2116 | // FF moves the bogus <br> before that bookmark (<editable><br /><bm />[]</editable>). | ||
2117 | // So even if the initial range was placed before the bogus <br>, after creating the bookmark it | ||
2118 | // is placed before the bookmark. | ||
2119 | // Fortunately, getBogus() is able to skip the bookmark so it finds the bogus <br> in this case. | ||
2120 | // We remove incorrectly placed one and add a brand new one. (#13001) | ||
2121 | var bogus = fixedBlock.getBogus(); | ||
2122 | if ( bogus ) { | ||
2123 | bogus.remove(); | ||
2124 | } | ||
2125 | fixedBlock.appendBogus(); | ||
2126 | |||
2127 | this.moveToBookmark( bookmark ); | ||
2128 | |||
2129 | return fixedBlock; | ||
2130 | }, | ||
2131 | |||
2132 | /** | ||
2133 | * @todo | ||
2134 | * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result blocks. | ||
2135 | */ | ||
2136 | splitBlock: function( blockTag, cloneId ) { | ||
2137 | var startPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ), | ||
2138 | endPath = new CKEDITOR.dom.elementPath( this.endContainer, this.root ); | ||
2139 | |||
2140 | var startBlockLimit = startPath.blockLimit, | ||
2141 | endBlockLimit = endPath.blockLimit; | ||
2142 | |||
2143 | var startBlock = startPath.block, | ||
2144 | endBlock = endPath.block; | ||
2145 | |||
2146 | var elementPath = null; | ||
2147 | // Do nothing if the boundaries are in different block limits. | ||
2148 | if ( !startBlockLimit.equals( endBlockLimit ) ) | ||
2149 | return null; | ||
2150 | |||
2151 | // Get or fix current blocks. | ||
2152 | if ( blockTag != 'br' ) { | ||
2153 | if ( !startBlock ) { | ||
2154 | startBlock = this.fixBlock( true, blockTag ); | ||
2155 | endBlock = new CKEDITOR.dom.elementPath( this.endContainer, this.root ).block; | ||
2156 | } | ||
2157 | |||
2158 | if ( !endBlock ) | ||
2159 | endBlock = this.fixBlock( false, blockTag ); | ||
2160 | } | ||
2161 | |||
2162 | // Get the range position. | ||
2163 | var isStartOfBlock = startBlock && this.checkStartOfBlock(), | ||
2164 | isEndOfBlock = endBlock && this.checkEndOfBlock(); | ||
2165 | |||
2166 | // Delete the current contents. | ||
2167 | // TODO: Why is 2.x doing CheckIsEmpty()? | ||
2168 | this.deleteContents(); | ||
2169 | |||
2170 | if ( startBlock && startBlock.equals( endBlock ) ) { | ||
2171 | if ( isEndOfBlock ) { | ||
2172 | elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ); | ||
2173 | this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END ); | ||
2174 | endBlock = null; | ||
2175 | } else if ( isStartOfBlock ) { | ||
2176 | elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ); | ||
2177 | this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START ); | ||
2178 | startBlock = null; | ||
2179 | } else { | ||
2180 | endBlock = this.splitElement( startBlock, cloneId || false ); | ||
2181 | |||
2182 | // In Gecko, the last child node must be a bogus <br>. | ||
2183 | // Note: bogus <br> added under <ul> or <ol> would cause | ||
2184 | // lists to be incorrectly rendered. | ||
2185 | if ( !startBlock.is( 'ul', 'ol' ) ) | ||
2186 | startBlock.appendBogus(); | ||
2187 | } | ||
2188 | } | ||
2189 | |||
2190 | return { | ||
2191 | previousBlock: startBlock, | ||
2192 | nextBlock: endBlock, | ||
2193 | wasStartOfBlock: isStartOfBlock, | ||
2194 | wasEndOfBlock: isEndOfBlock, | ||
2195 | elementPath: elementPath | ||
2196 | }; | ||
2197 | }, | ||
2198 | |||
2199 | /** | ||
2200 | * Branch the specified element from the collapsed range position and | ||
2201 | * place the caret between the two result branches. | ||
2202 | * | ||
2203 | * **Note:** The range must be collapsed and been enclosed by this element. | ||
2204 | * | ||
2205 | * @param {CKEDITOR.dom.element} element | ||
2206 | * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result elements. | ||
2207 | * @returns {CKEDITOR.dom.element} Root element of the new branch after the split. | ||
2208 | */ | ||
2209 | splitElement: function( toSplit, cloneId ) { | ||
2210 | if ( !this.collapsed ) | ||
2211 | return null; | ||
2212 | |||
2213 | // Extract the contents of the block from the selection point to the end | ||
2214 | // of its contents. | ||
2215 | this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END ); | ||
2216 | var documentFragment = this.extractContents( false, cloneId || false ); | ||
2217 | |||
2218 | // Duplicate the element after it. | ||
2219 | var clone = toSplit.clone( false, cloneId || false ); | ||
2220 | |||
2221 | // Place the extracted contents into the duplicated element. | ||
2222 | documentFragment.appendTo( clone ); | ||
2223 | clone.insertAfter( toSplit ); | ||
2224 | this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END ); | ||
2225 | return clone; | ||
2226 | }, | ||
2227 | |||
2228 | /** | ||
2229 | * Recursively remove any empty path blocks at the range boundary. | ||
2230 | * | ||
2231 | * @method | ||
2232 | * @param {Boolean} atEnd Removal to perform at the end boundary, | ||
2233 | * otherwise to perform at the start. | ||
2234 | */ | ||
2235 | removeEmptyBlocksAtEnd: ( function() { | ||
2236 | |||
2237 | var whitespace = CKEDITOR.dom.walker.whitespaces(), | ||
2238 | bookmark = CKEDITOR.dom.walker.bookmark( false ); | ||
2239 | |||
2240 | function childEval( parent ) { | ||
2241 | return function( node ) { | ||
2242 | // Whitespace, bookmarks, empty inlines. | ||
2243 | if ( whitespace( node ) || bookmark( node ) || | ||
2244 | node.type == CKEDITOR.NODE_ELEMENT && | ||
2245 | node.isEmptyInlineRemoveable() ) { | ||
2246 | return false; | ||
2247 | } else if ( parent.is( 'table' ) && node.is( 'caption' ) ) { | ||
2248 | return false; | ||
2249 | } | ||
2250 | |||
2251 | return true; | ||
2252 | }; | ||
2253 | } | ||
2254 | |||
2255 | return function( atEnd ) { | ||
2256 | |||
2257 | var bm = this.createBookmark(); | ||
2258 | var path = this[ atEnd ? 'endPath' : 'startPath' ](); | ||
2259 | var block = path.block || path.blockLimit, parent; | ||
2260 | |||
2261 | // Remove any childless block, including list and table. | ||
2262 | while ( block && !block.equals( path.root ) && | ||
2263 | !block.getFirst( childEval( block ) ) ) { | ||
2264 | parent = block.getParent(); | ||
2265 | this[ atEnd ? 'setEndAt' : 'setStartAt' ]( block, CKEDITOR.POSITION_AFTER_END ); | ||
2266 | block.remove( 1 ); | ||
2267 | block = parent; | ||
2268 | } | ||
2269 | |||
2270 | this.moveToBookmark( bm ); | ||
2271 | }; | ||
2272 | |||
2273 | } )(), | ||
2274 | |||
2275 | /** | ||
2276 | * Gets {@link CKEDITOR.dom.elementPath} for the {@link #startContainer}. | ||
2277 | * | ||
2278 | * @returns {CKEDITOR.dom.elementPath} | ||
2279 | */ | ||
2280 | startPath: function() { | ||
2281 | return new CKEDITOR.dom.elementPath( this.startContainer, this.root ); | ||
2282 | }, | ||
2283 | |||
2284 | /** | ||
2285 | * Gets {@link CKEDITOR.dom.elementPath} for the {@link #endContainer}. | ||
2286 | * | ||
2287 | * @returns {CKEDITOR.dom.elementPath} | ||
2288 | */ | ||
2289 | endPath: function() { | ||
2290 | return new CKEDITOR.dom.elementPath( this.endContainer, this.root ); | ||
2291 | }, | ||
2292 | |||
2293 | /** | ||
2294 | * Check whether a range boundary is at the inner boundary of a given | ||
2295 | * element. | ||
2296 | * | ||
2297 | * @param {CKEDITOR.dom.element} element The target element to check. | ||
2298 | * @param {Number} checkType The boundary to check for both the range | ||
2299 | * and the element. It can be {@link CKEDITOR#START} or {@link CKEDITOR#END}. | ||
2300 | * @returns {Boolean} `true` if the range boundary is at the inner | ||
2301 | * boundary of the element. | ||
2302 | */ | ||
2303 | checkBoundaryOfElement: function( element, checkType ) { | ||
2304 | var checkStart = ( checkType == CKEDITOR.START ); | ||
2305 | |||
2306 | // Create a copy of this range, so we can manipulate it for our checks. | ||
2307 | var walkerRange = this.clone(); | ||
2308 | |||
2309 | // Collapse the range at the proper size. | ||
2310 | walkerRange.collapse( checkStart ); | ||
2311 | |||
2312 | // Expand the range to element boundary. | ||
2313 | walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END ); | ||
2314 | |||
2315 | // Create the walker, which will check if we have anything useful | ||
2316 | // in the range. | ||
2317 | var walker = new CKEDITOR.dom.walker( walkerRange ); | ||
2318 | walker.evaluator = elementBoundaryEval( checkStart ); | ||
2319 | |||
2320 | return walker[ checkStart ? 'checkBackward' : 'checkForward' ](); | ||
2321 | }, | ||
2322 | |||
2323 | /** | ||
2324 | * **Note:** Calls to this function may produce changes to the DOM. The range may | ||
2325 | * be updated to reflect such changes. | ||
2326 | * | ||
2327 | * @returns {Boolean} | ||
2328 | * @todo | ||
2329 | */ | ||
2330 | checkStartOfBlock: function() { | ||
2331 | var startContainer = this.startContainer, | ||
2332 | startOffset = this.startOffset; | ||
2333 | |||
2334 | // [IE] Special handling for range start in text with a leading NBSP, | ||
2335 | // we it to be isolated, for bogus check. | ||
2336 | if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) { | ||
2337 | var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) ); | ||
2338 | if ( nbspRegExp.test( textBefore ) ) | ||
2339 | this.trim( 0, 1 ); | ||
2340 | } | ||
2341 | |||
2342 | // Antecipate the trim() call here, so the walker will not make | ||
2343 | // changes to the DOM, which would not get reflected into this | ||
2344 | // range otherwise. | ||
2345 | this.trim(); | ||
2346 | |||
2347 | // We need to grab the block element holding the start boundary, so | ||
2348 | // let's use an element path for it. | ||
2349 | var path = new CKEDITOR.dom.elementPath( this.startContainer, this.root ); | ||
2350 | |||
2351 | // Creates a range starting at the block start until the range start. | ||
2352 | var walkerRange = this.clone(); | ||
2353 | walkerRange.collapse( true ); | ||
2354 | walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START ); | ||
2355 | |||
2356 | var walker = new CKEDITOR.dom.walker( walkerRange ); | ||
2357 | walker.evaluator = getCheckStartEndBlockEvalFunction(); | ||
2358 | |||
2359 | return walker.checkBackward(); | ||
2360 | }, | ||
2361 | |||
2362 | /** | ||
2363 | * **Note:** Calls to this function may produce changes to the DOM. The range may | ||
2364 | * be updated to reflect such changes. | ||
2365 | * | ||
2366 | * @returns {Boolean} | ||
2367 | * @todo | ||
2368 | */ | ||
2369 | checkEndOfBlock: function() { | ||
2370 | var endContainer = this.endContainer, | ||
2371 | endOffset = this.endOffset; | ||
2372 | |||
2373 | // [IE] Special handling for range end in text with a following NBSP, | ||
2374 | // we it to be isolated, for bogus check. | ||
2375 | if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT ) { | ||
2376 | var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) ); | ||
2377 | if ( nbspRegExp.test( textAfter ) ) | ||
2378 | this.trim( 1, 0 ); | ||
2379 | } | ||
2380 | |||
2381 | // Antecipate the trim() call here, so the walker will not make | ||
2382 | // changes to the DOM, which would not get reflected into this | ||
2383 | // range otherwise. | ||
2384 | this.trim(); | ||
2385 | |||
2386 | // We need to grab the block element holding the start boundary, so | ||
2387 | // let's use an element path for it. | ||
2388 | var path = new CKEDITOR.dom.elementPath( this.endContainer, this.root ); | ||
2389 | |||
2390 | // Creates a range starting at the block start until the range start. | ||
2391 | var walkerRange = this.clone(); | ||
2392 | walkerRange.collapse( false ); | ||
2393 | walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END ); | ||
2394 | |||
2395 | var walker = new CKEDITOR.dom.walker( walkerRange ); | ||
2396 | walker.evaluator = getCheckStartEndBlockEvalFunction(); | ||
2397 | |||
2398 | return walker.checkForward(); | ||
2399 | }, | ||
2400 | |||
2401 | /** | ||
2402 | * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start. | ||
2403 | * | ||
2404 | * @param {Function} evaluator Function used as the walker's evaluator. | ||
2405 | * @param {Function} [guard] Function used as the walker's guard. | ||
2406 | * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited, | ||
2407 | * default to the root editable if not defined. | ||
2408 | * @returns {CKEDITOR.dom.element/null} The returned node from the traversal. | ||
2409 | */ | ||
2410 | getPreviousNode: function( evaluator, guard, boundary ) { | ||
2411 | var walkerRange = this.clone(); | ||
2412 | walkerRange.collapse( 1 ); | ||
2413 | walkerRange.setStartAt( boundary || this.root, CKEDITOR.POSITION_AFTER_START ); | ||
2414 | |||
2415 | var walker = new CKEDITOR.dom.walker( walkerRange ); | ||
2416 | walker.evaluator = evaluator; | ||
2417 | walker.guard = guard; | ||
2418 | return walker.previous(); | ||
2419 | }, | ||
2420 | |||
2421 | /** | ||
2422 | * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start. | ||
2423 | * | ||
2424 | * @param {Function} evaluator Function used as the walker's evaluator. | ||
2425 | * @param {Function} [guard] Function used as the walker's guard. | ||
2426 | * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited, | ||
2427 | * default to the root editable if not defined. | ||
2428 | * @returns {CKEDITOR.dom.element/null} The returned node from the traversal. | ||
2429 | */ | ||
2430 | getNextNode: function( evaluator, guard, boundary ) { | ||
2431 | var walkerRange = this.clone(); | ||
2432 | walkerRange.collapse(); | ||
2433 | walkerRange.setEndAt( boundary || this.root, CKEDITOR.POSITION_BEFORE_END ); | ||
2434 | |||
2435 | var walker = new CKEDITOR.dom.walker( walkerRange ); | ||
2436 | walker.evaluator = evaluator; | ||
2437 | walker.guard = guard; | ||
2438 | return walker.next(); | ||
2439 | }, | ||
2440 | |||
2441 | /** | ||
2442 | * Check if elements at which the range boundaries anchor are read-only, | ||
2443 | * with respect to `contenteditable` attribute. | ||
2444 | * | ||
2445 | * @returns {Boolean} | ||
2446 | */ | ||
2447 | checkReadOnly: ( function() { | ||
2448 | function checkNodesEditable( node, anotherEnd ) { | ||
2449 | while ( node ) { | ||
2450 | if ( node.type == CKEDITOR.NODE_ELEMENT ) { | ||
2451 | if ( node.getAttribute( 'contentEditable' ) == 'false' && !node.data( 'cke-editable' ) ) | ||
2452 | return 0; | ||
2453 | |||
2454 | // Range enclosed entirely in an editable element. | ||
2455 | else if ( node.is( 'html' ) || node.getAttribute( 'contentEditable' ) == 'true' && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) ) | ||
2456 | break; | ||
2457 | |||
2458 | } | ||
2459 | node = node.getParent(); | ||
2460 | } | ||
2461 | |||
2462 | return 1; | ||
2463 | } | ||
2464 | |||
2465 | return function() { | ||
2466 | var startNode = this.startContainer, | ||
2467 | endNode = this.endContainer; | ||
2468 | |||
2469 | // Check if elements path at both boundaries are editable. | ||
2470 | return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) ); | ||
2471 | }; | ||
2472 | } )(), | ||
2473 | |||
2474 | /** | ||
2475 | * Moves the range boundaries to the first/end editing point inside an | ||
2476 | * element. | ||
2477 | * | ||
2478 | * For example, in an element tree like | ||
2479 | * `<p><b><i></i></b> Text</p>`, the start editing point is | ||
2480 | * `<p><b><i>^</i></b> Text</p>` (inside `<i>`). | ||
2481 | * | ||
2482 | * @param {CKEDITOR.dom.element} el The element into which look for the | ||
2483 | * editing spot. | ||
2484 | * @param {Boolean} isMoveToEnd Whether move to the end editable position. | ||
2485 | * @returns {Boolean} Whether range was moved. | ||
2486 | */ | ||
2487 | moveToElementEditablePosition: function( el, isMoveToEnd ) { | ||
2488 | |||
2489 | function nextDFS( node, childOnly ) { | ||
2490 | var next; | ||
2491 | |||
2492 | if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) ) | ||
2493 | next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( notIgnoredEval ); | ||
2494 | |||
2495 | if ( !childOnly && !next ) | ||
2496 | next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( notIgnoredEval ); | ||
2497 | |||
2498 | return next; | ||
2499 | } | ||
2500 | |||
2501 | // Handle non-editable element e.g. HR. | ||
2502 | if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) { | ||
2503 | this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ); | ||
2504 | return true; | ||
2505 | } | ||
2506 | |||
2507 | var found = 0; | ||
2508 | |||
2509 | while ( el ) { | ||
2510 | // Stop immediately if we've found a text node. | ||
2511 | if ( el.type == CKEDITOR.NODE_TEXT ) { | ||
2512 | // Put cursor before block filler. | ||
2513 | if ( isMoveToEnd && this.endContainer && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) ) | ||
2514 | this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START ); | ||
2515 | else | ||
2516 | this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ); | ||
2517 | found = 1; | ||
2518 | break; | ||
2519 | } | ||
2520 | |||
2521 | // If an editable element is found, move inside it, but not stop the searching. | ||
2522 | if ( el.type == CKEDITOR.NODE_ELEMENT ) { | ||
2523 | if ( el.isEditable() ) { | ||
2524 | this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_START ); | ||
2525 | found = 1; | ||
2526 | } | ||
2527 | // Put cursor before padding block br. | ||
2528 | else if ( isMoveToEnd && el.is( 'br' ) && this.endContainer && this.checkEndOfBlock() ) | ||
2529 | this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START ); | ||
2530 | // Special case - non-editable block. Select entire element, because it does not make sense | ||
2531 | // to place collapsed selection next to it, because browsers can't handle that. | ||
2532 | else if ( el.getAttribute( 'contenteditable' ) == 'false' && el.is( CKEDITOR.dtd.$block ) ) { | ||
2533 | this.setStartBefore( el ); | ||
2534 | this.setEndAfter( el ); | ||
2535 | return true; | ||
2536 | } | ||
2537 | } | ||
2538 | |||
2539 | el = nextDFS( el, found ); | ||
2540 | } | ||
2541 | |||
2542 | return !!found; | ||
2543 | }, | ||
2544 | |||
2545 | /** | ||
2546 | * Moves the range boundaries to the closest editing point after/before an | ||
2547 | * element or the current range position (depends on whether the element was specified). | ||
2548 | * | ||
2549 | * For example, if the start element has `id="start"`, | ||
2550 | * `<p><b>foo</b><span id="start">start</start></p>`, the closest previous editing point is | ||
2551 | * `<p><b>foo</b>^<span id="start">start</start></p>` (between `<b>` and `<span>`). | ||
2552 | * | ||
2553 | * See also: {@link #moveToElementEditablePosition}. | ||
2554 | * | ||
2555 | * @since 4.3 | ||
2556 | * @param {CKEDITOR.dom.element} [element] The starting element. If not specified, the current range | ||
2557 | * position will be used. | ||
2558 | * @param {Boolean} [isMoveForward] Whether move to the end of editable. Otherwise, look back. | ||
2559 | * @returns {Boolean} Whether the range was moved. | ||
2560 | */ | ||
2561 | moveToClosestEditablePosition: function( element, isMoveForward ) { | ||
2562 | // We don't want to modify original range if there's no editable position. | ||
2563 | var range, | ||
2564 | found = 0, | ||
2565 | sibling, | ||
2566 | isElement, | ||
2567 | positions = [ CKEDITOR.POSITION_AFTER_END, CKEDITOR.POSITION_BEFORE_START ]; | ||
2568 | |||
2569 | if ( element ) { | ||
2570 | // Set collapsed range at one of ends of element. | ||
2571 | // Can't clone this range, because this range might not be yet positioned (no containers => errors). | ||
2572 | range = new CKEDITOR.dom.range( this.root ); | ||
2573 | range.moveToPosition( element, positions[ isMoveForward ? 0 : 1 ] ); | ||
2574 | } else { | ||
2575 | range = this.clone(); | ||
2576 | } | ||
2577 | |||
2578 | // Start element isn't a block, so we can automatically place range | ||
2579 | // next to it. | ||
2580 | if ( element && !element.is( CKEDITOR.dtd.$block ) ) | ||
2581 | found = 1; | ||
2582 | else { | ||
2583 | // Look for first node that fulfills eval function and place range next to it. | ||
2584 | sibling = range[ isMoveForward ? 'getNextEditableNode' : 'getPreviousEditableNode' ](); | ||
2585 | if ( sibling ) { | ||
2586 | found = 1; | ||
2587 | isElement = sibling.type == CKEDITOR.NODE_ELEMENT; | ||
2588 | |||
2589 | // Special case - eval accepts block element only if it's a non-editable block, | ||
2590 | // which we want to select, not place collapsed selection next to it (which browsers | ||
2591 | // can't handle). | ||
2592 | if ( isElement && sibling.is( CKEDITOR.dtd.$block ) && sibling.getAttribute( 'contenteditable' ) == 'false' ) { | ||
2593 | range.setStartAt( sibling, CKEDITOR.POSITION_BEFORE_START ); | ||
2594 | range.setEndAt( sibling, CKEDITOR.POSITION_AFTER_END ); | ||
2595 | } | ||
2596 | // Handle empty blocks which can be selection containers on old IEs. | ||
2597 | else if ( !CKEDITOR.env.needsBrFiller && isElement && sibling.is( CKEDITOR.dom.walker.validEmptyBlockContainers ) ) { | ||
2598 | range.setEnd( sibling, 0 ); | ||
2599 | range.collapse(); | ||
2600 | } else { | ||
2601 | range.moveToPosition( sibling, positions[ isMoveForward ? 1 : 0 ] ); | ||
2602 | } | ||
2603 | } | ||
2604 | } | ||
2605 | |||
2606 | if ( found ) | ||
2607 | this.moveToRange( range ); | ||
2608 | |||
2609 | return !!found; | ||
2610 | }, | ||
2611 | |||
2612 | /** | ||
2613 | * See {@link #moveToElementEditablePosition}. | ||
2614 | * | ||
2615 | * @returns {Boolean} Whether range was moved. | ||
2616 | */ | ||
2617 | moveToElementEditStart: function( target ) { | ||
2618 | return this.moveToElementEditablePosition( target ); | ||
2619 | }, | ||
2620 | |||
2621 | /** | ||
2622 | * See {@link #moveToElementEditablePosition}. | ||
2623 | * | ||
2624 | * @returns {Boolean} Whether range was moved. | ||
2625 | */ | ||
2626 | moveToElementEditEnd: function( target ) { | ||
2627 | return this.moveToElementEditablePosition( target, true ); | ||
2628 | }, | ||
2629 | |||
2630 | /** | ||
2631 | * Get the single node enclosed within the range if there's one. | ||
2632 | * | ||
2633 | * @returns {CKEDITOR.dom.node} | ||
2634 | */ | ||
2635 | getEnclosedNode: function() { | ||
2636 | var walkerRange = this.clone(); | ||
2637 | |||
2638 | // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780) | ||
2639 | walkerRange.optimize(); | ||
2640 | if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT ) | ||
2641 | return null; | ||
2642 | |||
2643 | var walker = new CKEDITOR.dom.walker( walkerRange ), | ||
2644 | isNotBookmarks = CKEDITOR.dom.walker.bookmark( false, true ), | ||
2645 | isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ); | ||
2646 | |||
2647 | walker.evaluator = function( node ) { | ||
2648 | return isNotWhitespaces( node ) && isNotBookmarks( node ); | ||
2649 | }; | ||
2650 | var node = walker.next(); | ||
2651 | walker.reset(); | ||
2652 | return node && node.equals( walker.previous() ) ? node : null; | ||
2653 | }, | ||
2654 | |||
2655 | /** | ||
2656 | * Get the node adjacent to the range start or {@link #startContainer}. | ||
2657 | * | ||
2658 | * @returns {CKEDITOR.dom.node} | ||
2659 | */ | ||
2660 | getTouchedStartNode: function() { | ||
2661 | var container = this.startContainer; | ||
2662 | |||
2663 | if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) | ||
2664 | return container; | ||
2665 | |||
2666 | return container.getChild( this.startOffset ) || container; | ||
2667 | }, | ||
2668 | |||
2669 | /** | ||
2670 | * Get the node adjacent to the range end or {@link #endContainer}. | ||
2671 | * | ||
2672 | * @returns {CKEDITOR.dom.node} | ||
2673 | */ | ||
2674 | getTouchedEndNode: function() { | ||
2675 | var container = this.endContainer; | ||
2676 | |||
2677 | if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) | ||
2678 | return container; | ||
2679 | |||
2680 | return container.getChild( this.endOffset - 1 ) || container; | ||
2681 | }, | ||
2682 | |||
2683 | /** | ||
2684 | * Gets next node which can be a container of a selection. | ||
2685 | * This methods mimics a behavior of right/left arrow keys in case of | ||
2686 | * collapsed selection. It does not return an exact position (with offset) though, | ||
2687 | * but just a selection's container. | ||
2688 | * | ||
2689 | * Note: use this method on a collapsed range. | ||
2690 | * | ||
2691 | * @since 4.3 | ||
2692 | * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text} | ||
2693 | */ | ||
2694 | getNextEditableNode: getNextEditableNode(), | ||
2695 | |||
2696 | /** | ||
2697 | * See {@link #getNextEditableNode}. | ||
2698 | * | ||
2699 | * @since 4.3 | ||
2700 | * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text} | ||
2701 | */ | ||
2702 | getPreviousEditableNode: getNextEditableNode( 1 ), | ||
2703 | |||
2704 | /** | ||
2705 | * Scrolls the start of current range into view. | ||
2706 | */ | ||
2707 | scrollIntoView: function() { | ||
2708 | |||
2709 | // The reference element contains a zero-width space to avoid | ||
2710 | // a premature removal. The view is to be scrolled with respect | ||
2711 | // to this element. | ||
2712 | var reference = new CKEDITOR.dom.element.createFromHtml( '<span> </span>', this.document ), | ||
2713 | afterCaretNode, startContainerText, isStartText; | ||
2714 | |||
2715 | var range = this.clone(); | ||
2716 | |||
2717 | // Work with the range to obtain a proper caret position. | ||
2718 | range.optimize(); | ||
2719 | |||
2720 | // Currently in a text node, so we need to split it into two | ||
2721 | // halves and put the reference between. | ||
2722 | if ( isStartText = range.startContainer.type == CKEDITOR.NODE_TEXT ) { | ||
2723 | // Keep the original content. It will be restored. | ||
2724 | startContainerText = range.startContainer.getText(); | ||
2725 | |||
2726 | // Split the startContainer at the this position. | ||
2727 | afterCaretNode = range.startContainer.split( range.startOffset ); | ||
2728 | |||
2729 | // Insert the reference between two text nodes. | ||
2730 | reference.insertAfter( range.startContainer ); | ||
2731 | } | ||
2732 | |||
2733 | // If not in a text node, simply insert the reference into the range. | ||
2734 | else { | ||
2735 | range.insertNode( reference ); | ||
2736 | } | ||
2737 | |||
2738 | // Scroll with respect to the reference element. | ||
2739 | reference.scrollIntoView(); | ||
2740 | |||
2741 | // Get rid of split parts if "in a text node" case. | ||
2742 | // Revert the original text of the startContainer. | ||
2743 | if ( isStartText ) { | ||
2744 | range.startContainer.setText( startContainerText ); | ||
2745 | afterCaretNode.remove(); | ||
2746 | } | ||
2747 | |||
2748 | // Get rid of the reference node. It is no longer necessary. | ||
2749 | reference.remove(); | ||
2750 | }, | ||
2751 | |||
2752 | /** | ||
2753 | * Setter for the {@link #startContainer}. | ||
2754 | * | ||
2755 | * @since 4.4.6 | ||
2756 | * @private | ||
2757 | * @param {CKEDITOR.dom.element} startContainer | ||
2758 | */ | ||
2759 | _setStartContainer: function( startContainer ) { | ||
2760 | // %REMOVE_START% | ||
2761 | var isRootAscendantOrSelf = this.root.equals( startContainer ) || this.root.contains( startContainer ); | ||
2762 | |||
2763 | if ( !isRootAscendantOrSelf ) { | ||
2764 | CKEDITOR.warn( 'range-startcontainer', { startContainer: startContainer, root: this.root } ); | ||
2765 | } | ||
2766 | // %REMOVE_END% | ||
2767 | this.startContainer = startContainer; | ||
2768 | }, | ||
2769 | |||
2770 | /** | ||
2771 | * Setter for the {@link #endContainer}. | ||
2772 | * | ||
2773 | * @since 4.4.6 | ||
2774 | * @private | ||
2775 | * @param {CKEDITOR.dom.element} endContainer | ||
2776 | */ | ||
2777 | _setEndContainer: function( endContainer ) { | ||
2778 | // %REMOVE_START% | ||
2779 | var isRootAscendantOrSelf = this.root.equals( endContainer ) || this.root.contains( endContainer ); | ||
2780 | |||
2781 | if ( !isRootAscendantOrSelf ) { | ||
2782 | CKEDITOR.warn( 'range-endcontainer', { endContainer: endContainer, root: this.root } ); | ||
2783 | } | ||
2784 | // %REMOVE_END% | ||
2785 | this.endContainer = endContainer; | ||
2786 | } | ||
2787 | }; | ||
2788 | |||
2789 | |||
2790 | } )(); | ||
2791 | |||
2792 | /** | ||
2793 | * Indicates a position after start of a node. | ||
2794 | * | ||
2795 | * // When used according to an element: | ||
2796 | * // <element>^contents</element> | ||
2797 | * | ||
2798 | * // When used according to a text node: | ||
2799 | * // "^text" (range is anchored in the text node) | ||
2800 | * | ||
2801 | * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition}, | ||
2802 | * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}. | ||
2803 | * | ||
2804 | * @readonly | ||
2805 | * @member CKEDITOR | ||
2806 | * @property {Number} [=1] | ||
2807 | */ | ||
2808 | CKEDITOR.POSITION_AFTER_START = 1; | ||
2809 | |||
2810 | /** | ||
2811 | * Indicates a position before end of a node. | ||
2812 | * | ||
2813 | * // When used according to an element: | ||
2814 | * // <element>contents^</element> | ||
2815 | * | ||
2816 | * // When used according to a text node: | ||
2817 | * // "text^" (range is anchored in the text node) | ||
2818 | * | ||
2819 | * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition}, | ||
2820 | * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}. | ||
2821 | * | ||
2822 | * @readonly | ||
2823 | * @member CKEDITOR | ||
2824 | * @property {Number} [=2] | ||
2825 | */ | ||
2826 | CKEDITOR.POSITION_BEFORE_END = 2; | ||
2827 | |||
2828 | /** | ||
2829 | * Indicates a position before start of a node. | ||
2830 | * | ||
2831 | * // When used according to an element: | ||
2832 | * // ^<element>contents</element> (range is anchored in element's parent) | ||
2833 | * | ||
2834 | * // When used according to a text node: | ||
2835 | * // ^"text" (range is anchored in text node's parent) | ||
2836 | * | ||
2837 | * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition}, | ||
2838 | * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}. | ||
2839 | * | ||
2840 | * @readonly | ||
2841 | * @member CKEDITOR | ||
2842 | * @property {Number} [=3] | ||
2843 | */ | ||
2844 | CKEDITOR.POSITION_BEFORE_START = 3; | ||
2845 | |||
2846 | /** | ||
2847 | * Indicates a position after end of a node. | ||
2848 | * | ||
2849 | * // When used according to an element: | ||
2850 | * // <element>contents</element>^ (range is anchored in element's parent) | ||
2851 | * | ||
2852 | * // When used according to a text node: | ||
2853 | * // "text"^ (range is anchored in text node's parent) | ||
2854 | * | ||
2855 | * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition}, | ||
2856 | * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}. | ||
2857 | * | ||
2858 | * @readonly | ||
2859 | * @member CKEDITOR | ||
2860 | * @property {Number} [=4] | ||
2861 | */ | ||
2862 | CKEDITOR.POSITION_AFTER_END = 4; | ||
2863 | |||
2864 | CKEDITOR.ENLARGE_ELEMENT = 1; | ||
2865 | CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2; | ||
2866 | CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3; | ||
2867 | CKEDITOR.ENLARGE_INLINE = 4; | ||
2868 | |||
2869 | // Check boundary types. | ||
2870 | |||
2871 | /** | ||
2872 | * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}. | ||
2873 | * | ||
2874 | * @readonly | ||
2875 | * @member CKEDITOR | ||
2876 | * @property {Number} [=1] | ||
2877 | */ | ||
2878 | CKEDITOR.START = 1; | ||
2879 | |||
2880 | /** | ||
2881 | * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}. | ||
2882 | * | ||
2883 | * @readonly | ||
2884 | * @member CKEDITOR | ||
2885 | * @property {Number} [=2] | ||
2886 | */ | ||
2887 | CKEDITOR.END = 2; | ||
2888 | |||
2889 | // Shrink range types. | ||
2890 | |||
2891 | /** | ||
2892 | * See {@link CKEDITOR.dom.range#shrink}. | ||
2893 | * | ||
2894 | * @readonly | ||
2895 | * @member CKEDITOR | ||
2896 | * @property {Number} [=1] | ||
2897 | */ | ||
2898 | CKEDITOR.SHRINK_ELEMENT = 1; | ||
2899 | |||
2900 | /** | ||
2901 | * See {@link CKEDITOR.dom.range#shrink}. | ||
2902 | * | ||
2903 | * @readonly | ||
2904 | * @member CKEDITOR | ||
2905 | * @property {Number} [=2] | ||
2906 | */ | ||
2907 | CKEDITOR.SHRINK_TEXT = 2; | ||
diff --git a/sources/core/dom/rangelist.js b/sources/core/dom/rangelist.js new file mode 100644 index 0000000..d02fc03 --- /dev/null +++ b/sources/core/dom/rangelist.js | |||
@@ -0,0 +1,199 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | ( function() { | ||
7 | /** | ||
8 | * Represents a list os CKEDITOR.dom.range objects, which can be easily | ||
9 | * iterated sequentially. | ||
10 | * | ||
11 | * @class | ||
12 | * @extends Array | ||
13 | * @constructor Creates a rangeList class instance. | ||
14 | * @param {CKEDITOR.dom.range/CKEDITOR.dom.range[]} [ranges] The ranges contained on this list. | ||
15 | * Note that, if an array of ranges is specified, the range sequence | ||
16 | * should match its DOM order. This class will not help to sort them. | ||
17 | */ | ||
18 | CKEDITOR.dom.rangeList = function( ranges ) { | ||
19 | if ( ranges instanceof CKEDITOR.dom.rangeList ) | ||
20 | return ranges; | ||
21 | |||
22 | if ( !ranges ) | ||
23 | ranges = []; | ||
24 | else if ( ranges instanceof CKEDITOR.dom.range ) | ||
25 | ranges = [ ranges ]; | ||
26 | |||
27 | return CKEDITOR.tools.extend( ranges, mixins ); | ||
28 | }; | ||
29 | |||
30 | var mixins = { | ||
31 | /** | ||
32 | * Creates an instance of the rangeList iterator, it should be used | ||
33 | * only when the ranges processing could be DOM intrusive, which | ||
34 | * means it may pollute and break other ranges in this list. | ||
35 | * Otherwise, it's enough to just iterate over this array in a for loop. | ||
36 | * | ||
37 | * @returns {CKEDITOR.dom.rangeListIterator} | ||
38 | */ | ||
39 | createIterator: function() { | ||
40 | var rangeList = this, | ||
41 | bookmark = CKEDITOR.dom.walker.bookmark(), | ||
42 | bookmarks = [], | ||
43 | current; | ||
44 | |||
45 | return { | ||
46 | /** | ||
47 | * Retrieves the next range in the list. | ||
48 | * | ||
49 | * @member CKEDITOR.dom.rangeListIterator | ||
50 | * @param {Boolean} [mergeConsequent=false] Whether join two adjacent | ||
51 | * ranges into single, e.g. consequent table cells. | ||
52 | */ | ||
53 | getNextRange: function( mergeConsequent ) { | ||
54 | current = current === undefined ? 0 : current + 1; | ||
55 | |||
56 | var range = rangeList[ current ]; | ||
57 | |||
58 | // Multiple ranges might be mangled by each other. | ||
59 | if ( range && rangeList.length > 1 ) { | ||
60 | // Bookmarking all other ranges on the first iteration, | ||
61 | // the range correctness after it doesn't matter since we'll | ||
62 | // restore them before the next iteration. | ||
63 | if ( !current ) { | ||
64 | // Make sure bookmark correctness by reverse processing. | ||
65 | for ( var i = rangeList.length - 1; i >= 0; i-- ) | ||
66 | bookmarks.unshift( rangeList[ i ].createBookmark( true ) ); | ||
67 | } | ||
68 | |||
69 | if ( mergeConsequent ) { | ||
70 | // Figure out how many ranges should be merged. | ||
71 | var mergeCount = 0; | ||
72 | while ( rangeList[ current + mergeCount + 1 ] ) { | ||
73 | var doc = range.document, | ||
74 | found = 0, | ||
75 | left = doc.getById( bookmarks[ mergeCount ].endNode ), | ||
76 | right = doc.getById( bookmarks[ mergeCount + 1 ].startNode ), | ||
77 | next; | ||
78 | |||
79 | // Check subsequent range. | ||
80 | while ( 1 ) { | ||
81 | next = left.getNextSourceNode( false ); | ||
82 | if ( !right.equals( next ) ) { | ||
83 | // This could be yet another bookmark or | ||
84 | // walking across block boundaries. | ||
85 | if ( bookmark( next ) || ( next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() ) ) { | ||
86 | left = next; | ||
87 | continue; | ||
88 | } | ||
89 | } else { | ||
90 | found = 1; | ||
91 | } | ||
92 | |||
93 | break; | ||
94 | } | ||
95 | |||
96 | if ( !found ) | ||
97 | break; | ||
98 | |||
99 | mergeCount++; | ||
100 | } | ||
101 | } | ||
102 | |||
103 | range.moveToBookmark( bookmarks.shift() ); | ||
104 | |||
105 | // Merge ranges finally after moving to bookmarks. | ||
106 | while ( mergeCount-- ) { | ||
107 | next = rangeList[ ++current ]; | ||
108 | next.moveToBookmark( bookmarks.shift() ); | ||
109 | range.setEnd( next.endContainer, next.endOffset ); | ||
110 | } | ||
111 | } | ||
112 | |||
113 | return range; | ||
114 | } | ||
115 | }; | ||
116 | }, | ||
117 | |||
118 | /** | ||
119 | * Create bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark}. | ||
120 | * | ||
121 | * @param {Boolean} [serializable=false] See {@link CKEDITOR.dom.range#createBookmark}. | ||
122 | * @returns {Array} Array of bookmarks. | ||
123 | */ | ||
124 | createBookmarks: function( serializable ) { | ||
125 | var retval = [], | ||
126 | bookmark; | ||
127 | for ( var i = 0; i < this.length; i++ ) { | ||
128 | retval.push( bookmark = this[ i ].createBookmark( serializable, true ) ); | ||
129 | |||
130 | // Updating the container & offset values for ranges | ||
131 | // that have been touched. | ||
132 | for ( var j = i + 1; j < this.length; j++ ) { | ||
133 | this[ j ] = updateDirtyRange( bookmark, this[ j ] ); | ||
134 | this[ j ] = updateDirtyRange( bookmark, this[ j ], true ); | ||
135 | } | ||
136 | } | ||
137 | return retval; | ||
138 | }, | ||
139 | |||
140 | /** | ||
141 | * Create "unobtrusive" bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark2}. | ||
142 | * | ||
143 | * @param {Boolean} [normalized=false] See {@link CKEDITOR.dom.range#createBookmark2}. | ||
144 | * @returns {Array} Array of bookmarks. | ||
145 | */ | ||
146 | createBookmarks2: function( normalized ) { | ||
147 | var bookmarks = []; | ||
148 | |||
149 | for ( var i = 0; i < this.length; i++ ) | ||
150 | bookmarks.push( this[ i ].createBookmark2( normalized ) ); | ||
151 | |||
152 | return bookmarks; | ||
153 | }, | ||
154 | |||
155 | /** | ||
156 | * Move each range in the list to the position specified by a list of bookmarks. | ||
157 | * | ||
158 | * @param {Array} bookmarks The list of bookmarks, each one matching a range in the list. | ||
159 | */ | ||
160 | moveToBookmarks: function( bookmarks ) { | ||
161 | for ( var i = 0; i < this.length; i++ ) | ||
162 | this[ i ].moveToBookmark( bookmarks[ i ] ); | ||
163 | } | ||
164 | }; | ||
165 | |||
166 | // Update the specified range which has been mangled by previous insertion of | ||
167 | // range bookmark nodes.(#3256) | ||
168 | function updateDirtyRange( bookmark, dirtyRange, checkEnd ) { | ||
169 | var serializable = bookmark.serializable, | ||
170 | container = dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ], | ||
171 | offset = checkEnd ? 'endOffset' : 'startOffset'; | ||
172 | |||
173 | var bookmarkStart = serializable ? dirtyRange.document.getById( bookmark.startNode ) : bookmark.startNode; | ||
174 | |||
175 | var bookmarkEnd = serializable ? dirtyRange.document.getById( bookmark.endNode ) : bookmark.endNode; | ||
176 | |||
177 | if ( container.equals( bookmarkStart.getPrevious() ) ) { | ||
178 | dirtyRange.startOffset = dirtyRange.startOffset - container.getLength() - bookmarkEnd.getPrevious().getLength(); | ||
179 | container = bookmarkEnd.getNext(); | ||
180 | } else if ( container.equals( bookmarkEnd.getPrevious() ) ) { | ||
181 | dirtyRange.startOffset = dirtyRange.startOffset - container.getLength(); | ||
182 | container = bookmarkEnd.getNext(); | ||
183 | } | ||
184 | |||
185 | container.equals( bookmarkStart.getParent() ) && dirtyRange[ offset ]++; | ||
186 | container.equals( bookmarkEnd.getParent() ) && dirtyRange[ offset ]++; | ||
187 | |||
188 | // Update and return this range. | ||
189 | dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ] = container; | ||
190 | return dirtyRange; | ||
191 | } | ||
192 | } )(); | ||
193 | |||
194 | /** | ||
195 | * (Virtual Class) Do not call this constructor. This class is not really part | ||
196 | * of the API. It just describes the return type of {@link CKEDITOR.dom.rangeList#createIterator}. | ||
197 | * | ||
198 | * @class CKEDITOR.dom.rangeListIterator | ||
199 | */ | ||
diff --git a/sources/core/dom/text.js b/sources/core/dom/text.js new file mode 100644 index 0000000..c313259 --- /dev/null +++ b/sources/core/dom/text.js | |||
@@ -0,0 +1,135 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 0000000..746b406 --- /dev/null +++ b/sources/core/dom/walker.js | |||
@@ -0,0 +1,652 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 Filling Char Sequence text node used in Webkit. (#9384, #13816) | ||
392 | isWhitespace = !CKEDITOR.tools.trim( node.getText() ) || | ||
393 | CKEDITOR.env.webkit && node.getText() == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE; | ||
394 | } | ||
395 | |||
396 | return !!( isReject ^ isWhitespace ); | ||
397 | }; | ||
398 | }; | ||
399 | |||
400 | /** | ||
401 | * Returns a function which checks whether the node is invisible in the WYSIWYG mode. | ||
402 | * | ||
403 | * @static | ||
404 | * @param {Boolean} [isReject=false] | ||
405 | * @returns {Function} | ||
406 | */ | ||
407 | CKEDITOR.dom.walker.invisible = function( isReject ) { | ||
408 | var whitespace = CKEDITOR.dom.walker.whitespaces(), | ||
409 | // #12221 (Chrome) plus #11111 (Safari). | ||
410 | offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0; | ||
411 | |||
412 | return function( node ) { | ||
413 | var invisible; | ||
414 | |||
415 | if ( whitespace( node ) ) | ||
416 | invisible = 1; | ||
417 | else { | ||
418 | // Visibility should be checked on element. | ||
419 | if ( node.type == CKEDITOR.NODE_TEXT ) | ||
420 | node = node.getParent(); | ||
421 | |||
422 | // Nodes that take no spaces in wysiwyg: | ||
423 | // 1. White-spaces but not including NBSP. | ||
424 | // 2. Empty inline elements, e.g. <b></b>. | ||
425 | // 3. <br> elements (bogus, surrounded by text) (#12423). | ||
426 | invisible = node.$.offsetWidth <= offsetWidth0; | ||
427 | } | ||
428 | |||
429 | return !!( isReject ^ invisible ); | ||
430 | }; | ||
431 | }; | ||
432 | |||
433 | /** | ||
434 | * Returns a function which checks whether the node type is equal to the passed one. | ||
435 | * | ||
436 | * @static | ||
437 | * @param {Number} type | ||
438 | * @param {Boolean} [isReject=false] | ||
439 | * @returns {Function} | ||
440 | */ | ||
441 | CKEDITOR.dom.walker.nodeType = function( type, isReject ) { | ||
442 | return function( node ) { | ||
443 | return !!( isReject ^ ( node.type == type ) ); | ||
444 | }; | ||
445 | }; | ||
446 | |||
447 | /** | ||
448 | * Returns a function which checks whether the node is a bogus (filler) node from | ||
449 | * `contenteditable` element's point of view. | ||
450 | * | ||
451 | * @static | ||
452 | * @param {Boolean} [isReject=false] | ||
453 | * @returns {Function} | ||
454 | */ | ||
455 | CKEDITOR.dom.walker.bogus = function( isReject ) { | ||
456 | function nonEmpty( node ) { | ||
457 | return !isWhitespaces( node ) && !isBookmark( node ); | ||
458 | } | ||
459 | |||
460 | return function( node ) { | ||
461 | var isBogus = CKEDITOR.env.needsBrFiller ? node.is && node.is( 'br' ) : node.getText && tailNbspRegex.test( node.getText() ); | ||
462 | |||
463 | if ( isBogus ) { | ||
464 | var parent = node.getParent(), | ||
465 | next = node.getNext( nonEmpty ); | ||
466 | |||
467 | isBogus = parent.isBlockBoundary() && ( !next || next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() ); | ||
468 | } | ||
469 | |||
470 | return !!( isReject ^ isBogus ); | ||
471 | }; | ||
472 | }; | ||
473 | |||
474 | /** | ||
475 | * Returns a function which checks whether the node is a temporary element | ||
476 | * (element with the `data-cke-temp` attribute) or its child. | ||
477 | * | ||
478 | * @since 4.3 | ||
479 | * @static | ||
480 | * @param {Boolean} [isReject=false] Whether to return `false` for the | ||
481 | * temporary element instead of `true` (default). | ||
482 | * @returns {Function} | ||
483 | */ | ||
484 | CKEDITOR.dom.walker.temp = function( isReject ) { | ||
485 | return function( node ) { | ||
486 | if ( node.type != CKEDITOR.NODE_ELEMENT ) | ||
487 | node = node.getParent(); | ||
488 | |||
489 | var isTemp = node && node.hasAttribute( 'data-cke-temp' ); | ||
490 | |||
491 | return !!( isReject ^ isTemp ); | ||
492 | }; | ||
493 | }; | ||
494 | |||
495 | var tailNbspRegex = /^[\t\r\n ]*(?: |\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 0000000..edfeb84 --- /dev/null +++ b/sources/core/dom/window.js | |||
@@ -0,0 +1,95 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 | } ); | ||
diff --git a/sources/core/dtd.js b/sources/core/dtd.js new file mode 100644 index 0000000..d6dc5a6 --- /dev/null +++ b/sources/core/dtd.js | |||
@@ -0,0 +1,349 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.dtd} object, which holds the DTD | ||
8 | * mapping for XHTML 1.0 Transitional. This file was automatically | ||
9 | * generated from the file: xhtml1-transitional.dtd. | ||
10 | */ | ||
11 | |||
12 | /** | ||
13 | * Holds and object representation of the HTML DTD to be used by the | ||
14 | * editor in its internal operations. | ||
15 | * | ||
16 | * Each element in the DTD is represented by a property in this object. Each | ||
17 | * property contains the list of elements that can be contained by the element. | ||
18 | * Text is represented by the `#` property. | ||
19 | * | ||
20 | * Several special grouping properties are also available. Their names start | ||
21 | * with the `$` character. | ||
22 | * | ||
23 | * // Check if <div> can be contained in a <p> element. | ||
24 | * alert( !!CKEDITOR.dtd[ 'p' ][ 'div' ] ); // false | ||
25 | * | ||
26 | * // Check if <p> can be contained in a <div> element. | ||
27 | * alert( !!CKEDITOR.dtd[ 'div' ][ 'p' ] ); // true | ||
28 | * | ||
29 | * // Check if <p> is a block element. | ||
30 | * alert( !!CKEDITOR.dtd.$block[ 'p' ] ); // true | ||
31 | * | ||
32 | * @class CKEDITOR.dtd | ||
33 | * @singleton | ||
34 | */ | ||
35 | CKEDITOR.dtd = ( function() { | ||
36 | 'use strict'; | ||
37 | |||
38 | var X = CKEDITOR.tools.extend, | ||
39 | // Subtraction rest of sets, from the first set. | ||
40 | Y = function( source, removed ) { | ||
41 | var substracted = CKEDITOR.tools.clone( source ); | ||
42 | for ( var i = 1; i < arguments.length; i++ ) { | ||
43 | removed = arguments[ i ]; | ||
44 | for ( var name in removed ) | ||
45 | delete substracted[ name ]; | ||
46 | } | ||
47 | return substracted; | ||
48 | }; | ||
49 | |||
50 | // Phrasing elements. | ||
51 | // P = { a: 1, em: 1, strong: 1, small: 1, abbr: 1, dfn: 1, i: 1, b: 1, s: 1, | ||
52 | // u: 1, code: 1, 'var': 1, samp: 1, kbd: 1, sup: 1, sub: 1, q: 1, cite: 1, | ||
53 | // span: 1, bdo: 1, bdi: 1, br: 1, wbr: 1, ins: 1, del: 1, img: 1, embed: 1, | ||
54 | // object: 1, iframe: 1, map: 1, area: 1, script: 1, noscript: 1, ruby: 1, | ||
55 | // video: 1, audio: 1, input: 1, textarea: 1, select: 1, button: 1, label: 1, | ||
56 | // output: 1, keygen: 1, progress: 1, command: 1, canvas: 1, time: 1, | ||
57 | // meter: 1, detalist: 1 }, | ||
58 | |||
59 | // Flow elements. | ||
60 | // F = { a: 1, p: 1, hr: 1, pre: 1, ul: 1, ol: 1, dl: 1, div: 1, h1: 1, h2: 1, | ||
61 | // h3: 1, h4: 1, h5: 1, h6: 1, hgroup: 1, address: 1, blockquote: 1, ins: 1, | ||
62 | // del: 1, object: 1, map: 1, noscript: 1, section: 1, nav: 1, article: 1, | ||
63 | // aside: 1, header: 1, footer: 1, video: 1, audio: 1, figure: 1, table: 1, | ||
64 | // form: 1, fieldset: 1, menu: 1, canvas: 1, details:1 }, | ||
65 | |||
66 | // Text can be everywhere. | ||
67 | // X( P, T ); | ||
68 | // Flow elements set consists of phrasing elements set. | ||
69 | // X( F, P ); | ||
70 | |||
71 | var P = {}, F = {}, | ||
72 | // Intersection of flow elements set and phrasing elements set. | ||
73 | PF = { | ||
74 | a: 1, abbr: 1, area: 1, audio: 1, b: 1, bdi: 1, bdo: 1, br: 1, button: 1, canvas: 1, cite: 1, | ||
75 | code: 1, command: 1, datalist: 1, del: 1, dfn: 1, em: 1, embed: 1, i: 1, iframe: 1, img: 1, | ||
76 | input: 1, ins: 1, kbd: 1, keygen: 1, label: 1, map: 1, mark: 1, meter: 1, noscript: 1, object: 1, | ||
77 | output: 1, progress: 1, q: 1, ruby: 1, s: 1, samp: 1, script: 1, select: 1, small: 1, span: 1, | ||
78 | strong: 1, sub: 1, sup: 1, textarea: 1, time: 1, u: 1, 'var': 1, video: 1, wbr: 1 | ||
79 | }, | ||
80 | // F - PF (Flow Only). | ||
81 | FO = { | ||
82 | address: 1, article: 1, aside: 1, blockquote: 1, details: 1, div: 1, dl: 1, fieldset: 1, | ||
83 | figure: 1, footer: 1, form: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, header: 1, hgroup: 1, | ||
84 | hr: 1, main: 1, menu: 1, nav: 1, ol: 1, p: 1, pre: 1, section: 1, table: 1, ul: 1 | ||
85 | }, | ||
86 | // Metadata elements. | ||
87 | M = { command: 1, link: 1, meta: 1, noscript: 1, script: 1, style: 1 }, | ||
88 | // Empty. | ||
89 | E = {}, | ||
90 | // Text. | ||
91 | T = { '#': 1 }, | ||
92 | |||
93 | // Deprecated phrasing elements. | ||
94 | DP = { acronym: 1, applet: 1, basefont: 1, big: 1, font: 1, isindex: 1, strike: 1, style: 1, tt: 1 }, // TODO remove "style". | ||
95 | // Deprecated flow only elements. | ||
96 | DFO = { center: 1, dir: 1, noframes: 1 }; | ||
97 | |||
98 | // Phrasing elements := PF + T + DP | ||
99 | X( P, PF, T, DP ); | ||
100 | // Flow elements := FO + P + DFO | ||
101 | X( F, FO, P, DFO ); | ||
102 | |||
103 | var dtd = { | ||
104 | a: Y( P, { a: 1, button: 1 } ), // Treat as normal inline element (not a transparent one). | ||
105 | abbr: P, | ||
106 | address: F, | ||
107 | area: E, | ||
108 | article: F, | ||
109 | aside: F, | ||
110 | audio: X( { source: 1, track: 1 }, F ), | ||
111 | b: P, | ||
112 | base: E, | ||
113 | bdi: P, | ||
114 | bdo: P, | ||
115 | blockquote: F, | ||
116 | body: F, | ||
117 | br: E, | ||
118 | button: Y( P, { a: 1, button: 1 } ), | ||
119 | canvas: P, // Treat as normal inline element (not a transparent one). | ||
120 | caption: F, | ||
121 | cite: P, | ||
122 | code: P, | ||
123 | col: E, | ||
124 | colgroup: { col: 1 }, | ||
125 | command: E, | ||
126 | datalist: X( { option: 1 }, P ), | ||
127 | dd: F, | ||
128 | del: P, // Treat as normal inline element (not a transparent one). | ||
129 | details: X( { summary: 1 }, F ), | ||
130 | dfn: P, | ||
131 | div: F, | ||
132 | dl: { dt: 1, dd: 1 }, | ||
133 | dt: F, | ||
134 | em: P, | ||
135 | embed: E, | ||
136 | fieldset: X( { legend: 1 }, F ), | ||
137 | figcaption: F, | ||
138 | figure: X( { figcaption: 1 }, F ), | ||
139 | footer: F, | ||
140 | form: F, | ||
141 | h1: P, | ||
142 | h2: P, | ||
143 | h3: P, | ||
144 | h4: P, | ||
145 | h5: P, | ||
146 | h6: P, | ||
147 | head: X( { title: 1, base: 1 }, M ), | ||
148 | header: F, | ||
149 | hgroup: { h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1 }, | ||
150 | hr: E, | ||
151 | html: X( { head: 1, body: 1 }, F, M ), // Head and body are optional... | ||
152 | i: P, | ||
153 | iframe: T, | ||
154 | img: E, | ||
155 | input: E, | ||
156 | ins: P, // Treat as normal inline element (not a transparent one). | ||
157 | kbd: P, | ||
158 | keygen: E, | ||
159 | label: P, | ||
160 | legend: P, | ||
161 | li: F, | ||
162 | link: E, | ||
163 | // Can't be a descendant of article, aside, footer, header, nav, but we don't need this | ||
164 | // complication. As well as checking if it's used only once. | ||
165 | main: F, | ||
166 | map: F, | ||
167 | mark: P, // Treat as normal inline element (not a transparent one). | ||
168 | menu: X( { li: 1 }, F ), | ||
169 | meta: E, | ||
170 | meter: Y( P, { meter: 1 } ), | ||
171 | nav: F, | ||
172 | noscript: X( { link: 1, meta: 1, style: 1 }, P ), // Treat as normal inline element (not a transparent one). | ||
173 | object: X( { param: 1 }, P ), // Treat as normal inline element (not a transparent one). | ||
174 | ol: { li: 1 }, | ||
175 | optgroup: { option: 1 }, | ||
176 | option: T, | ||
177 | output: P, | ||
178 | p: P, | ||
179 | param: E, | ||
180 | pre: P, | ||
181 | progress: Y( P, { progress: 1 } ), | ||
182 | q: P, | ||
183 | rp: P, | ||
184 | rt: P, | ||
185 | ruby: X( { rp: 1, rt: 1 }, P ), | ||
186 | s: P, | ||
187 | samp: P, | ||
188 | script: T, | ||
189 | section: F, | ||
190 | select: { optgroup: 1, option: 1 }, | ||
191 | small: P, | ||
192 | source: E, | ||
193 | span: P, | ||
194 | strong: P, | ||
195 | style: T, | ||
196 | sub: P, | ||
197 | summary: X( { h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1 }, P ), | ||
198 | sup: P, | ||
199 | table: { caption: 1, colgroup: 1, thead: 1, tfoot: 1, tbody: 1, tr: 1 }, | ||
200 | tbody: { tr: 1 }, | ||
201 | td: F, | ||
202 | textarea: T, | ||
203 | tfoot: { tr: 1 }, | ||
204 | th: F, | ||
205 | thead: { tr: 1 }, | ||
206 | time: Y( P, { time: 1 } ), | ||
207 | title: T, | ||
208 | tr: { th: 1, td: 1 }, | ||
209 | track: E, | ||
210 | u: P, | ||
211 | ul: { li: 1 }, | ||
212 | 'var': P, | ||
213 | video: X( { source: 1, track: 1 }, F ), | ||
214 | wbr: E, | ||
215 | |||
216 | // Deprecated tags. | ||
217 | acronym: P, | ||
218 | applet: X( { param: 1 }, F ), | ||
219 | basefont: E, | ||
220 | big: P, | ||
221 | center: F, | ||
222 | dialog: E, | ||
223 | dir: { li: 1 }, | ||
224 | font: P, | ||
225 | isindex: E, | ||
226 | noframes: F, | ||
227 | strike: P, | ||
228 | tt: P | ||
229 | }; | ||
230 | |||
231 | X( dtd, { | ||
232 | /** | ||
233 | * List of block elements, like `<p>` or `<div>`. | ||
234 | */ | ||
235 | $block: X( { audio: 1, dd: 1, dt: 1, figcaption: 1, li: 1, video: 1 }, FO, DFO ), | ||
236 | |||
237 | /** | ||
238 | * List of elements that contain other blocks, in which block-level operations should be limited, | ||
239 | * this property is not intended to be checked directly, use {@link CKEDITOR.dom.elementPath#blockLimit} instead. | ||
240 | * | ||
241 | * Some examples of editor behaviors that are impacted by block limits: | ||
242 | * | ||
243 | * * Enter key never split a block-limit element; | ||
244 | * * Style application is constraint by the block limit of the current selection. | ||
245 | * * Pasted html will be inserted into the block limit of the current selection. | ||
246 | * | ||
247 | * **Note:** As an exception `<li>` is not considered as a block limit, as it's generally used as a text block. | ||
248 | */ | ||
249 | $blockLimit: { | ||
250 | article: 1, aside: 1, audio: 1, body: 1, caption: 1, details: 1, dir: 1, div: 1, dl: 1, | ||
251 | fieldset: 1, figcaption: 1, figure: 1, footer: 1, form: 1, header: 1, hgroup: 1, main: 1, menu: 1, nav: 1, | ||
252 | ol: 1, section: 1, table: 1, td: 1, th: 1, tr: 1, ul: 1, video: 1 | ||
253 | }, | ||
254 | |||
255 | /** | ||
256 | * List of elements that contain character data. | ||
257 | */ | ||
258 | $cdata: { script: 1, style: 1 }, | ||
259 | |||
260 | /** | ||
261 | * List of elements that are accepted as inline editing hosts. | ||
262 | */ | ||
263 | $editable: { | ||
264 | address: 1, article: 1, aside: 1, blockquote: 1, body: 1, details: 1, div: 1, fieldset: 1, | ||
265 | figcaption: 1, footer: 1, form: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, header: 1, hgroup: 1, | ||
266 | main: 1, nav: 1, p: 1, pre: 1, section: 1 | ||
267 | }, | ||
268 | |||
269 | /** | ||
270 | * List of empty (self-closing) elements, like `<br>` or `<img>`. | ||
271 | */ | ||
272 | $empty: { | ||
273 | area: 1, base: 1, basefont: 1, br: 1, col: 1, command: 1, dialog: 1, embed: 1, hr: 1, img: 1, | ||
274 | input: 1, isindex: 1, keygen: 1, link: 1, meta: 1, param: 1, source: 1, track: 1, wbr: 1 | ||
275 | }, | ||
276 | |||
277 | /** | ||
278 | * List of inline (`<span>` like) elements. | ||
279 | */ | ||
280 | $inline: P, | ||
281 | |||
282 | /** | ||
283 | * List of list root elements. | ||
284 | */ | ||
285 | $list: { dl: 1, ol: 1, ul: 1 }, | ||
286 | |||
287 | /** | ||
288 | * List of list item elements, like `<li>` or `<dd>`. | ||
289 | */ | ||
290 | $listItem: { dd: 1, dt: 1, li: 1 }, | ||
291 | |||
292 | /** | ||
293 | * List of elements which may live outside body. | ||
294 | */ | ||
295 | $nonBodyContent: X( { body: 1, head: 1, html: 1 }, dtd.head ), | ||
296 | |||
297 | /** | ||
298 | * Elements that accept text nodes, but are not possible to edit into the browser. | ||
299 | */ | ||
300 | $nonEditable: { | ||
301 | applet: 1, audio: 1, button: 1, embed: 1, iframe: 1, map: 1, object: 1, option: 1, | ||
302 | param: 1, script: 1, textarea: 1, video: 1 | ||
303 | }, | ||
304 | |||
305 | /** | ||
306 | * Elements that are considered objects, therefore selected as a whole in the editor. | ||
307 | */ | ||
308 | $object: { | ||
309 | applet: 1, audio: 1, button: 1, hr: 1, iframe: 1, img: 1, input: 1, object: 1, select: 1, | ||
310 | table: 1, textarea: 1, video: 1 | ||
311 | }, | ||
312 | |||
313 | /** | ||
314 | * List of elements that can be ignored if empty, like `<b>` or `<span>`. | ||
315 | */ | ||
316 | $removeEmpty: { | ||
317 | abbr: 1, acronym: 1, b: 1, bdi: 1, bdo: 1, big: 1, cite: 1, code: 1, del: 1, dfn: 1, | ||
318 | em: 1, font: 1, i: 1, ins: 1, label: 1, kbd: 1, mark: 1, meter: 1, output: 1, q: 1, ruby: 1, s: 1, | ||
319 | samp: 1, small: 1, span: 1, strike: 1, strong: 1, sub: 1, sup: 1, time: 1, tt: 1, u: 1, 'var': 1 | ||
320 | }, | ||
321 | |||
322 | /** | ||
323 | * List of elements that have tabindex set to zero by default. | ||
324 | */ | ||
325 | $tabIndex: { a: 1, area: 1, button: 1, input: 1, object: 1, select: 1, textarea: 1 }, | ||
326 | |||
327 | /** | ||
328 | * List of elements used inside the `<table>` element, like `<tbody>` or `<td>`. | ||
329 | */ | ||
330 | $tableContent: { caption: 1, col: 1, colgroup: 1, tbody: 1, td: 1, tfoot: 1, th: 1, thead: 1, tr: 1 }, | ||
331 | |||
332 | /** | ||
333 | * List of "transparent" elements. See [W3C's definition of "transparent" element](http://dev.w3.org/html5/markup/terminology.html#transparent). | ||
334 | */ | ||
335 | $transparent: { a: 1, audio: 1, canvas: 1, del: 1, ins: 1, map: 1, noscript: 1, object: 1, video: 1 }, | ||
336 | |||
337 | /** | ||
338 | * List of elements that are not to exist standalone that must live under it's parent element. | ||
339 | */ | ||
340 | $intermediate: { | ||
341 | caption: 1, colgroup: 1, dd: 1, dt: 1, figcaption: 1, legend: 1, li: 1, optgroup: 1, | ||
342 | option: 1, rp: 1, rt: 1, summary: 1, tbody: 1, td: 1, tfoot: 1, th: 1, thead: 1, tr: 1 | ||
343 | } | ||
344 | } ); | ||
345 | |||
346 | return dtd; | ||
347 | } )(); | ||
348 | |||
349 | // PACKAGER_RENAME( CKEDITOR.dtd ) | ||
diff --git a/sources/core/editable.js b/sources/core/editable.js new file mode 100644 index 0000000..b9b0270 --- /dev/null +++ b/sources/core/editable.js | |||
@@ -0,0 +1,3158 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | ( function() { | ||
7 | /** | ||
8 | * Editable class which provides all editing related activities by | ||
9 | * the `contenteditable` element, dynamically get attached to editor instance. | ||
10 | * | ||
11 | * @class CKEDITOR.editable | ||
12 | * @extends CKEDITOR.dom.element | ||
13 | */ | ||
14 | CKEDITOR.editable = CKEDITOR.tools.createClass( { | ||
15 | base: CKEDITOR.dom.element, | ||
16 | /** | ||
17 | * The constructor only stores generic editable creation logic that is commonly shared among | ||
18 | * all different editable elements. | ||
19 | * | ||
20 | * @constructor Creates an editable class instance. | ||
21 | * @param {CKEDITOR.editor} editor The editor instance on which the editable operates. | ||
22 | * @param {HTMLElement/CKEDITOR.dom.element} element Any DOM element that was as the editor's | ||
23 | * editing container, e.g. it could be either an HTML element with the `contenteditable` attribute | ||
24 | * set to the `true` that handles WYSIWYG editing or a `<textarea>` element that handles source editing. | ||
25 | */ | ||
26 | $: function( editor, element ) { | ||
27 | // Transform the element into a CKEDITOR.dom.element instance. | ||
28 | this.base( element.$ || element ); | ||
29 | |||
30 | this.editor = editor; | ||
31 | |||
32 | /** | ||
33 | * Indicates the initialization status of the editable element. The following statuses are available: | ||
34 | * | ||
35 | * * **unloaded** – the initial state. The editable's instance was created but | ||
36 | * is not fully loaded (in particular it has no data). | ||
37 | * * **ready** – the editable is fully initialized. The `ready` status is set after | ||
38 | * the first {@link CKEDITOR.editor#method-setData} is called. | ||
39 | * * **detached** – the editable was detached. | ||
40 | * | ||
41 | * @since 4.3.3 | ||
42 | * @readonly | ||
43 | * @property {String} | ||
44 | */ | ||
45 | this.status = 'unloaded'; | ||
46 | |||
47 | /** | ||
48 | * Indicates whether the editable element gained focus. | ||
49 | * | ||
50 | * @property {Boolean} hasFocus | ||
51 | */ | ||
52 | this.hasFocus = false; | ||
53 | |||
54 | // The bootstrapping logic. | ||
55 | this.setup(); | ||
56 | }, | ||
57 | |||
58 | proto: { | ||
59 | focus: function() { | ||
60 | |||
61 | var active; | ||
62 | |||
63 | // [Webkit] When DOM focus is inside of nested contenteditable elements, | ||
64 | // apply focus on the main editable will compromise it's text selection. | ||
65 | if ( CKEDITOR.env.webkit && !this.hasFocus ) { | ||
66 | // Restore focus on element which we cached (on selectionCheck) as previously active. | ||
67 | active = this.editor._.previousActive || this.getDocument().getActive(); | ||
68 | if ( this.contains( active ) ) { | ||
69 | active.focus(); | ||
70 | return; | ||
71 | } | ||
72 | } | ||
73 | |||
74 | // [IE] Use instead "setActive" method to focus the editable if it belongs to | ||
75 | // the host page document, to avoid bringing an unexpected scroll. | ||
76 | try { | ||
77 | this.$[ CKEDITOR.env.ie && this.getDocument().equals( CKEDITOR.document ) ? 'setActive' : 'focus' ](); | ||
78 | } catch ( e ) { | ||
79 | // IE throws unspecified error when focusing editable after closing dialog opened on nested editable. | ||
80 | if ( !CKEDITOR.env.ie ) | ||
81 | throw e; | ||
82 | } | ||
83 | |||
84 | // Remedy if Safari doens't applies focus properly. (#279) | ||
85 | if ( CKEDITOR.env.safari && !this.isInline() ) { | ||
86 | active = CKEDITOR.document.getActive(); | ||
87 | if ( !active.equals( this.getWindow().getFrame() ) ) | ||
88 | this.getWindow().focus(); | ||
89 | |||
90 | } | ||
91 | }, | ||
92 | |||
93 | /** | ||
94 | * Overrides {@link CKEDITOR.dom.element#on} to have special `focus/blur` handling. | ||
95 | * The `focusin/focusout` events are used in IE to replace regular `focus/blur` events | ||
96 | * because we want to avoid the asynchronous nature of later ones. | ||
97 | */ | ||
98 | on: function( name, fn ) { | ||
99 | var args = Array.prototype.slice.call( arguments, 0 ); | ||
100 | |||
101 | if ( CKEDITOR.env.ie && ( /^focus|blur$/ ).exec( name ) ) { | ||
102 | name = name == 'focus' ? 'focusin' : 'focusout'; | ||
103 | |||
104 | // The "focusin/focusout" events bubbled, e.g. If there are elements with layout | ||
105 | // they fire this event when clicking in to edit them but it must be ignored | ||
106 | // to allow edit their contents. (#4682) | ||
107 | fn = isNotBubbling( fn, this ); | ||
108 | args[ 0 ] = name; | ||
109 | args[ 1 ] = fn; | ||
110 | } | ||
111 | |||
112 | return CKEDITOR.dom.element.prototype.on.apply( this, args ); | ||
113 | }, | ||
114 | |||
115 | /** | ||
116 | * Registers an event listener that needs to be removed when detaching this editable. | ||
117 | * This means that it will be automatically removed when {@link #detach} is executed, | ||
118 | * for example on {@link CKEDITOR.editor#setMode changing editor mode} or destroying editor. | ||
119 | * | ||
120 | * Except for `obj` all other arguments have the same meaning as in {@link CKEDITOR.event#on}. | ||
121 | * | ||
122 | * This method is strongly related to the {@link CKEDITOR.editor#contentDom} and | ||
123 | * {@link CKEDITOR.editor#contentDomUnload} events, because they are fired | ||
124 | * when an editable is being attached and detached. Therefore, this method is usually used | ||
125 | * in the following way: | ||
126 | * | ||
127 | * editor.on( 'contentDom', function() { | ||
128 | * var editable = editor.editable(); | ||
129 | * editable.attachListener( editable, 'mousedown', function() { | ||
130 | * // ... | ||
131 | * } ); | ||
132 | * } ); | ||
133 | * | ||
134 | * This code will attach the `mousedown` listener every time a new editable is attached | ||
135 | * to the editor, which in classic (`iframe`-based) editor happens every time the | ||
136 | * data or the mode is set. This listener will also be removed when that editable is detached. | ||
137 | * | ||
138 | * It is also possible to attach a listener to another object (e.g. to a document). | ||
139 | * | ||
140 | * editor.on( 'contentDom', function() { | ||
141 | * editor.editable().attachListener( editor.document, 'mousedown', function() { | ||
142 | * // ... | ||
143 | * } ); | ||
144 | * } ); | ||
145 | * | ||
146 | * @param {CKEDITOR.event} obj The element/object to which the listener will be attached. Every object | ||
147 | * which inherits from {@link CKEDITOR.event} may be used including {@link CKEDITOR.dom.element}, | ||
148 | * {@link CKEDITOR.dom.document}, and {@link CKEDITOR.editable}. | ||
149 | * @param {String} eventName The name of the event that will be listened to. | ||
150 | * @param {Function} listenerFunction The function listening to the | ||
151 | * event. A single {@link CKEDITOR.eventInfo} object instance | ||
152 | * containing all the event data is passed to this function. | ||
153 | * @param {Object} [scopeObj] The object used to scope the listener | ||
154 | * call (the `this` object). If omitted, the current object is used. | ||
155 | * @param {Object} [listenerData] Data to be sent as the | ||
156 | * {@link CKEDITOR.eventInfo#listenerData} when calling the listener. | ||
157 | * @param {Number} [priority=10] The listener priority. Lower priority | ||
158 | * listeners are called first. Listeners with the same priority | ||
159 | * value are called in the registration order. | ||
160 | * @returns {Object} An object containing the `removeListener` | ||
161 | * function that can be used to remove the listener at any time. | ||
162 | */ | ||
163 | attachListener: function( obj /*, event, fn, scope, listenerData, priority*/ ) { | ||
164 | !this._.listeners && ( this._.listeners = [] ); | ||
165 | // Register the listener. | ||
166 | var args = Array.prototype.slice.call( arguments, 1 ), | ||
167 | listener = obj.on.apply( obj, args ); | ||
168 | |||
169 | this._.listeners.push( listener ); | ||
170 | |||
171 | return listener; | ||
172 | }, | ||
173 | |||
174 | /** | ||
175 | * Remove all event listeners registered from {@link #attachListener}. | ||
176 | */ | ||
177 | clearListeners: function() { | ||
178 | var listeners = this._.listeners; | ||
179 | // Don't get broken by this. | ||
180 | try { | ||
181 | while ( listeners.length ) | ||
182 | listeners.pop().removeListener(); | ||
183 | } catch ( e ) {} | ||
184 | }, | ||
185 | |||
186 | /** | ||
187 | * Restore all attribution changes made by {@link #changeAttr }. | ||
188 | */ | ||
189 | restoreAttrs: function() { | ||
190 | var changes = this._.attrChanges, orgVal; | ||
191 | for ( var attr in changes ) { | ||
192 | if ( changes.hasOwnProperty( attr ) ) { | ||
193 | orgVal = changes[ attr ]; | ||
194 | // Restore original attribute. | ||
195 | orgVal !== null ? this.setAttribute( attr, orgVal ) : this.removeAttribute( attr ); | ||
196 | } | ||
197 | } | ||
198 | }, | ||
199 | |||
200 | /** | ||
201 | * Adds a CSS class name to this editable that needs to be removed on detaching. | ||
202 | * | ||
203 | * @param {String} className The class name to be added. | ||
204 | * @see CKEDITOR.dom.element#addClass | ||
205 | */ | ||
206 | attachClass: function( cls ) { | ||
207 | var classes = this.getCustomData( 'classes' ); | ||
208 | if ( !this.hasClass( cls ) ) { | ||
209 | !classes && ( classes = [] ), classes.push( cls ); | ||
210 | this.setCustomData( 'classes', classes ); | ||
211 | this.addClass( cls ); | ||
212 | } | ||
213 | }, | ||
214 | |||
215 | /** | ||
216 | * Make an attribution change that would be reverted on editable detaching. | ||
217 | * @param {String} attr The attribute name to be changed. | ||
218 | * @param {String} val The value of specified attribute. | ||
219 | */ | ||
220 | changeAttr: function( attr, val ) { | ||
221 | var orgVal = this.getAttribute( attr ); | ||
222 | if ( val !== orgVal ) { | ||
223 | !this._.attrChanges && ( this._.attrChanges = {} ); | ||
224 | |||
225 | // Saved the original attribute val. | ||
226 | if ( !( attr in this._.attrChanges ) ) | ||
227 | this._.attrChanges[ attr ] = orgVal; | ||
228 | |||
229 | this.setAttribute( attr, val ); | ||
230 | } | ||
231 | }, | ||
232 | |||
233 | /** | ||
234 | * Low-level method for inserting text into the editable. | ||
235 | * See the {@link CKEDITOR.editor#method-insertText} method which is the editor-level API | ||
236 | * for this purpose. | ||
237 | * | ||
238 | * @param {String} text | ||
239 | */ | ||
240 | insertText: function( text ) { | ||
241 | // Focus the editor before calling transformPlainTextToHtml. (#12726) | ||
242 | this.editor.focus(); | ||
243 | this.insertHtml( this.transformPlainTextToHtml( text ), 'text' ); | ||
244 | }, | ||
245 | |||
246 | /** | ||
247 | * Transforms plain text to HTML based on current selection and {@link CKEDITOR.editor#activeEnterMode}. | ||
248 | * | ||
249 | * @since 4.5 | ||
250 | * @param {String} text Text to transform. | ||
251 | * @returns {String} HTML generated from the text. | ||
252 | */ | ||
253 | transformPlainTextToHtml: function( text ) { | ||
254 | var enterMode = this.editor.getSelection().getStartElement().hasAscendant( 'pre', true ) ? | ||
255 | CKEDITOR.ENTER_BR : | ||
256 | this.editor.activeEnterMode; | ||
257 | |||
258 | return CKEDITOR.tools.transformPlainTextToHtml( text, enterMode ); | ||
259 | }, | ||
260 | |||
261 | /** | ||
262 | * Low-level method for inserting HTML into the editable. | ||
263 | * See the {@link CKEDITOR.editor#method-insertHtml} method which is the editor-level API | ||
264 | * for this purpose. | ||
265 | * | ||
266 | * This method will insert HTML into the current selection or a given range. It also creates an undo snapshot, | ||
267 | * scrolls the viewport to the insertion and selects the range next to the inserted content. | ||
268 | * If you want to insert HTML without additional operations use {@link #method-insertHtmlIntoRange}. | ||
269 | * | ||
270 | * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event. | ||
271 | * | ||
272 | * @param {String} data The HTML to be inserted. | ||
273 | * @param {String} [mode='html'] See {@link CKEDITOR.editor#method-insertHtml}'s param. | ||
274 | * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range | ||
275 | * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case). | ||
276 | * Introduced in CKEditor 4.5. | ||
277 | */ | ||
278 | insertHtml: function( data, mode, range ) { | ||
279 | var editor = this.editor; | ||
280 | |||
281 | editor.focus(); | ||
282 | editor.fire( 'saveSnapshot' ); | ||
283 | |||
284 | if ( !range ) { | ||
285 | // HTML insertion only considers the first range. | ||
286 | // Note: getRanges will be overwritten for tests since we want to test | ||
287 | // custom ranges and bypass native selections. | ||
288 | range = editor.getSelection().getRanges()[ 0 ]; | ||
289 | } | ||
290 | |||
291 | // Default mode is 'html'. | ||
292 | insert( this, mode || 'html', data, range ); | ||
293 | |||
294 | // Make the final range selection. | ||
295 | range.select(); | ||
296 | |||
297 | afterInsert( this ); | ||
298 | |||
299 | this.editor.fire( 'afterInsertHtml', {} ); | ||
300 | }, | ||
301 | |||
302 | /** | ||
303 | * Inserts HTML into the position in the editor determined by the range. | ||
304 | * | ||
305 | * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects inserted | ||
306 | * HTML. If you want to do it, use {@link #method-insertHtml}. | ||
307 | * | ||
308 | * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event. | ||
309 | * | ||
310 | * @since 4.5 | ||
311 | * @param {String} data HTML code to be inserted into the editor. | ||
312 | * @param {CKEDITOR.dom.range} range The range as a place of insertion. | ||
313 | * @param {String} [mode='html'] Mode in which HTML will be inserted. | ||
314 | * See {@link CKEDITOR.editor#method-insertHtml}. | ||
315 | */ | ||
316 | insertHtmlIntoRange: function( data, range, mode ) { | ||
317 | // Default mode is 'html' | ||
318 | insert( this, mode || 'html', data, range ); | ||
319 | |||
320 | this.editor.fire( 'afterInsertHtml', { intoRange: range } ); | ||
321 | }, | ||
322 | |||
323 | /** | ||
324 | * Low-level method for inserting an element into the editable. | ||
325 | * See the {@link CKEDITOR.editor#method-insertElement} method which is the editor-level API | ||
326 | * for this purpose. | ||
327 | * | ||
328 | * This method will insert the element into the current selection or a given range. It also creates an undo | ||
329 | * snapshot, scrolls the viewport to the insertion and selects the range next to the inserted content. | ||
330 | * If you want to insert an element without additional operations use {@link #method-insertElementIntoRange}. | ||
331 | * | ||
332 | * @param {CKEDITOR.dom.element} element The element to insert. | ||
333 | * @param {CKEDITOR.dom.range} [range] If specified, the element will be inserted into the range | ||
334 | * instead of into the selection. | ||
335 | */ | ||
336 | insertElement: function( element, range ) { | ||
337 | var editor = this.editor; | ||
338 | |||
339 | // Prepare for the insertion. For example - focus editor (#11848). | ||
340 | editor.focus(); | ||
341 | editor.fire( 'saveSnapshot' ); | ||
342 | |||
343 | var enterMode = editor.activeEnterMode, | ||
344 | selection = editor.getSelection(), | ||
345 | elementName = element.getName(), | ||
346 | isBlock = CKEDITOR.dtd.$block[ elementName ]; | ||
347 | |||
348 | if ( !range ) { | ||
349 | range = selection.getRanges()[ 0 ]; | ||
350 | } | ||
351 | |||
352 | // Insert element into first range only and ignore the rest (#11183). | ||
353 | if ( this.insertElementIntoRange( element, range ) ) { | ||
354 | range.moveToPosition( element, CKEDITOR.POSITION_AFTER_END ); | ||
355 | |||
356 | // If we're inserting a block element, the new cursor position must be | ||
357 | // optimized. (#3100,#5436,#8950) | ||
358 | if ( isBlock ) { | ||
359 | // Find next, meaningful element. | ||
360 | var next = element.getNext( function( node ) { | ||
361 | return isNotEmpty( node ) && !isBogus( node ); | ||
362 | } ); | ||
363 | |||
364 | if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.is( CKEDITOR.dtd.$block ) ) { | ||
365 | // If the next one is a text block, move cursor to the start of it's content. | ||
366 | if ( next.getDtd()[ '#' ] ) | ||
367 | range.moveToElementEditStart( next ); | ||
368 | // Otherwise move cursor to the before end of the last element. | ||
369 | else | ||
370 | range.moveToElementEditEnd( element ); | ||
371 | } | ||
372 | // Open a new line if the block is inserted at the end of parent. | ||
373 | else if ( !next && enterMode != CKEDITOR.ENTER_BR ) { | ||
374 | next = range.fixBlock( true, enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' ); | ||
375 | range.moveToElementEditStart( next ); | ||
376 | } | ||
377 | } | ||
378 | } | ||
379 | |||
380 | // Set up the correct selection. | ||
381 | selection.selectRanges( [ range ] ); | ||
382 | |||
383 | afterInsert( this ); | ||
384 | }, | ||
385 | |||
386 | /** | ||
387 | * Alias for {@link #insertElement}. | ||
388 | * | ||
389 | * @deprecated | ||
390 | * @param {CKEDITOR.dom.element} element The element to be inserted. | ||
391 | */ | ||
392 | insertElementIntoSelection: function( element ) { | ||
393 | this.insertElement( element ); | ||
394 | }, | ||
395 | |||
396 | /** | ||
397 | * Inserts an element into the position in the editor determined by the range. | ||
398 | * | ||
399 | * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects the inserted | ||
400 | * element. If you want to do it, use the {@link #method-insertElement} method. | ||
401 | * | ||
402 | * @param {CKEDITOR.dom.element} element The element to be inserted. | ||
403 | * @param {CKEDITOR.dom.range} range The range as a place of insertion. | ||
404 | * @returns {Boolean} Informs whether the insertion was successful. | ||
405 | */ | ||
406 | insertElementIntoRange: function( element, range ) { | ||
407 | var editor = this.editor, | ||
408 | enterMode = editor.config.enterMode, | ||
409 | elementName = element.getName(), | ||
410 | isBlock = CKEDITOR.dtd.$block[ elementName ]; | ||
411 | |||
412 | if ( range.checkReadOnly() ) | ||
413 | return false; | ||
414 | |||
415 | // Remove the original contents, merge split nodes. | ||
416 | range.deleteContents( 1 ); | ||
417 | |||
418 | // If range is placed in inermediate element (not td or th), we need to do three things: | ||
419 | // * fill emptied <td/th>s with if browser needs them, | ||
420 | // * remove empty text nodes so IE8 won't crash (http://dev.ckeditor.com/ticket/11183#comment:8), | ||
421 | // * fix structure and move range into the <td/th> element. | ||
422 | if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.is( { tr: 1, table: 1, tbody: 1, thead: 1, tfoot: 1 } ) ) | ||
423 | fixTableAfterContentsDeletion( range ); | ||
424 | |||
425 | // If we're inserting a block at dtd-violated position, split | ||
426 | // the parent blocks until we reach blockLimit. | ||
427 | var current, dtd; | ||
428 | |||
429 | if ( isBlock ) { | ||
430 | while ( ( current = range.getCommonAncestor( 0, 1 ) ) && | ||
431 | ( dtd = CKEDITOR.dtd[ current.getName() ] ) && | ||
432 | !( dtd && dtd[ elementName ] ) ) { | ||
433 | // Split up inline elements. | ||
434 | if ( current.getName() in CKEDITOR.dtd.span ) | ||
435 | range.splitElement( current ); | ||
436 | |||
437 | // If we're in an empty block which indicate a new paragraph, | ||
438 | // simply replace it with the inserting block.(#3664) | ||
439 | else if ( range.checkStartOfBlock() && range.checkEndOfBlock() ) { | ||
440 | range.setStartBefore( current ); | ||
441 | range.collapse( true ); | ||
442 | current.remove(); | ||
443 | } else { | ||
444 | range.splitBlock( enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p', editor.editable() ); | ||
445 | } | ||
446 | } | ||
447 | } | ||
448 | |||
449 | // Insert the new node. | ||
450 | range.insertNode( element ); | ||
451 | |||
452 | // Return true if insertion was successful. | ||
453 | return true; | ||
454 | }, | ||
455 | |||
456 | /** | ||
457 | * @see CKEDITOR.editor#setData | ||
458 | */ | ||
459 | setData: function( data, isSnapshot ) { | ||
460 | if ( !isSnapshot ) | ||
461 | data = this.editor.dataProcessor.toHtml( data ); | ||
462 | |||
463 | this.setHtml( data ); | ||
464 | this.fixInitialSelection(); | ||
465 | |||
466 | // Editable is ready after first setData. | ||
467 | if ( this.status == 'unloaded' ) | ||
468 | this.status = 'ready'; | ||
469 | |||
470 | this.editor.fire( 'dataReady' ); | ||
471 | }, | ||
472 | |||
473 | /** | ||
474 | * @see CKEDITOR.editor#getData | ||
475 | */ | ||
476 | getData: function( isSnapshot ) { | ||
477 | var data = this.getHtml(); | ||
478 | |||
479 | if ( !isSnapshot ) | ||
480 | data = this.editor.dataProcessor.toDataFormat( data ); | ||
481 | |||
482 | return data; | ||
483 | }, | ||
484 | |||
485 | /** | ||
486 | * Changes the read-only state of this editable. | ||
487 | * | ||
488 | * @param {Boolean} isReadOnly | ||
489 | */ | ||
490 | setReadOnly: function( isReadOnly ) { | ||
491 | this.setAttribute( 'contenteditable', !isReadOnly ); | ||
492 | }, | ||
493 | |||
494 | /** | ||
495 | * Detaches this editable object from the DOM (removes classes, listeners, etc.) | ||
496 | */ | ||
497 | detach: function() { | ||
498 | // Cleanup the element. | ||
499 | this.removeClass( 'cke_editable' ); | ||
500 | |||
501 | this.status = 'detached'; | ||
502 | |||
503 | // Save the editor reference which will be lost after | ||
504 | // calling detach from super class. | ||
505 | var editor = this.editor; | ||
506 | |||
507 | this._.detach(); | ||
508 | |||
509 | delete editor.document; | ||
510 | delete editor.window; | ||
511 | }, | ||
512 | |||
513 | /** | ||
514 | * Checks if the editable is one of the host page elements, indicates | ||
515 | * an inline editing environment. | ||
516 | * | ||
517 | * @returns {Boolean} | ||
518 | */ | ||
519 | isInline: function() { | ||
520 | return this.getDocument().equals( CKEDITOR.document ); | ||
521 | }, | ||
522 | |||
523 | /** | ||
524 | * Fixes the selection and focus which may be in incorrect state after | ||
525 | * editable's inner HTML was overwritten. | ||
526 | * | ||
527 | * If the editable did not have focus, then the selection will be fixed when the editable | ||
528 | * is focused for the first time. If the editable already had focus, then the selection will | ||
529 | * be fixed immediately. | ||
530 | * | ||
531 | * To understand the problem see: | ||
532 | * | ||
533 | * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata | ||
534 | * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusafterundoing | ||
535 | * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/selectionafterfocusing | ||
536 | * * http://tests.ckeditor.dev:1030/tests/plugins/newpage/manual/selectionafternewpage | ||
537 | * | ||
538 | * @since 4.4.6 | ||
539 | * @private | ||
540 | */ | ||
541 | fixInitialSelection: function() { | ||
542 | var that = this; | ||
543 | |||
544 | // Deal with IE8- IEQM (the old MS selection) first. | ||
545 | if ( CKEDITOR.env.ie && ( CKEDITOR.env.version < 9 || CKEDITOR.env.quirks ) ) { | ||
546 | if ( this.hasFocus ) { | ||
547 | this.focus(); | ||
548 | fixMSSelection(); | ||
549 | } | ||
550 | |||
551 | return; | ||
552 | } | ||
553 | |||
554 | // If editable did not have focus, fix the selection when it is first focused. | ||
555 | if ( !this.hasFocus ) { | ||
556 | this.once( 'focus', function() { | ||
557 | fixSelection(); | ||
558 | }, null, null, -999 ); | ||
559 | // If editable had focus, fix the selection immediately. | ||
560 | } else { | ||
561 | this.focus(); | ||
562 | fixSelection(); | ||
563 | } | ||
564 | |||
565 | function fixSelection() { | ||
566 | var $doc = that.getDocument().$, | ||
567 | $sel = $doc.getSelection(); | ||
568 | |||
569 | if ( requiresFix( $sel ) ) { | ||
570 | var range = new CKEDITOR.dom.range( that ); | ||
571 | range.moveToElementEditStart( that ); | ||
572 | |||
573 | var $range = $doc.createRange(); | ||
574 | $range.setStart( range.startContainer.$, range.startOffset ); | ||
575 | $range.collapse( true ); | ||
576 | |||
577 | $sel.removeAllRanges(); | ||
578 | $sel.addRange( $range ); | ||
579 | } | ||
580 | } | ||
581 | |||
582 | function requiresFix( $sel ) { | ||
583 | // This condition covers most broken cases after setting data. | ||
584 | if ( $sel.anchorNode && $sel.anchorNode == that.$ ) { | ||
585 | return true; | ||
586 | } | ||
587 | |||
588 | // Fix for: | ||
589 | // http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata | ||
590 | // (the inline editor TC) | ||
591 | if ( CKEDITOR.env.webkit ) { | ||
592 | var active = that.getDocument().getActive(); | ||
593 | if ( active && active.equals( that ) && !$sel.anchorNode ) { | ||
594 | return true; | ||
595 | } | ||
596 | } | ||
597 | } | ||
598 | |||
599 | function fixMSSelection() { | ||
600 | var $doc = that.getDocument().$, | ||
601 | $sel = $doc.selection, | ||
602 | active = that.getDocument().getActive(); | ||
603 | |||
604 | if ( $sel.type == 'None' && active.equals( that ) ) { | ||
605 | var range = new CKEDITOR.dom.range( that ), | ||
606 | parentElement, | ||
607 | $range = $doc.body.createTextRange(); | ||
608 | |||
609 | range.moveToElementEditStart( that ); | ||
610 | |||
611 | parentElement = range.startContainer; | ||
612 | if ( parentElement.type != CKEDITOR.NODE_ELEMENT ) { | ||
613 | parentElement = parentElement.getParent(); | ||
614 | } | ||
615 | |||
616 | $range.moveToElementText( parentElement.$ ); | ||
617 | $range.collapse( true ); | ||
618 | $range.select(); | ||
619 | } | ||
620 | } | ||
621 | }, | ||
622 | |||
623 | /** | ||
624 | * The base of the {@link CKEDITOR.editor#getSelectedHtml} method. | ||
625 | * | ||
626 | * @since 4.5 | ||
627 | * @method getHtmlFromRange | ||
628 | * @param {CKEDITOR.dom.range} range | ||
629 | * @returns {CKEDITOR.dom.documentFragment} | ||
630 | */ | ||
631 | getHtmlFromRange: function( range ) { | ||
632 | // There's nothing to return if range is collapsed. | ||
633 | if ( range.collapsed ) | ||
634 | return new CKEDITOR.dom.documentFragment( range.document ); | ||
635 | |||
636 | // Info object passed between methods. | ||
637 | var that = { | ||
638 | doc: this.getDocument(), | ||
639 | // Leave original range object untouched. | ||
640 | range: range.clone() | ||
641 | }; | ||
642 | |||
643 | getHtmlFromRangeHelpers.eol.detect( that, this ); | ||
644 | getHtmlFromRangeHelpers.bogus.exclude( that ); | ||
645 | getHtmlFromRangeHelpers.cell.shrink( that ); | ||
646 | |||
647 | that.fragment = that.range.cloneContents(); | ||
648 | |||
649 | getHtmlFromRangeHelpers.tree.rebuild( that, this ); | ||
650 | getHtmlFromRangeHelpers.eol.fix( that, this ); | ||
651 | |||
652 | return new CKEDITOR.dom.documentFragment( that.fragment.$ ); | ||
653 | }, | ||
654 | |||
655 | /** | ||
656 | * The base of the {@link CKEDITOR.editor#extractSelectedHtml} method. | ||
657 | * | ||
658 | * **Note:** The range is modified so it matches the desired selection after extraction | ||
659 | * even though the selection is not made. | ||
660 | * | ||
661 | * @since 4.5 | ||
662 | * @param {CKEDITOR.dom.range} range | ||
663 | * @param {Boolean} [removeEmptyBlock=false] See {@link CKEDITOR.editor#extractSelectedHtml}'s parameter. | ||
664 | * Note that the range will not be modified if this parameter is set to `true`. | ||
665 | * @returns {CKEDITOR.dom.documentFragment} The extracted fragment of the editable content. | ||
666 | */ | ||
667 | extractHtmlFromRange: function( range, removeEmptyBlock ) { | ||
668 | var helpers = extractHtmlFromRangeHelpers, | ||
669 | that = { | ||
670 | range: range, | ||
671 | doc: range.document | ||
672 | }, | ||
673 | // Since it is quite hard to build a valid documentFragment | ||
674 | // out of extracted contents because DOM changes, let's mimic | ||
675 | // extracted HTML with #getHtmlFromRange. Yep. It's a hack. | ||
676 | extractedFragment = this.getHtmlFromRange( range ); | ||
677 | |||
678 | // Collapsed range means that there's nothing to extract. | ||
679 | if ( range.collapsed ) { | ||
680 | range.optimize(); | ||
681 | return extractedFragment; | ||
682 | } | ||
683 | |||
684 | // Include inline element if possible. | ||
685 | range.enlarge( CKEDITOR.ENLARGE_INLINE, 1 ); | ||
686 | |||
687 | // This got to be done before bookmarks are created because purging | ||
688 | // depends on the position of the range at the boundaries of the table, | ||
689 | // usually distorted by bookmark spans. | ||
690 | helpers.table.detectPurge( that ); | ||
691 | |||
692 | // We'll play with DOM, let's hold the position of the range. | ||
693 | that.bookmark = range.createBookmark(); | ||
694 | // While bookmarked, make unaccessible, to make sure that none of the methods | ||
695 | // will try to use it (they should use that.bookmark). | ||
696 | // This is done because ranges get desynchronized with the DOM when more bookmarks | ||
697 | // is created (as for instance that.targetBookmark). | ||
698 | delete that.range; | ||
699 | |||
700 | // The range to be restored after extraction should be kept | ||
701 | // outside of the range, so it's not removed by range.extractContents. | ||
702 | var targetRange = this.editor.createRange(); | ||
703 | targetRange.moveToPosition( that.bookmark.startNode, CKEDITOR.POSITION_BEFORE_START ); | ||
704 | that.targetBookmark = targetRange.createBookmark(); | ||
705 | |||
706 | // Execute content-specific detections. | ||
707 | helpers.list.detectMerge( that, this ); | ||
708 | helpers.table.detectRanges( that, this ); | ||
709 | helpers.block.detectMerge( that, this ); | ||
710 | |||
711 | // Simply, do the job. | ||
712 | if ( that.tableContentsRanges ) { | ||
713 | helpers.table.deleteRanges( that ); | ||
714 | |||
715 | // Done here only to remove bookmark's spans. | ||
716 | range.moveToBookmark( that.bookmark ); | ||
717 | that.range = range; | ||
718 | } else { | ||
719 | // To use the range we need to restore the bookmark and make | ||
720 | // the range accessible again. | ||
721 | range.moveToBookmark( that.bookmark ); | ||
722 | that.range = range; | ||
723 | range.extractContents( helpers.detectExtractMerge( that ) ); | ||
724 | } | ||
725 | |||
726 | // Move working range to desired, pre-computed position. | ||
727 | range.moveToBookmark( that.targetBookmark ); | ||
728 | |||
729 | // Make sure range is always anchored in an element. For consistency. | ||
730 | range.optimize(); | ||
731 | |||
732 | // It my happen that the uncollapsed range which referred to a valid selection, | ||
733 | // will be placed in an uneditable location after being collapsed: | ||
734 | // <tr>[<td>x</td>]</tr> -> <tr>[]<td>x</td></tr> -> <tr><td>[]x</td></tr> | ||
735 | helpers.fixUneditableRangePosition( range ); | ||
736 | |||
737 | // Execute content-specific post-extract routines. | ||
738 | helpers.list.merge( that, this ); | ||
739 | helpers.table.purge( that, this ); | ||
740 | helpers.block.merge( that, this ); | ||
741 | |||
742 | // Remove empty block, duh! | ||
743 | if ( removeEmptyBlock ) { | ||
744 | var path = range.startPath(); | ||
745 | |||
746 | // <p><b>^</b></p> is empty block. | ||
747 | if ( | ||
748 | range.checkStartOfBlock() && | ||
749 | range.checkEndOfBlock() && | ||
750 | path.block && | ||
751 | !range.root.equals( path.block ) && | ||
752 | // Do not remove a block with bookmarks. (#13465) | ||
753 | !hasBookmarks( path.block ) ) { | ||
754 | range.moveToPosition( path.block, CKEDITOR.POSITION_BEFORE_START ); | ||
755 | path.block.remove(); | ||
756 | } | ||
757 | } else { | ||
758 | // Auto paragraph, if needed. | ||
759 | helpers.autoParagraph( this.editor, range ); | ||
760 | |||
761 | // Let's have a bogus next to the caret, if needed. | ||
762 | if ( isEmpty( range.startContainer ) ) | ||
763 | range.startContainer.appendBogus(); | ||
764 | } | ||
765 | |||
766 | // Merge inline siblings if any around the caret. | ||
767 | range.startContainer.mergeSiblings(); | ||
768 | |||
769 | return extractedFragment; | ||
770 | }, | ||
771 | |||
772 | /** | ||
773 | * Editable element bootstrapping. | ||
774 | * | ||
775 | * @private | ||
776 | */ | ||
777 | setup: function() { | ||
778 | var editor = this.editor; | ||
779 | |||
780 | // Handle the load/read of editor data/snapshot. | ||
781 | this.attachListener( editor, 'beforeGetData', function() { | ||
782 | var data = this.getData(); | ||
783 | |||
784 | // Post processing html output of wysiwyg editable. | ||
785 | if ( !this.is( 'textarea' ) ) { | ||
786 | // Reset empty if the document contains only one empty paragraph. | ||
787 | if ( editor.config.ignoreEmptyParagraph !== false ) | ||
788 | data = data.replace( emptyParagraphRegexp, function( match, lookback ) { | ||
789 | return lookback; | ||
790 | } ); | ||
791 | } | ||
792 | |||
793 | editor.setData( data, null, 1 ); | ||
794 | }, this ); | ||
795 | |||
796 | this.attachListener( editor, 'getSnapshot', function( evt ) { | ||
797 | evt.data = this.getData( 1 ); | ||
798 | }, this ); | ||
799 | |||
800 | this.attachListener( editor, 'afterSetData', function() { | ||
801 | this.setData( editor.getData( 1 ) ); | ||
802 | }, this ); | ||
803 | this.attachListener( editor, 'loadSnapshot', function( evt ) { | ||
804 | this.setData( evt.data, 1 ); | ||
805 | }, this ); | ||
806 | |||
807 | // Delegate editor focus/blur to editable. | ||
808 | this.attachListener( editor, 'beforeFocus', function() { | ||
809 | var sel = editor.getSelection(), | ||
810 | ieSel = sel && sel.getNative(); | ||
811 | |||
812 | // IE considers control-type element as separate | ||
813 | // focus host when selected, avoid destroying the | ||
814 | // selection in such case. (#5812) (#8949) | ||
815 | if ( ieSel && ieSel.type == 'Control' ) | ||
816 | return; | ||
817 | |||
818 | this.focus(); | ||
819 | }, this ); | ||
820 | |||
821 | this.attachListener( editor, 'insertHtml', function( evt ) { | ||
822 | this.insertHtml( evt.data.dataValue, evt.data.mode, evt.data.range ); | ||
823 | }, this ); | ||
824 | this.attachListener( editor, 'insertElement', function( evt ) { | ||
825 | this.insertElement( evt.data ); | ||
826 | }, this ); | ||
827 | this.attachListener( editor, 'insertText', function( evt ) { | ||
828 | this.insertText( evt.data ); | ||
829 | }, this ); | ||
830 | |||
831 | // Update editable state. | ||
832 | this.setReadOnly( editor.readOnly ); | ||
833 | |||
834 | // The editable class. | ||
835 | this.attachClass( 'cke_editable' ); | ||
836 | |||
837 | // The element mode css class. | ||
838 | if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) { | ||
839 | this.attachClass( 'cke_editable_inline' ); | ||
840 | } else if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE || | ||
841 | editor.elementMode == CKEDITOR.ELEMENT_MODE_APPENDTO ) { | ||
842 | this.attachClass( 'cke_editable_themed' ); | ||
843 | } | ||
844 | |||
845 | this.attachClass( 'cke_contents_' + editor.config.contentsLangDirection ); | ||
846 | |||
847 | // Setup editor keystroke handlers on this element. | ||
848 | var keystrokeHandler = editor.keystrokeHandler; | ||
849 | |||
850 | // If editor is read-only, then make sure that BACKSPACE key | ||
851 | // is blocked to prevent browser history navigation. | ||
852 | keystrokeHandler.blockedKeystrokes[ 8 ] = +editor.readOnly; | ||
853 | |||
854 | editor.keystrokeHandler.attach( this ); | ||
855 | |||
856 | // Update focus states. | ||
857 | this.on( 'blur', function() { | ||
858 | this.hasFocus = false; | ||
859 | }, null, null, -1 ); | ||
860 | |||
861 | this.on( 'focus', function() { | ||
862 | this.hasFocus = true; | ||
863 | }, null, null, -1 ); | ||
864 | |||
865 | // Register to focus manager. | ||
866 | editor.focusManager.add( this ); | ||
867 | |||
868 | // Inherit the initial focus on editable element. | ||
869 | if ( this.equals( CKEDITOR.document.getActive() ) ) { | ||
870 | this.hasFocus = true; | ||
871 | // Pending until this editable has attached. | ||
872 | editor.once( 'contentDom', function() { | ||
873 | editor.focusManager.focus( this ); | ||
874 | }, this ); | ||
875 | } | ||
876 | |||
877 | // Apply tab index on demand, with original direction saved. | ||
878 | if ( this.isInline() ) { | ||
879 | |||
880 | // tabIndex of the editable is different than editor's one. | ||
881 | // Update the attribute of the editable. | ||
882 | this.changeAttr( 'tabindex', editor.tabIndex ); | ||
883 | } | ||
884 | |||
885 | // The above is all we'll be doing for a <textarea> editable. | ||
886 | if ( this.is( 'textarea' ) ) | ||
887 | return; | ||
888 | |||
889 | // The DOM document which the editing acts upon. | ||
890 | editor.document = this.getDocument(); | ||
891 | editor.window = this.getWindow(); | ||
892 | |||
893 | var doc = editor.document; | ||
894 | |||
895 | this.changeAttr( 'spellcheck', !editor.config.disableNativeSpellChecker ); | ||
896 | |||
897 | // Apply contents direction on demand, with original direction saved. | ||
898 | var dir = editor.config.contentsLangDirection; | ||
899 | if ( this.getDirection( 1 ) != dir ) | ||
900 | this.changeAttr( 'dir', dir ); | ||
901 | |||
902 | // Create the content stylesheet for this document. | ||
903 | var styles = CKEDITOR.getCss(); | ||
904 | if ( styles ) { | ||
905 | var head = doc.getHead(); | ||
906 | if ( !head.getCustomData( 'stylesheet' ) ) { | ||
907 | var sheet = doc.appendStyleText( styles ); | ||
908 | sheet = new CKEDITOR.dom.element( sheet.ownerNode || sheet.owningElement ); | ||
909 | head.setCustomData( 'stylesheet', sheet ); | ||
910 | sheet.data( 'cke-temp', 1 ); | ||
911 | } | ||
912 | } | ||
913 | |||
914 | // Update the stylesheet sharing count. | ||
915 | var ref = doc.getCustomData( 'stylesheet_ref' ) || 0; | ||
916 | doc.setCustomData( 'stylesheet_ref', ref + 1 ); | ||
917 | |||
918 | // Pass this configuration to styles system. | ||
919 | this.setCustomData( 'cke_includeReadonly', !editor.config.disableReadonlyStyling ); | ||
920 | |||
921 | // Prevent the browser opening read-only links. (#6032 & #10912) | ||
922 | this.attachListener( this, 'click', function( evt ) { | ||
923 | evt = evt.data; | ||
924 | |||
925 | var link = new CKEDITOR.dom.elementPath( evt.getTarget(), this ).contains( 'a' ); | ||
926 | |||
927 | if ( link && evt.$.button != 2 && link.isReadOnly() ) | ||
928 | evt.preventDefault(); | ||
929 | } ); | ||
930 | |||
931 | var backspaceOrDelete = { 8: 1, 46: 1 }; | ||
932 | |||
933 | // Override keystrokes which should have deletion behavior | ||
934 | // on fully selected element . (#4047) (#7645) | ||
935 | this.attachListener( editor, 'key', function( evt ) { | ||
936 | if ( editor.readOnly ) | ||
937 | return true; | ||
938 | |||
939 | // Use getKey directly in order to ignore modifiers. | ||
940 | // Justification: http://dev.ckeditor.com/ticket/11861#comment:13 | ||
941 | var keyCode = evt.data.domEvent.getKey(), | ||
942 | isHandled; | ||
943 | |||
944 | // Backspace OR Delete. | ||
945 | if ( keyCode in backspaceOrDelete ) { | ||
946 | var sel = editor.getSelection(), | ||
947 | selected, | ||
948 | range = sel.getRanges()[ 0 ], | ||
949 | path = range.startPath(), | ||
950 | block, | ||
951 | parent, | ||
952 | next, | ||
953 | rtl = keyCode == 8; | ||
954 | |||
955 | if ( | ||
956 | // [IE<11] Remove selected image/anchor/etc here to avoid going back in history. (#10055) | ||
957 | ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 && ( selected = sel.getSelectedElement() ) ) || | ||
958 | // Remove the entire list/table on fully selected content. (#7645) | ||
959 | ( selected = getSelectedTableList( sel ) ) ) { | ||
960 | // Make undo snapshot. | ||
961 | editor.fire( 'saveSnapshot' ); | ||
962 | |||
963 | // Delete any element that 'hasLayout' (e.g. hr,table) in IE8 will | ||
964 | // break up the selection, safely manage it here. (#4795) | ||
965 | range.moveToPosition( selected, CKEDITOR.POSITION_BEFORE_START ); | ||
966 | // Remove the control manually. | ||
967 | selected.remove(); | ||
968 | range.select(); | ||
969 | |||
970 | editor.fire( 'saveSnapshot' ); | ||
971 | |||
972 | isHandled = 1; | ||
973 | } else if ( range.collapsed ) { | ||
974 | // Handle the following special cases: (#6217) | ||
975 | // 1. Del/Backspace key before/after table; | ||
976 | // 2. Backspace Key after start of table. | ||
977 | if ( ( block = path.block ) && | ||
978 | ( next = block[ rtl ? 'getPrevious' : 'getNext' ]( isNotWhitespace ) ) && | ||
979 | ( next.type == CKEDITOR.NODE_ELEMENT ) && | ||
980 | next.is( 'table' ) && | ||
981 | range[ rtl ? 'checkStartOfBlock' : 'checkEndOfBlock' ]() ) { | ||
982 | editor.fire( 'saveSnapshot' ); | ||
983 | |||
984 | // Remove the current empty block. | ||
985 | if ( range[ rtl ? 'checkEndOfBlock' : 'checkStartOfBlock' ]() ) | ||
986 | block.remove(); | ||
987 | |||
988 | // Move cursor to the beginning/end of table cell. | ||
989 | range[ 'moveToElementEdit' + ( rtl ? 'End' : 'Start' ) ]( next ); | ||
990 | range.select(); | ||
991 | |||
992 | editor.fire( 'saveSnapshot' ); | ||
993 | |||
994 | isHandled = 1; | ||
995 | } | ||
996 | else if ( path.blockLimit && path.blockLimit.is( 'td' ) && | ||
997 | ( parent = path.blockLimit.getAscendant( 'table' ) ) && | ||
998 | range.checkBoundaryOfElement( parent, rtl ? CKEDITOR.START : CKEDITOR.END ) && | ||
999 | ( next = parent[ rtl ? 'getPrevious' : 'getNext' ]( isNotWhitespace ) ) ) { | ||
1000 | editor.fire( 'saveSnapshot' ); | ||
1001 | |||
1002 | // Move cursor to the end of previous block. | ||
1003 | range[ 'moveToElementEdit' + ( rtl ? 'End' : 'Start' ) ]( next ); | ||
1004 | |||
1005 | // Remove any previous empty block. | ||
1006 | if ( range.checkStartOfBlock() && range.checkEndOfBlock() ) | ||
1007 | next.remove(); | ||
1008 | else | ||
1009 | range.select(); | ||
1010 | |||
1011 | editor.fire( 'saveSnapshot' ); | ||
1012 | |||
1013 | isHandled = 1; | ||
1014 | } | ||
1015 | // BACKSPACE/DEL pressed at the start/end of table cell. | ||
1016 | else if ( ( parent = path.contains( [ 'td', 'th', 'caption' ] ) ) && | ||
1017 | range.checkBoundaryOfElement( parent, rtl ? CKEDITOR.START : CKEDITOR.END ) ) { | ||
1018 | isHandled = 1; | ||
1019 | } | ||
1020 | } | ||
1021 | |||
1022 | } | ||
1023 | |||
1024 | return !isHandled; | ||
1025 | } ); | ||
1026 | |||
1027 | // On IE>=11 we need to fill blockless editable with <br> if it was deleted. | ||
1028 | if ( editor.blockless && CKEDITOR.env.ie && CKEDITOR.env.needsBrFiller ) { | ||
1029 | this.attachListener( this, 'keyup', function( evt ) { | ||
1030 | if ( evt.data.getKeystroke() in backspaceOrDelete && !this.getFirst( isNotEmpty ) ) { | ||
1031 | this.appendBogus(); | ||
1032 | |||
1033 | // Set the selection before bogus, because IE tends to put it after. | ||
1034 | var range = editor.createRange(); | ||
1035 | range.moveToPosition( this, CKEDITOR.POSITION_AFTER_START ); | ||
1036 | range.select(); | ||
1037 | } | ||
1038 | } ); | ||
1039 | } | ||
1040 | |||
1041 | this.attachListener( this, 'dblclick', function( evt ) { | ||
1042 | if ( editor.readOnly ) | ||
1043 | return false; | ||
1044 | |||
1045 | var data = { element: evt.data.getTarget() }; | ||
1046 | editor.fire( 'doubleclick', data ); | ||
1047 | } ); | ||
1048 | |||
1049 | // Prevent automatic submission in IE #6336 | ||
1050 | CKEDITOR.env.ie && this.attachListener( this, 'click', blockInputClick ); | ||
1051 | |||
1052 | // Gecko/Webkit need some help when selecting control type elements. (#3448) | ||
1053 | // We apply same behavior for IE Edge. (#13386) | ||
1054 | if ( !CKEDITOR.env.ie || CKEDITOR.env.edge ) { | ||
1055 | this.attachListener( this, 'mousedown', function( ev ) { | ||
1056 | var control = ev.data.getTarget(); | ||
1057 | // #11727. Note: htmlDP assures that input/textarea/select have contenteditable=false | ||
1058 | // attributes. However, they also have data-cke-editable attribute, so isReadOnly() returns false, | ||
1059 | // and therefore those elements are correctly selected by this code. | ||
1060 | if ( control.is( 'img', 'hr', 'input', 'textarea', 'select' ) && !control.isReadOnly() ) { | ||
1061 | editor.getSelection().selectElement( control ); | ||
1062 | |||
1063 | // Prevent focus from stealing from the editable. (#9515) | ||
1064 | if ( control.is( 'input', 'textarea', 'select' ) ) | ||
1065 | ev.data.preventDefault(); | ||
1066 | } | ||
1067 | } ); | ||
1068 | } | ||
1069 | |||
1070 | // For some reason, after click event is done, IE Edge loses focus on the selected element. (#13386) | ||
1071 | if ( CKEDITOR.env.edge ) { | ||
1072 | this.attachListener( this, 'mouseup', function( ev ) { | ||
1073 | var selectedElement = ev.data.getTarget(); | ||
1074 | if ( selectedElement && selectedElement.is( 'img' ) ) { | ||
1075 | editor.getSelection().selectElement( selectedElement ); | ||
1076 | } | ||
1077 | } ); | ||
1078 | } | ||
1079 | |||
1080 | // Prevent right click from selecting an empty block even | ||
1081 | // when selection is anchored inside it. (#5845) | ||
1082 | if ( CKEDITOR.env.gecko ) { | ||
1083 | this.attachListener( this, 'mouseup', function( ev ) { | ||
1084 | if ( ev.data.$.button == 2 ) { | ||
1085 | var target = ev.data.getTarget(); | ||
1086 | |||
1087 | if ( !target.getOuterHtml().replace( emptyParagraphRegexp, '' ) ) { | ||
1088 | var range = editor.createRange(); | ||
1089 | range.moveToElementEditStart( target ); | ||
1090 | range.select( true ); | ||
1091 | } | ||
1092 | } | ||
1093 | } ); | ||
1094 | } | ||
1095 | |||
1096 | // Webkit: avoid from editing form control elements content. | ||
1097 | if ( CKEDITOR.env.webkit ) { | ||
1098 | // Prevent from tick checkbox/radiobox/select | ||
1099 | this.attachListener( this, 'click', function( ev ) { | ||
1100 | if ( ev.data.getTarget().is( 'input', 'select' ) ) | ||
1101 | ev.data.preventDefault(); | ||
1102 | } ); | ||
1103 | |||
1104 | // Prevent from editig textfield/textarea value. | ||
1105 | this.attachListener( this, 'mouseup', function( ev ) { | ||
1106 | if ( ev.data.getTarget().is( 'input', 'textarea' ) ) | ||
1107 | ev.data.preventDefault(); | ||
1108 | } ); | ||
1109 | } | ||
1110 | |||
1111 | // Prevent Webkit/Blink from going rogue when joining | ||
1112 | // blocks on BACKSPACE/DEL (#11861,#9998). | ||
1113 | if ( CKEDITOR.env.webkit ) { | ||
1114 | this.attachListener( editor, 'key', function( evt ) { | ||
1115 | if ( editor.readOnly ) { | ||
1116 | return true; | ||
1117 | } | ||
1118 | |||
1119 | // Use getKey directly in order to ignore modifiers. | ||
1120 | // Justification: http://dev.ckeditor.com/ticket/11861#comment:13 | ||
1121 | var key = evt.data.domEvent.getKey(); | ||
1122 | |||
1123 | if ( !( key in backspaceOrDelete ) ) | ||
1124 | return; | ||
1125 | |||
1126 | var backspace = key == 8, | ||
1127 | range = editor.getSelection().getRanges()[ 0 ], | ||
1128 | startPath = range.startPath(); | ||
1129 | |||
1130 | if ( range.collapsed ) { | ||
1131 | if ( !mergeBlocksCollapsedSelection( editor, range, backspace, startPath ) ) | ||
1132 | return; | ||
1133 | } else { | ||
1134 | if ( !mergeBlocksNonCollapsedSelection( editor, range, startPath ) ) | ||
1135 | return; | ||
1136 | } | ||
1137 | |||
1138 | // Scroll to the new position of the caret (#11960). | ||
1139 | editor.getSelection().scrollIntoView(); | ||
1140 | editor.fire( 'saveSnapshot' ); | ||
1141 | |||
1142 | return false; | ||
1143 | }, this, null, 100 ); // Later is better – do not override existing listeners. | ||
1144 | } | ||
1145 | } | ||
1146 | }, | ||
1147 | |||
1148 | _: { | ||
1149 | detach: function() { | ||
1150 | // Update the editor cached data with current data. | ||
1151 | this.editor.setData( this.editor.getData(), 0, 1 ); | ||
1152 | |||
1153 | this.clearListeners(); | ||
1154 | this.restoreAttrs(); | ||
1155 | |||
1156 | // Cleanup our custom classes. | ||
1157 | var classes; | ||
1158 | if ( ( classes = this.removeCustomData( 'classes' ) ) ) { | ||
1159 | while ( classes.length ) | ||
1160 | this.removeClass( classes.pop() ); | ||
1161 | } | ||
1162 | |||
1163 | // Remove contents stylesheet from document if it's the last usage. | ||
1164 | if ( !this.is( 'textarea' ) ) { | ||
1165 | var doc = this.getDocument(), | ||
1166 | head = doc.getHead(); | ||
1167 | if ( head.getCustomData( 'stylesheet' ) ) { | ||
1168 | var refs = doc.getCustomData( 'stylesheet_ref' ); | ||
1169 | if ( !( --refs ) ) { | ||
1170 | doc.removeCustomData( 'stylesheet_ref' ); | ||
1171 | var sheet = head.removeCustomData( 'stylesheet' ); | ||
1172 | sheet.remove(); | ||
1173 | } else { | ||
1174 | doc.setCustomData( 'stylesheet_ref', refs ); | ||
1175 | } | ||
1176 | } | ||
1177 | } | ||
1178 | |||
1179 | this.editor.fire( 'contentDomUnload' ); | ||
1180 | |||
1181 | // Free up the editor reference. | ||
1182 | delete this.editor; | ||
1183 | } | ||
1184 | } | ||
1185 | } ); | ||
1186 | |||
1187 | /** | ||
1188 | * Creates, retrieves or detaches an editable element of the editor. | ||
1189 | * This method should always be used instead of calling {@link CKEDITOR.editable} directly. | ||
1190 | * | ||
1191 | * @method editable | ||
1192 | * @member CKEDITOR.editor | ||
1193 | * @param {CKEDITOR.dom.element/CKEDITOR.editable} elementOrEditable The | ||
1194 | * DOM element to become the editable or a {@link CKEDITOR.editable} object. | ||
1195 | */ | ||
1196 | CKEDITOR.editor.prototype.editable = function( element ) { | ||
1197 | var editable = this._.editable; | ||
1198 | |||
1199 | // This editor has already associated with | ||
1200 | // an editable element, silently fails. | ||
1201 | if ( editable && element ) | ||
1202 | return 0; | ||
1203 | |||
1204 | if ( arguments.length ) { | ||
1205 | editable = this._.editable = element ? ( element instanceof CKEDITOR.editable ? element : new CKEDITOR.editable( this, element ) ) : | ||
1206 | // Detach the editable from editor. | ||
1207 | ( editable && editable.detach(), null ); | ||
1208 | } | ||
1209 | |||
1210 | // Just retrieve the editable. | ||
1211 | return editable; | ||
1212 | }; | ||
1213 | |||
1214 | CKEDITOR.on( 'instanceLoaded', function( evt ) { | ||
1215 | var editor = evt.editor; | ||
1216 | |||
1217 | // and flag that the element was locked by our code so it'll be editable by the editor functions (#6046). | ||
1218 | editor.on( 'insertElement', function( evt ) { | ||
1219 | var element = evt.data; | ||
1220 | if ( element.type == CKEDITOR.NODE_ELEMENT && ( element.is( 'input' ) || element.is( 'textarea' ) ) ) { | ||
1221 | // // The element is still not inserted yet, force attribute-based check. | ||
1222 | if ( element.getAttribute( 'contentEditable' ) != 'false' ) | ||
1223 | element.data( 'cke-editable', element.hasAttribute( 'contenteditable' ) ? 'true' : '1' ); | ||
1224 | element.setAttribute( 'contentEditable', false ); | ||
1225 | } | ||
1226 | } ); | ||
1227 | |||
1228 | editor.on( 'selectionChange', function( evt ) { | ||
1229 | if ( editor.readOnly ) | ||
1230 | return; | ||
1231 | |||
1232 | // Auto fixing on some document structure weakness to enhance usabilities. (#3190 and #3189) | ||
1233 | var sel = editor.getSelection(); | ||
1234 | // Do it only when selection is not locked. (#8222) | ||
1235 | if ( sel && !sel.isLocked ) { | ||
1236 | var isDirty = editor.checkDirty(); | ||
1237 | |||
1238 | // Lock undoM before touching DOM to prevent | ||
1239 | // recording these changes as separate snapshot. | ||
1240 | editor.fire( 'lockSnapshot' ); | ||
1241 | fixDom( evt ); | ||
1242 | editor.fire( 'unlockSnapshot' ); | ||
1243 | |||
1244 | !isDirty && editor.resetDirty(); | ||
1245 | } | ||
1246 | } ); | ||
1247 | } ); | ||
1248 | |||
1249 | CKEDITOR.on( 'instanceCreated', function( evt ) { | ||
1250 | var editor = evt.editor; | ||
1251 | |||
1252 | editor.on( 'mode', function() { | ||
1253 | |||
1254 | var editable = editor.editable(); | ||
1255 | |||
1256 | // Setup proper ARIA roles and properties for inline editable, classic | ||
1257 | // (iframe-based) editable is instead handled by plugin. | ||
1258 | if ( editable && editable.isInline() ) { | ||
1259 | |||
1260 | var ariaLabel = editor.title; | ||
1261 | |||
1262 | editable.changeAttr( 'role', 'textbox' ); | ||
1263 | editable.changeAttr( 'aria-label', ariaLabel ); | ||
1264 | |||
1265 | if ( ariaLabel ) | ||
1266 | editable.changeAttr( 'title', ariaLabel ); | ||
1267 | |||
1268 | var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label; | ||
1269 | if ( helpLabel ) { | ||
1270 | // Put the voice label in different spaces, depending on element mode, so | ||
1271 | // the DOM element get auto detached on mode reload or editor destroy. | ||
1272 | var ct = this.ui.space( this.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ? 'top' : 'contents' ); | ||
1273 | if ( ct ) { | ||
1274 | var ariaDescId = CKEDITOR.tools.getNextId(), | ||
1275 | desc = CKEDITOR.dom.element.createFromHtml( '<span id="' + ariaDescId + '" class="cke_voice_label">' + helpLabel + '</span>' ); | ||
1276 | ct.append( desc ); | ||
1277 | editable.changeAttr( 'aria-describedby', ariaDescId ); | ||
1278 | } | ||
1279 | } | ||
1280 | } | ||
1281 | } ); | ||
1282 | } ); | ||
1283 | |||
1284 | // #9222: Show text cursor in Gecko. | ||
1285 | // Show default cursor over control elements on all non-IEs. | ||
1286 | CKEDITOR.addCss( '.cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}' ); | ||
1287 | |||
1288 | // | ||
1289 | // | ||
1290 | // Bazillion helpers for the editable class and above listeners. | ||
1291 | // | ||
1292 | // | ||
1293 | |||
1294 | var isNotWhitespace = CKEDITOR.dom.walker.whitespaces( true ), | ||
1295 | isNotBookmark = CKEDITOR.dom.walker.bookmark( false, true ), | ||
1296 | isEmpty = CKEDITOR.dom.walker.empty(), | ||
1297 | isBogus = CKEDITOR.dom.walker.bogus(), | ||
1298 | // Matching an empty paragraph at the end of document. | ||
1299 | emptyParagraphRegexp = /(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>| |\u00A0| )?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi; | ||
1300 | |||
1301 | // Auto-fixing block-less content by wrapping paragraph (#3190), prevent | ||
1302 | // non-exitable-block by padding extra br.(#3189) | ||
1303 | // Returns truly value when dom was changed, falsy otherwise. | ||
1304 | function fixDom( evt ) { | ||
1305 | var editor = evt.editor, | ||
1306 | path = evt.data.path, | ||
1307 | blockLimit = path.blockLimit, | ||
1308 | selection = evt.data.selection, | ||
1309 | range = selection.getRanges()[ 0 ], | ||
1310 | selectionUpdateNeeded; | ||
1311 | |||
1312 | if ( CKEDITOR.env.gecko || ( CKEDITOR.env.ie && CKEDITOR.env.needsBrFiller ) ) { | ||
1313 | var blockNeedsFiller = needsBrFiller( selection, path ); | ||
1314 | if ( blockNeedsFiller ) { | ||
1315 | blockNeedsFiller.appendBogus(); | ||
1316 | // IE tends to place selection after appended bogus, so we need to | ||
1317 | // select the original range (placed before bogus). | ||
1318 | selectionUpdateNeeded = CKEDITOR.env.ie; | ||
1319 | } | ||
1320 | } | ||
1321 | |||
1322 | // When we're in block enter mode, a new paragraph will be established | ||
1323 | // to encapsulate inline contents inside editable. (#3657) | ||
1324 | // Don't autoparagraph if browser (namely - IE) incorrectly anchored selection | ||
1325 | // inside non-editable content. This happens e.g. if non-editable block is the only | ||
1326 | // content of editable. | ||
1327 | if ( shouldAutoParagraph( editor, path.block, blockLimit ) && range.collapsed && !range.getCommonAncestor().isReadOnly() ) { | ||
1328 | var testRng = range.clone(); | ||
1329 | testRng.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS ); | ||
1330 | var walker = new CKEDITOR.dom.walker( testRng ); | ||
1331 | walker.guard = function( node ) { | ||
1332 | return !isNotEmpty( node ) || | ||
1333 | node.type == CKEDITOR.NODE_COMMENT || | ||
1334 | node.isReadOnly(); | ||
1335 | }; | ||
1336 | |||
1337 | // 1. Inline content discovered under cursor; | ||
1338 | // 2. Empty editable. | ||
1339 | if ( !walker.checkForward() || testRng.checkStartOfBlock() && testRng.checkEndOfBlock() ) { | ||
1340 | var fixedBlock = range.fixBlock( true, editor.activeEnterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' ); | ||
1341 | |||
1342 | // For IE<11, we should remove any filler node which was introduced before. | ||
1343 | if ( !CKEDITOR.env.needsBrFiller ) { | ||
1344 | var first = fixedBlock.getFirst( isNotEmpty ); | ||
1345 | if ( first && isNbsp( first ) ) | ||
1346 | first.remove(); | ||
1347 | } | ||
1348 | |||
1349 | selectionUpdateNeeded = 1; | ||
1350 | |||
1351 | // Cancel this selection change in favor of the next (correct). (#6811) | ||
1352 | evt.cancel(); | ||
1353 | } | ||
1354 | } | ||
1355 | |||
1356 | if ( selectionUpdateNeeded ) | ||
1357 | range.select(); | ||
1358 | } | ||
1359 | |||
1360 | // Checks whether current selection requires br filler to be appended. | ||
1361 | // @returns Block which needs filler or falsy value. | ||
1362 | function needsBrFiller( selection, path ) { | ||
1363 | // Fake selection does not need filler, because it is fake. | ||
1364 | if ( selection.isFake ) | ||
1365 | return 0; | ||
1366 | |||
1367 | // Ensure bogus br could help to move cursor (out of styles) to the end of block. (#7041) | ||
1368 | var pathBlock = path.block || path.blockLimit, | ||
1369 | lastNode = pathBlock && pathBlock.getLast( isNotEmpty ); | ||
1370 | |||
1371 | // Check some specialities of the current path block: | ||
1372 | // 1. It is really displayed as block; (#7221) | ||
1373 | // 2. It doesn't end with one inner block; (#7467) | ||
1374 | // 3. It doesn't have bogus br yet. | ||
1375 | if ( | ||
1376 | pathBlock && pathBlock.isBlockBoundary() && | ||
1377 | !( lastNode && lastNode.type == CKEDITOR.NODE_ELEMENT && lastNode.isBlockBoundary() ) && | ||
1378 | !pathBlock.is( 'pre' ) && !pathBlock.getBogus() | ||
1379 | ) | ||
1380 | return pathBlock; | ||
1381 | } | ||
1382 | |||
1383 | function blockInputClick( evt ) { | ||
1384 | var element = evt.data.getTarget(); | ||
1385 | if ( element.is( 'input' ) ) { | ||
1386 | var type = element.getAttribute( 'type' ); | ||
1387 | if ( type == 'submit' || type == 'reset' ) | ||
1388 | evt.data.preventDefault(); | ||
1389 | } | ||
1390 | } | ||
1391 | |||
1392 | function isNotEmpty( node ) { | ||
1393 | return isNotWhitespace( node ) && isNotBookmark( node ); | ||
1394 | } | ||
1395 | |||
1396 | function isNbsp( node ) { | ||
1397 | return node.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( node.getText() ).match( /^(?: |\xa0)$/ ); | ||
1398 | } | ||
1399 | |||
1400 | function isNotBubbling( fn, src ) { | ||
1401 | return function( evt ) { | ||
1402 | var other = evt.data.$.toElement || evt.data.$.fromElement || evt.data.$.relatedTarget; | ||
1403 | |||
1404 | // First of all, other may simply be null/undefined. | ||
1405 | // Second of all, at least early versions of Spartan returned empty objects from evt.relatedTarget, | ||
1406 | // so let's also check the node type. | ||
1407 | other = ( other && other.nodeType == CKEDITOR.NODE_ELEMENT ) ? new CKEDITOR.dom.element( other ) : null; | ||
1408 | |||
1409 | if ( !( other && ( src.equals( other ) || src.contains( other ) ) ) ) | ||
1410 | fn.call( this, evt ); | ||
1411 | }; | ||
1412 | } | ||
1413 | |||
1414 | function hasBookmarks( element ) { | ||
1415 | // We use getElementsByTag() instead of find() to retain compatibility with IE quirks mode. | ||
1416 | var potentialBookmarks = element.getElementsByTag( 'span' ), | ||
1417 | i = 0, | ||
1418 | child; | ||
1419 | |||
1420 | if ( potentialBookmarks ) { | ||
1421 | while ( ( child = potentialBookmarks.getItem( i++ ) ) ) { | ||
1422 | if ( !isNotBookmark( child ) ) { | ||
1423 | return true; | ||
1424 | } | ||
1425 | } | ||
1426 | } | ||
1427 | |||
1428 | return false; | ||
1429 | } | ||
1430 | |||
1431 | // Check if the entire table/list contents is selected. | ||
1432 | function getSelectedTableList( sel ) { | ||
1433 | var selected, | ||
1434 | range = sel.getRanges()[ 0 ], | ||
1435 | editable = sel.root, | ||
1436 | path = range.startPath(), | ||
1437 | structural = { table: 1, ul: 1, ol: 1, dl: 1 }; | ||
1438 | |||
1439 | if ( path.contains( structural ) ) { | ||
1440 | // Clone the original range. | ||
1441 | var walkerRng = range.clone(); | ||
1442 | |||
1443 | // Enlarge the range: X<ul><li>[Y]</li></ul>X => [X<ul><li>]Y</li></ul>X | ||
1444 | walkerRng.collapse( 1 ); | ||
1445 | walkerRng.setStartAt( editable, CKEDITOR.POSITION_AFTER_START ); | ||
1446 | |||
1447 | // Create a new walker. | ||
1448 | var walker = new CKEDITOR.dom.walker( walkerRng ); | ||
1449 | |||
1450 | // Assign a new guard to the walker. | ||
1451 | walker.guard = guard(); | ||
1452 | |||
1453 | // Go backwards checking for selected structural node. | ||
1454 | walker.checkBackward(); | ||
1455 | |||
1456 | // If there's a selected structured element when checking backwards, | ||
1457 | // then check the same forwards. | ||
1458 | if ( selected ) { | ||
1459 | // Clone the original range. | ||
1460 | walkerRng = range.clone(); | ||
1461 | |||
1462 | // Enlarge the range (assuming <ul> is selected element from guard): | ||
1463 | // | ||
1464 | // X<ul><li>[Y]</li></ul>X => X<ul><li>Y[</li></ul>]X | ||
1465 | // | ||
1466 | // If the walker went deeper down DOM than a while ago when traversing | ||
1467 | // backwards, then it doesn't make sense: an element must be selected | ||
1468 | // symmetrically. By placing range end **after previously selected node**, | ||
1469 | // we make sure we don't go no deeper in DOM when going forwards. | ||
1470 | walkerRng.collapse(); | ||
1471 | walkerRng.setEndAt( selected, CKEDITOR.POSITION_AFTER_END ); | ||
1472 | |||
1473 | // Create a new walker. | ||
1474 | walker = new CKEDITOR.dom.walker( walkerRng ); | ||
1475 | |||
1476 | // Assign a new guard to the walker. | ||
1477 | walker.guard = guard( true ); | ||
1478 | |||
1479 | // Reset selected node. | ||
1480 | selected = false; | ||
1481 | |||
1482 | // Go forwards checking for selected structural node. | ||
1483 | walker.checkForward(); | ||
1484 | |||
1485 | return selected; | ||
1486 | } | ||
1487 | } | ||
1488 | |||
1489 | return null; | ||
1490 | |||
1491 | function guard( forwardGuard ) { | ||
1492 | return function( node, isWalkOut ) { | ||
1493 | // Save the encountered node as selected if going down the DOM structure | ||
1494 | // and the node is structured element. | ||
1495 | if ( isWalkOut && node.type == CKEDITOR.NODE_ELEMENT && node.is( structural ) ) | ||
1496 | selected = node; | ||
1497 | |||
1498 | // Stop the walker when either traversing another non-empty node at the same | ||
1499 | // DOM level as in previous step. | ||
1500 | // NOTE: When going forwards, stop if encountered a bogus. | ||
1501 | if ( !isWalkOut && isNotEmpty( node ) && !( forwardGuard && isBogus( node ) ) ) | ||
1502 | return false; | ||
1503 | }; | ||
1504 | } | ||
1505 | } | ||
1506 | |||
1507 | // Whether in given context (pathBlock, pathBlockLimit and editor settings) | ||
1508 | // editor should automatically wrap inline contents with blocks. | ||
1509 | function shouldAutoParagraph( editor, pathBlock, pathBlockLimit ) { | ||
1510 | // Check whether pathBlock equals pathBlockLimit to support nested editable (#12162). | ||
1511 | return editor.config.autoParagraph !== false && | ||
1512 | editor.activeEnterMode != CKEDITOR.ENTER_BR && | ||
1513 | ( | ||
1514 | ( editor.editable().equals( pathBlockLimit ) && !pathBlock ) || | ||
1515 | ( pathBlock && pathBlock.getAttribute( 'contenteditable' ) == 'true' ) | ||
1516 | ); | ||
1517 | } | ||
1518 | |||
1519 | function autoParagraphTag( editor ) { | ||
1520 | return ( editor.activeEnterMode != CKEDITOR.ENTER_BR && editor.config.autoParagraph !== false ) ? editor.activeEnterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' : false; | ||
1521 | } | ||
1522 | |||
1523 | // | ||
1524 | // Functions related to insertXXX methods | ||
1525 | // | ||
1526 | var insert = ( function() { | ||
1527 | 'use strict'; | ||
1528 | |||
1529 | var DTD = CKEDITOR.dtd; | ||
1530 | |||
1531 | // Inserts the given (valid) HTML into the range position (with range content deleted), | ||
1532 | // guarantee it's result to be a valid DOM tree. | ||
1533 | function insert( editable, type, data, range ) { | ||
1534 | var editor = editable.editor, | ||
1535 | dontFilter = false; | ||
1536 | |||
1537 | if ( type == 'unfiltered_html' ) { | ||
1538 | type = 'html'; | ||
1539 | dontFilter = true; | ||
1540 | } | ||
1541 | |||
1542 | // Check range spans in non-editable. | ||
1543 | if ( range.checkReadOnly() ) | ||
1544 | return; | ||
1545 | |||
1546 | // RANGE PREPARATIONS | ||
1547 | |||
1548 | var path = new CKEDITOR.dom.elementPath( range.startContainer, range.root ), | ||
1549 | // Let root be the nearest block that's impossible to be split | ||
1550 | // during html processing. | ||
1551 | blockLimit = path.blockLimit || range.root, | ||
1552 | // The "state" value. | ||
1553 | that = { | ||
1554 | type: type, | ||
1555 | dontFilter: dontFilter, | ||
1556 | editable: editable, | ||
1557 | editor: editor, | ||
1558 | range: range, | ||
1559 | blockLimit: blockLimit, | ||
1560 | // During pre-processing / preparations startContainer of affectedRange should be placed | ||
1561 | // in this element in which inserted or moved (in case when we merge blocks) content | ||
1562 | // could create situation that will need merging inline elements. | ||
1563 | // Examples: | ||
1564 | // <div><b>A</b>^B</div> + <b>C</b> => <div><b>A</b><b>C</b>B</div> - affected container is <div>. | ||
1565 | // <p><b>A[B</b></p><p><b>C]D</b></p> + E => <p><b>AE</b></p><p><b>D</b></p> => | ||
1566 | // <p><b>AE</b><b>D</b></p> - affected container is <p> (in text mode). | ||
1567 | mergeCandidates: [], | ||
1568 | zombies: [] | ||
1569 | }; | ||
1570 | |||
1571 | prepareRangeToDataInsertion( that ); | ||
1572 | |||
1573 | // DATA PROCESSING | ||
1574 | |||
1575 | // Select range and stop execution. | ||
1576 | // If data has been totally emptied after the filtering, | ||
1577 | // any insertion is pointless (#10339). | ||
1578 | if ( data && processDataForInsertion( that, data ) ) { | ||
1579 | // DATA INSERTION | ||
1580 | insertDataIntoRange( that ); | ||
1581 | } | ||
1582 | |||
1583 | // FINAL CLEANUP | ||
1584 | // Set final range position and clean up. | ||
1585 | |||
1586 | cleanupAfterInsertion( that ); | ||
1587 | } | ||
1588 | |||
1589 | // Prepare range to its data deletion. | ||
1590 | // Delete its contents. | ||
1591 | // Prepare it to insertion. | ||
1592 | function prepareRangeToDataInsertion( that ) { | ||
1593 | var range = that.range, | ||
1594 | mergeCandidates = that.mergeCandidates, | ||
1595 | node, marker, path, startPath, endPath, previous, bm; | ||
1596 | |||
1597 | // If range starts in inline element then insert a marker, so empty | ||
1598 | // inline elements won't be removed while range.deleteContents | ||
1599 | // and we will be able to move range back into this element. | ||
1600 | // E.g. 'aa<b>[bb</b>]cc' -> (after deleting) 'aa<b><span/></b>cc' | ||
1601 | if ( that.type == 'text' && range.shrink( CKEDITOR.SHRINK_ELEMENT, true, false ) ) { | ||
1602 | marker = CKEDITOR.dom.element.createFromHtml( '<span> </span>', range.document ); | ||
1603 | range.insertNode( marker ); | ||
1604 | range.setStartAfter( marker ); | ||
1605 | } | ||
1606 | |||
1607 | // By using path we can recover in which element was startContainer | ||
1608 | // before deleting contents. | ||
1609 | // Start and endPathElements will be used to squash selected blocks, after removing | ||
1610 | // selection contents. See rule 5. | ||
1611 | startPath = new CKEDITOR.dom.elementPath( range.startContainer ); | ||
1612 | that.endPath = endPath = new CKEDITOR.dom.elementPath( range.endContainer ); | ||
1613 | |||
1614 | if ( !range.collapsed ) { | ||
1615 | // Anticipate the possibly empty block at the end of range after deletion. | ||
1616 | node = endPath.block || endPath.blockLimit; | ||
1617 | var ancestor = range.getCommonAncestor(); | ||
1618 | if ( node && !( node.equals( ancestor ) || node.contains( ancestor ) ) && range.checkEndOfBlock() ) { | ||
1619 | that.zombies.push( node ); | ||
1620 | } | ||
1621 | |||
1622 | range.deleteContents(); | ||
1623 | } | ||
1624 | |||
1625 | // Rule 4. | ||
1626 | // Move range into the previous block. | ||
1627 | while ( | ||
1628 | ( previous = getRangePrevious( range ) ) && checkIfElement( previous ) && previous.isBlockBoundary() && | ||
1629 | // Check if previousNode was parent of range's startContainer before deleteContents. | ||
1630 | startPath.contains( previous ) | ||
1631 | ) | ||
1632 | range.moveToPosition( previous, CKEDITOR.POSITION_BEFORE_END ); | ||
1633 | |||
1634 | // Rule 5. | ||
1635 | mergeAncestorElementsOfSelectionEnds( range, that.blockLimit, startPath, endPath ); | ||
1636 | |||
1637 | // Rule 1. | ||
1638 | if ( marker ) { | ||
1639 | // If marker was created then move collapsed range into its place. | ||
1640 | range.setEndBefore( marker ); | ||
1641 | range.collapse(); | ||
1642 | marker.remove(); | ||
1643 | } | ||
1644 | |||
1645 | // Split inline elements so HTML will be inserted with its own styles. | ||
1646 | path = range.startPath(); | ||
1647 | if ( ( node = path.contains( isInline, false, 1 ) ) ) { | ||
1648 | range.splitElement( node ); | ||
1649 | that.inlineStylesRoot = node; | ||
1650 | that.inlineStylesPeak = path.lastElement; | ||
1651 | } | ||
1652 | |||
1653 | // Record inline merging candidates for later cleanup in place. | ||
1654 | bm = range.createBookmark(); | ||
1655 | |||
1656 | // 1. Inline siblings. | ||
1657 | node = bm.startNode.getPrevious( isNotEmpty ); | ||
1658 | node && checkIfElement( node ) && isInline( node ) && mergeCandidates.push( node ); | ||
1659 | node = bm.startNode.getNext( isNotEmpty ); | ||
1660 | node && checkIfElement( node ) && isInline( node ) && mergeCandidates.push( node ); | ||
1661 | |||
1662 | // 2. Inline parents. | ||
1663 | node = bm.startNode; | ||
1664 | while ( ( node = node.getParent() ) && isInline( node ) ) | ||
1665 | mergeCandidates.push( node ); | ||
1666 | |||
1667 | range.moveToBookmark( bm ); | ||
1668 | } | ||
1669 | |||
1670 | function processDataForInsertion( that, data ) { | ||
1671 | var range = that.range; | ||
1672 | |||
1673 | // Rule 8. - wrap entire data in inline styles. | ||
1674 | // (e.g. <p><b>x^z</b></p> + <p>a</p><p>b</p> -> <b><p>a</p><p>b</p></b>) | ||
1675 | // Incorrect tags order will be fixed by htmlDataProcessor. | ||
1676 | if ( that.type == 'text' && that.inlineStylesRoot ) | ||
1677 | data = wrapDataWithInlineStyles( data, that ); | ||
1678 | |||
1679 | |||
1680 | var context = that.blockLimit.getName(); | ||
1681 | |||
1682 | // Wrap data to be inserted, to avoid losing leading whitespaces | ||
1683 | // when going through the below procedure. | ||
1684 | if ( /^\s+|\s+$/.test( data ) && 'span' in CKEDITOR.dtd[ context ] ) { | ||
1685 | var protect = '<span data-cke-marker="1"> </span>'; | ||
1686 | data = protect + data + protect; | ||
1687 | } | ||
1688 | |||
1689 | // Process the inserted html, in context of the insertion root. | ||
1690 | // Don't use the "fix for body" feature as auto paragraphing must | ||
1691 | // be handled during insertion. | ||
1692 | data = that.editor.dataProcessor.toHtml( data, { | ||
1693 | context: null, | ||
1694 | fixForBody: false, | ||
1695 | protectedWhitespaces: !!protect, | ||
1696 | dontFilter: that.dontFilter, | ||
1697 | // Use the current, contextual settings. | ||
1698 | filter: that.editor.activeFilter, | ||
1699 | enterMode: that.editor.activeEnterMode | ||
1700 | } ); | ||
1701 | |||
1702 | |||
1703 | // Build the node list for insertion. | ||
1704 | var doc = range.document, | ||
1705 | wrapper = doc.createElement( 'body' ); | ||
1706 | |||
1707 | wrapper.setHtml( data ); | ||
1708 | |||
1709 | // Eventually remove the temporaries. | ||
1710 | if ( protect ) { | ||
1711 | wrapper.getFirst().remove(); | ||
1712 | wrapper.getLast().remove(); | ||
1713 | } | ||
1714 | |||
1715 | // Rule 7. | ||
1716 | var block = range.startPath().block; | ||
1717 | if ( block && // Apply when there exists path block after deleting selection's content... | ||
1718 | !( block.getChildCount() == 1 && block.getBogus() ) ) { // ... and the only content of this block isn't a bogus. | ||
1719 | stripBlockTagIfSingleLine( wrapper ); | ||
1720 | } | ||
1721 | |||
1722 | that.dataWrapper = wrapper; | ||
1723 | |||
1724 | return data; | ||
1725 | } | ||
1726 | |||
1727 | function insertDataIntoRange( that ) { | ||
1728 | var range = that.range, | ||
1729 | doc = range.document, | ||
1730 | path, | ||
1731 | blockLimit = that.blockLimit, | ||
1732 | nodesData, nodeData, node, | ||
1733 | nodeIndex = 0, | ||
1734 | bogus, | ||
1735 | bogusNeededBlocks = [], | ||
1736 | pathBlock, fixBlock, | ||
1737 | splittingContainer = 0, | ||
1738 | dontMoveCaret = 0, | ||
1739 | insertionContainer, toSplit, newContainer, | ||
1740 | startContainer = range.startContainer, | ||
1741 | endContainer = that.endPath.elements[ 0 ], | ||
1742 | filteredNodes, | ||
1743 | // If endContainer was merged into startContainer: <p>a[b</p><p>c]d</p> | ||
1744 | // or it's equal to startContainer: <p>a^b</p> | ||
1745 | // or different situation happened :P | ||
1746 | // then there's no separate container for the end of selection. | ||
1747 | pos = endContainer.getPosition( startContainer ), | ||
1748 | separateEndContainer = !!endContainer.getCommonAncestor( startContainer ) && // endC is not detached. | ||
1749 | pos != CKEDITOR.POSITION_IDENTICAL && !( pos & CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_IS_CONTAINED ); // endC & endS are in separate branches. | ||
1750 | |||
1751 | nodesData = extractNodesData( that.dataWrapper, that ); | ||
1752 | |||
1753 | removeBrsAdjacentToPastedBlocks( nodesData, range ); | ||
1754 | |||
1755 | for ( ; nodeIndex < nodesData.length; nodeIndex++ ) { | ||
1756 | nodeData = nodesData[ nodeIndex ]; | ||
1757 | |||
1758 | // Ignore trailing <brs> | ||
1759 | if ( nodeData.isLineBreak && splitOnLineBreak( range, blockLimit, nodeData ) ) { | ||
1760 | // Do not move caret towards the text (in cleanupAfterInsertion), | ||
1761 | // because caret was placed after a line break. | ||
1762 | dontMoveCaret = nodeIndex > 0; | ||
1763 | continue; | ||
1764 | } | ||
1765 | |||
1766 | path = range.startPath(); | ||
1767 | |||
1768 | // Auto paragraphing. | ||
1769 | if ( !nodeData.isBlock && shouldAutoParagraph( that.editor, path.block, path.blockLimit ) && ( fixBlock = autoParagraphTag( that.editor ) ) ) { | ||
1770 | fixBlock = doc.createElement( fixBlock ); | ||
1771 | fixBlock.appendBogus(); | ||
1772 | range.insertNode( fixBlock ); | ||
1773 | if ( CKEDITOR.env.needsBrFiller && ( bogus = fixBlock.getBogus() ) ) | ||
1774 | bogus.remove(); | ||
1775 | range.moveToPosition( fixBlock, CKEDITOR.POSITION_BEFORE_END ); | ||
1776 | } | ||
1777 | |||
1778 | node = range.startPath().block; | ||
1779 | |||
1780 | // Remove any bogus element on the current path block for now, and mark | ||
1781 | // it for later compensation. | ||
1782 | if ( node && !node.equals( pathBlock ) ) { | ||
1783 | bogus = node.getBogus(); | ||
1784 | if ( bogus ) { | ||
1785 | bogus.remove(); | ||
1786 | bogusNeededBlocks.push( node ); | ||
1787 | } | ||
1788 | |||
1789 | pathBlock = node; | ||
1790 | } | ||
1791 | |||
1792 | // First not allowed node reached - start splitting original container | ||
1793 | if ( nodeData.firstNotAllowed ) | ||
1794 | splittingContainer = 1; | ||
1795 | |||
1796 | if ( splittingContainer && nodeData.isElement ) { | ||
1797 | insertionContainer = range.startContainer; | ||
1798 | toSplit = null; | ||
1799 | |||
1800 | // Find the first ancestor that can contain current node. | ||
1801 | // This one won't be split. | ||
1802 | while ( insertionContainer && !DTD[ insertionContainer.getName() ][ nodeData.name ] ) { | ||
1803 | if ( insertionContainer.equals( blockLimit ) ) { | ||
1804 | insertionContainer = null; | ||
1805 | break; | ||
1806 | } | ||
1807 | |||
1808 | toSplit = insertionContainer; | ||
1809 | insertionContainer = insertionContainer.getParent(); | ||
1810 | } | ||
1811 | |||
1812 | // If split has to be done - do it and mark both ends as a possible zombies. | ||
1813 | if ( insertionContainer ) { | ||
1814 | if ( toSplit ) { | ||
1815 | newContainer = range.splitElement( toSplit ); | ||
1816 | that.zombies.push( newContainer ); | ||
1817 | that.zombies.push( toSplit ); | ||
1818 | } | ||
1819 | } | ||
1820 | // Unable to make the insertion happen in place, resort to the content filter. | ||
1821 | else { | ||
1822 | // If everything worked fine insertionContainer == blockLimit here. | ||
1823 | filteredNodes = filterElement( nodeData.node, blockLimit.getName(), !nodeIndex, nodeIndex == nodesData.length - 1 ); | ||
1824 | } | ||
1825 | } | ||
1826 | |||
1827 | if ( filteredNodes ) { | ||
1828 | while ( ( node = filteredNodes.pop() ) ) | ||
1829 | range.insertNode( node ); | ||
1830 | filteredNodes = 0; | ||
1831 | } else { | ||
1832 | // Insert current node at the start of range. | ||
1833 | range.insertNode( nodeData.node ); | ||
1834 | } | ||
1835 | |||
1836 | // Move range to the endContainer for the final allowed elements. | ||
1837 | if ( nodeData.lastNotAllowed && nodeIndex < nodesData.length - 1 ) { | ||
1838 | // If separateEndContainer exists move range there. | ||
1839 | // Otherwise try to move range to container created during splitting. | ||
1840 | // If this doesn't work - don't move range. | ||
1841 | newContainer = separateEndContainer ? endContainer : newContainer; | ||
1842 | newContainer && range.setEndAt( newContainer, CKEDITOR.POSITION_AFTER_START ); | ||
1843 | splittingContainer = 0; | ||
1844 | } | ||
1845 | |||
1846 | // Collapse range after insertion to end. | ||
1847 | range.collapse(); | ||
1848 | } | ||
1849 | |||
1850 | // Rule 9. Non-editable content should be selected as a whole. | ||
1851 | if ( isSingleNonEditableElement( nodesData ) ) { | ||
1852 | dontMoveCaret = true; | ||
1853 | node = nodesData[ 0 ].node; | ||
1854 | range.setStartAt( node, CKEDITOR.POSITION_BEFORE_START ); | ||
1855 | range.setEndAt( node, CKEDITOR.POSITION_AFTER_END ); | ||
1856 | } | ||
1857 | |||
1858 | that.dontMoveCaret = dontMoveCaret; | ||
1859 | that.bogusNeededBlocks = bogusNeededBlocks; | ||
1860 | } | ||
1861 | |||
1862 | function cleanupAfterInsertion( that ) { | ||
1863 | var range = that.range, | ||
1864 | node, testRange, movedIntoInline, | ||
1865 | bogusNeededBlocks = that.bogusNeededBlocks, | ||
1866 | // Create a bookmark to defend against the following range deconstructing operations. | ||
1867 | bm = range.createBookmark(); | ||
1868 | |||
1869 | // Remove all elements that could be created while splitting nodes | ||
1870 | // with ranges at its start|end. | ||
1871 | // E.g. remove <div><p></p></div> | ||
1872 | // But not <div><p> </p></div> | ||
1873 | // And replace <div><p><span data="cke-bookmark"/></p></div> with found bookmark. | ||
1874 | while ( ( node = that.zombies.pop() ) ) { | ||
1875 | // Detached element. | ||
1876 | if ( !node.getParent() ) | ||
1877 | continue; | ||
1878 | |||
1879 | testRange = range.clone(); | ||
1880 | testRange.moveToElementEditStart( node ); | ||
1881 | testRange.removeEmptyBlocksAtEnd(); | ||
1882 | } | ||
1883 | |||
1884 | if ( bogusNeededBlocks ) { | ||
1885 | // Bring back all block bogus nodes. | ||
1886 | while ( ( node = bogusNeededBlocks.pop() ) ) { | ||
1887 | if ( CKEDITOR.env.needsBrFiller ) | ||
1888 | node.appendBogus(); | ||
1889 | else | ||
1890 | node.append( range.document.createText( '\u00a0' ) ); | ||
1891 | } | ||
1892 | } | ||
1893 | |||
1894 | // Eventually merge identical inline elements. | ||
1895 | while ( ( node = that.mergeCandidates.pop() ) ) | ||
1896 | node.mergeSiblings(); | ||
1897 | |||
1898 | range.moveToBookmark( bm ); | ||
1899 | |||
1900 | // Rule 3. | ||
1901 | // Shrink range to the BEFOREEND of previous innermost editable node in source order. | ||
1902 | |||
1903 | if ( !that.dontMoveCaret ) { | ||
1904 | node = getRangePrevious( range ); | ||
1905 | |||
1906 | while ( node && checkIfElement( node ) && !node.is( DTD.$empty ) ) { | ||
1907 | if ( node.isBlockBoundary() ) | ||
1908 | range.moveToPosition( node, CKEDITOR.POSITION_BEFORE_END ); | ||
1909 | else { | ||
1910 | // Don't move into inline element (which ends with a text node) | ||
1911 | // found which contains white-space at its end. | ||
1912 | // If not - move range's end to the end of this element. | ||
1913 | if ( isInline( node ) && node.getHtml().match( /(\s| )$/g ) ) { | ||
1914 | movedIntoInline = null; | ||
1915 | break; | ||
1916 | } | ||
1917 | |||
1918 | movedIntoInline = range.clone(); | ||
1919 | movedIntoInline.moveToPosition( node, CKEDITOR.POSITION_BEFORE_END ); | ||
1920 | } | ||
1921 | |||
1922 | node = node.getLast( isNotEmpty ); | ||
1923 | } | ||
1924 | |||
1925 | movedIntoInline && range.moveToRange( movedIntoInline ); | ||
1926 | } | ||
1927 | |||
1928 | } | ||
1929 | |||
1930 | // | ||
1931 | // HELPERS ------------------------------------------------------------ | ||
1932 | // | ||
1933 | |||
1934 | function checkIfElement( node ) { | ||
1935 | return node.type == CKEDITOR.NODE_ELEMENT; | ||
1936 | } | ||
1937 | |||
1938 | function extractNodesData( dataWrapper, that ) { | ||
1939 | var node, sibling, nodeName, allowed, | ||
1940 | nodesData = [], | ||
1941 | startContainer = that.range.startContainer, | ||
1942 | path = that.range.startPath(), | ||
1943 | allowedNames = DTD[ startContainer.getName() ], | ||
1944 | nodeIndex = 0, | ||
1945 | nodesList = dataWrapper.getChildren(), | ||
1946 | nodesCount = nodesList.count(), | ||
1947 | firstNotAllowed = -1, | ||
1948 | lastNotAllowed = -1, | ||
1949 | lineBreak = 0, | ||
1950 | blockSibling; | ||
1951 | |||
1952 | // Selection start within a list. | ||
1953 | var insideOfList = path.contains( DTD.$list ); | ||
1954 | |||
1955 | for ( ; nodeIndex < nodesCount; ++nodeIndex ) { | ||
1956 | node = nodesList.getItem( nodeIndex ); | ||
1957 | |||
1958 | if ( checkIfElement( node ) ) { | ||
1959 | nodeName = node.getName(); | ||
1960 | |||
1961 | // Extract only the list items, when insertion happens | ||
1962 | // inside of a list, reads as rearrange list items. (#7957) | ||
1963 | if ( insideOfList && nodeName in CKEDITOR.dtd.$list ) { | ||
1964 | nodesData = nodesData.concat( extractNodesData( node, that ) ); | ||
1965 | continue; | ||
1966 | } | ||
1967 | |||
1968 | allowed = !!allowedNames[ nodeName ]; | ||
1969 | |||
1970 | // Mark <brs data-cke-eol="1"> at the beginning and at the end. | ||
1971 | if ( nodeName == 'br' && node.data( 'cke-eol' ) && ( !nodeIndex || nodeIndex == nodesCount - 1 ) ) { | ||
1972 | sibling = nodeIndex ? nodesData[ nodeIndex - 1 ].node : nodesList.getItem( nodeIndex + 1 ); | ||
1973 | |||
1974 | // Line break has to have sibling which is not an <br>. | ||
1975 | lineBreak = sibling && ( !checkIfElement( sibling ) || !sibling.is( 'br' ) ); | ||
1976 | // Line break has block element as a sibling. | ||
1977 | blockSibling = sibling && checkIfElement( sibling ) && DTD.$block[ sibling.getName() ]; | ||
1978 | } | ||
1979 | |||
1980 | if ( firstNotAllowed == -1 && !allowed ) | ||
1981 | firstNotAllowed = nodeIndex; | ||
1982 | if ( !allowed ) | ||
1983 | lastNotAllowed = nodeIndex; | ||
1984 | |||
1985 | nodesData.push( { | ||
1986 | isElement: 1, | ||
1987 | isLineBreak: lineBreak, | ||
1988 | isBlock: node.isBlockBoundary(), | ||
1989 | hasBlockSibling: blockSibling, | ||
1990 | node: node, | ||
1991 | name: nodeName, | ||
1992 | allowed: allowed | ||
1993 | } ); | ||
1994 | |||
1995 | lineBreak = 0; | ||
1996 | blockSibling = 0; | ||
1997 | } else { | ||
1998 | nodesData.push( { isElement: 0, node: node, allowed: 1 } ); | ||
1999 | } | ||
2000 | } | ||
2001 | |||
2002 | // Mark first node that cannot be inserted directly into startContainer | ||
2003 | // and last node for which startContainer has to be split. | ||
2004 | if ( firstNotAllowed > -1 ) | ||
2005 | nodesData[ firstNotAllowed ].firstNotAllowed = 1; | ||
2006 | if ( lastNotAllowed > -1 ) | ||
2007 | nodesData[ lastNotAllowed ].lastNotAllowed = 1; | ||
2008 | |||
2009 | return nodesData; | ||
2010 | } | ||
2011 | |||
2012 | // TODO: Review content transformation rules on filtering element. | ||
2013 | function filterElement( element, parentName, isFirst, isLast ) { | ||
2014 | var nodes = filterElementInner( element, parentName ), | ||
2015 | nodes2 = [], | ||
2016 | nodesCount = nodes.length, | ||
2017 | nodeIndex = 0, | ||
2018 | node, | ||
2019 | afterSpace = 0, | ||
2020 | lastSpaceIndex = -1; | ||
2021 | |||
2022 | // Remove duplicated spaces and spaces at the: | ||
2023 | // * beginnig if filtered element isFirst (isFirst that's going to be inserted) | ||
2024 | // * end if filtered element isLast. | ||
2025 | for ( ; nodeIndex < nodesCount; nodeIndex++ ) { | ||
2026 | node = nodes[ nodeIndex ]; | ||
2027 | |||
2028 | if ( node == ' ' ) { | ||
2029 | // Don't push doubled space and if it's leading space for insertion. | ||
2030 | if ( !afterSpace && !( isFirst && !nodeIndex ) ) { | ||
2031 | nodes2.push( new CKEDITOR.dom.text( ' ' ) ); | ||
2032 | lastSpaceIndex = nodes2.length; | ||
2033 | } | ||
2034 | afterSpace = 1; | ||
2035 | } else { | ||
2036 | nodes2.push( node ); | ||
2037 | afterSpace = 0; | ||
2038 | } | ||
2039 | } | ||
2040 | |||
2041 | // Remove trailing space. | ||
2042 | if ( isLast && lastSpaceIndex == nodes2.length ) | ||
2043 | nodes2.pop(); | ||
2044 | |||
2045 | return nodes2; | ||
2046 | } | ||
2047 | |||
2048 | function filterElementInner( element, parentName ) { | ||
2049 | var nodes = [], | ||
2050 | children = element.getChildren(), | ||
2051 | childrenCount = children.count(), | ||
2052 | child, | ||
2053 | childIndex = 0, | ||
2054 | allowedNames = DTD[ parentName ], | ||
2055 | surroundBySpaces = !element.is( DTD.$inline ) || element.is( 'br' ); | ||
2056 | |||
2057 | if ( surroundBySpaces ) | ||
2058 | nodes.push( ' ' ); | ||
2059 | |||
2060 | for ( ; childIndex < childrenCount; childIndex++ ) { | ||
2061 | child = children.getItem( childIndex ); | ||
2062 | |||
2063 | if ( checkIfElement( child ) && !child.is( allowedNames ) ) | ||
2064 | nodes = nodes.concat( filterElementInner( child, parentName ) ); | ||
2065 | else | ||
2066 | nodes.push( child ); | ||
2067 | } | ||
2068 | |||
2069 | if ( surroundBySpaces ) | ||
2070 | nodes.push( ' ' ); | ||
2071 | |||
2072 | return nodes; | ||
2073 | } | ||
2074 | |||
2075 | function getRangePrevious( range ) { | ||
2076 | return checkIfElement( range.startContainer ) && range.startContainer.getChild( range.startOffset - 1 ); | ||
2077 | } | ||
2078 | |||
2079 | function isInline( node ) { | ||
2080 | return node && checkIfElement( node ) && ( node.is( DTD.$removeEmpty ) || node.is( 'a' ) && !node.isBlockBoundary() ); | ||
2081 | } | ||
2082 | |||
2083 | // Checks if only non-editable element is being inserted. | ||
2084 | function isSingleNonEditableElement( nodesData ) { | ||
2085 | if ( nodesData.length != 1 ) | ||
2086 | return false; | ||
2087 | |||
2088 | var nodeData = nodesData[ 0 ]; | ||
2089 | |||
2090 | return nodeData.isElement && ( nodeData.node.getAttribute( 'contenteditable' ) == 'false' ); | ||
2091 | } | ||
2092 | |||
2093 | var blockMergedTags = { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, li: 1, pre: 1, dl: 1, blockquote: 1 }; | ||
2094 | |||
2095 | // See rule 5. in TCs. | ||
2096 | // Initial situation: | ||
2097 | // <ul><li>AA^</li></ul><ul><li>BB</li></ul> | ||
2098 | // We're looking for 2nd <ul>, comparing with 1st <ul> and merging. | ||
2099 | // We're not merging if caret is between these elements. | ||
2100 | function mergeAncestorElementsOfSelectionEnds( range, blockLimit, startPath, endPath ) { | ||
2101 | var walkerRange = range.clone(), | ||
2102 | walker, nextNode, previousNode; | ||
2103 | |||
2104 | walkerRange.setEndAt( blockLimit, CKEDITOR.POSITION_BEFORE_END ); | ||
2105 | walker = new CKEDITOR.dom.walker( walkerRange ); | ||
2106 | |||
2107 | if ( ( nextNode = walker.next() ) && // Find next source node | ||
2108 | checkIfElement( nextNode ) && // which is an element | ||
2109 | blockMergedTags[ nextNode.getName() ] && // that can be merged. | ||
2110 | ( previousNode = nextNode.getPrevious() ) && // Take previous one | ||
2111 | checkIfElement( previousNode ) && // which also has to be an element. | ||
2112 | !previousNode.getParent().equals( range.startContainer ) && // Fail if caret is on the same level. | ||
2113 | // This means that caret is between these nodes. | ||
2114 | startPath.contains( previousNode ) && // Elements path of start of selection has | ||
2115 | endPath.contains( nextNode ) && // to contain prevNode and vice versa. | ||
2116 | nextNode.isIdentical( previousNode ) // Check if elements are identical. | ||
2117 | ) { | ||
2118 | // Merge blocks and repeat. | ||
2119 | nextNode.moveChildren( previousNode ); | ||
2120 | nextNode.remove(); | ||
2121 | mergeAncestorElementsOfSelectionEnds( range, blockLimit, startPath, endPath ); | ||
2122 | } | ||
2123 | } | ||
2124 | |||
2125 | // If last node that will be inserted is a block (but not a <br>) | ||
2126 | // and it will be inserted right before <br> remove this <br>. | ||
2127 | // Do the same for the first element that will be inserted and preceding <br>. | ||
2128 | function removeBrsAdjacentToPastedBlocks( nodesData, range ) { | ||
2129 | var succeedingNode = range.endContainer.getChild( range.endOffset ), | ||
2130 | precedingNode = range.endContainer.getChild( range.endOffset - 1 ); | ||
2131 | |||
2132 | if ( succeedingNode ) | ||
2133 | remove( succeedingNode, nodesData[ nodesData.length - 1 ] ); | ||
2134 | |||
2135 | if ( precedingNode && remove( precedingNode, nodesData[ 0 ] ) ) { | ||
2136 | // If preceding <br> was removed - move range left. | ||
2137 | range.setEnd( range.endContainer, range.endOffset - 1 ); | ||
2138 | range.collapse(); | ||
2139 | } | ||
2140 | |||
2141 | function remove( maybeBr, maybeBlockData ) { | ||
2142 | if ( maybeBlockData.isBlock && maybeBlockData.isElement && !maybeBlockData.node.is( 'br' ) && | ||
2143 | checkIfElement( maybeBr ) && maybeBr.is( 'br' ) ) { | ||
2144 | maybeBr.remove(); | ||
2145 | return 1; | ||
2146 | } | ||
2147 | } | ||
2148 | } | ||
2149 | |||
2150 | // Return 1 if <br> should be skipped when inserting, 0 otherwise. | ||
2151 | function splitOnLineBreak( range, blockLimit, nodeData ) { | ||
2152 | var firstBlockAscendant, pos; | ||
2153 | |||
2154 | if ( nodeData.hasBlockSibling ) | ||
2155 | return 1; | ||
2156 | |||
2157 | firstBlockAscendant = range.startContainer.getAscendant( DTD.$block, 1 ); | ||
2158 | if ( !firstBlockAscendant || !firstBlockAscendant.is( { div: 1, p: 1 } ) ) | ||
2159 | return 0; | ||
2160 | |||
2161 | pos = firstBlockAscendant.getPosition( blockLimit ); | ||
2162 | |||
2163 | if ( pos == CKEDITOR.POSITION_IDENTICAL || pos == CKEDITOR.POSITION_CONTAINS ) | ||
2164 | return 0; | ||
2165 | |||
2166 | var newContainer = range.splitElement( firstBlockAscendant ); | ||
2167 | range.moveToPosition( newContainer, CKEDITOR.POSITION_AFTER_START ); | ||
2168 | |||
2169 | return 1; | ||
2170 | } | ||
2171 | |||
2172 | var stripSingleBlockTags = { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1 }, | ||
2173 | inlineButNotBr = CKEDITOR.tools.extend( {}, DTD.$inline ); | ||
2174 | delete inlineButNotBr.br; | ||
2175 | |||
2176 | // Rule 7. | ||
2177 | function stripBlockTagIfSingleLine( dataWrapper ) { | ||
2178 | var block, children; | ||
2179 | |||
2180 | if ( dataWrapper.getChildCount() == 1 && // Only one node bein inserted. | ||
2181 | checkIfElement( block = dataWrapper.getFirst() ) && // And it's an element. | ||
2182 | block.is( stripSingleBlockTags ) && // That's <p> or <div> or header. | ||
2183 | !block.hasAttribute( 'contenteditable' ) // It's not a non-editable block or nested editable. | ||
2184 | ) { | ||
2185 | // Check children not containing block. | ||
2186 | children = block.getElementsByTag( '*' ); | ||
2187 | for ( var i = 0, child, count = children.count(); i < count; i++ ) { | ||
2188 | child = children.getItem( i ); | ||
2189 | if ( !child.is( inlineButNotBr ) ) | ||
2190 | return; | ||
2191 | } | ||
2192 | |||
2193 | block.moveChildren( block.getParent( 1 ) ); | ||
2194 | block.remove(); | ||
2195 | } | ||
2196 | } | ||
2197 | |||
2198 | function wrapDataWithInlineStyles( data, that ) { | ||
2199 | var element = that.inlineStylesPeak, | ||
2200 | doc = element.getDocument(), | ||
2201 | wrapper = doc.createText( '{cke-peak}' ), | ||
2202 | limit = that.inlineStylesRoot.getParent(); | ||
2203 | |||
2204 | while ( !element.equals( limit ) ) { | ||
2205 | wrapper = wrapper.appendTo( element.clone() ); | ||
2206 | element = element.getParent(); | ||
2207 | } | ||
2208 | |||
2209 | // Don't use String.replace because it fails in IE7 if special replacement | ||
2210 | // characters ($$, $&, etc.) are in data (#10367). | ||
2211 | return wrapper.getOuterHtml().split( '{cke-peak}' ).join( data ); | ||
2212 | } | ||
2213 | |||
2214 | return insert; | ||
2215 | } )(); | ||
2216 | |||
2217 | function afterInsert( editable ) { | ||
2218 | var editor = editable.editor; | ||
2219 | |||
2220 | // Scroll using selection, not ranges, to affect native pastes. | ||
2221 | editor.getSelection().scrollIntoView(); | ||
2222 | |||
2223 | // Save snaps after the whole execution completed. | ||
2224 | // This's a workaround for make DOM modification's happened after | ||
2225 | // 'insertElement' to be included either, e.g. Form-based dialogs' 'commitContents' | ||
2226 | // call. | ||
2227 | setTimeout( function() { | ||
2228 | editor.fire( 'saveSnapshot' ); | ||
2229 | }, 0 ); | ||
2230 | } | ||
2231 | |||
2232 | // 1. Fixes a range which is a result of deleteContents() and is placed in an intermediate element (see dtd.$intermediate), | ||
2233 | // inside a table. A goal is to find a closest <td> or <th> element and when this fails, recreate the structure of the table. | ||
2234 | // 2. Fixes empty cells by appending bogus <br>s or deleting empty text nodes in IE<=8 case. | ||
2235 | var fixTableAfterContentsDeletion = ( function() { | ||
2236 | // Creates an element walker which can only "go deeper". It won't | ||
2237 | // move out from any element. Therefore it can be used to find <td>x</td> in cases like: | ||
2238 | // <table><tbody><tr><td>x</td></tr></tbody>^<tfoot>... | ||
2239 | function getFixTableSelectionWalker( testRange ) { | ||
2240 | var walker = new CKEDITOR.dom.walker( testRange ); | ||
2241 | walker.guard = function( node, isMovingOut ) { | ||
2242 | if ( isMovingOut ) | ||
2243 | return false; | ||
2244 | if ( node.type == CKEDITOR.NODE_ELEMENT ) | ||
2245 | return node.is( CKEDITOR.dtd.$tableContent ); | ||
2246 | }; | ||
2247 | walker.evaluator = function( node ) { | ||
2248 | return node.type == CKEDITOR.NODE_ELEMENT; | ||
2249 | }; | ||
2250 | |||
2251 | return walker; | ||
2252 | } | ||
2253 | |||
2254 | function fixTableStructure( element, newElementName, appendToStart ) { | ||
2255 | var temp = element.getDocument().createElement( newElementName ); | ||
2256 | element.append( temp, appendToStart ); | ||
2257 | return temp; | ||
2258 | } | ||
2259 | |||
2260 | // Fix empty cells. This means: | ||
2261 | // * add bogus <br> if browser needs it | ||
2262 | // * remove empty text nodes on IE8, because it will crash (http://dev.ckeditor.com/ticket/11183#comment:8). | ||
2263 | function fixEmptyCells( cells ) { | ||
2264 | var i = cells.count(), | ||
2265 | cell; | ||
2266 | |||
2267 | for ( i; i-- > 0; ) { | ||
2268 | cell = cells.getItem( i ); | ||
2269 | |||
2270 | if ( !CKEDITOR.tools.trim( cell.getHtml() ) ) { | ||
2271 | cell.appendBogus(); | ||
2272 | if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && cell.getChildCount() ) | ||
2273 | cell.getFirst().remove(); | ||
2274 | } | ||
2275 | } | ||
2276 | } | ||
2277 | |||
2278 | return function( range ) { | ||
2279 | var container = range.startContainer, | ||
2280 | table = container.getAscendant( 'table', 1 ), | ||
2281 | testRange, | ||
2282 | deeperSibling, | ||
2283 | appendToStart = false; | ||
2284 | |||
2285 | fixEmptyCells( table.getElementsByTag( 'td' ) ); | ||
2286 | fixEmptyCells( table.getElementsByTag( 'th' ) ); | ||
2287 | |||
2288 | // Look left. | ||
2289 | testRange = range.clone(); | ||
2290 | testRange.setStart( container, 0 ); | ||
2291 | deeperSibling = getFixTableSelectionWalker( testRange ).lastBackward(); | ||
2292 | |||
2293 | // If left is empty, look right. | ||
2294 | if ( !deeperSibling ) { | ||
2295 | testRange = range.clone(); | ||
2296 | testRange.setEndAt( container, CKEDITOR.POSITION_BEFORE_END ); | ||
2297 | deeperSibling = getFixTableSelectionWalker( testRange ).lastForward(); | ||
2298 | appendToStart = true; | ||
2299 | } | ||
2300 | |||
2301 | // If there's no deeper nested element in both direction - container is empty - we'll use it then. | ||
2302 | if ( !deeperSibling ) | ||
2303 | deeperSibling = container; | ||
2304 | |||
2305 | // Fix structure... | ||
2306 | |||
2307 | // We found a table what means that it's empty - remove it completely. | ||
2308 | if ( deeperSibling.is( 'table' ) ) { | ||
2309 | range.setStartAt( deeperSibling, CKEDITOR.POSITION_BEFORE_START ); | ||
2310 | range.collapse( true ); | ||
2311 | deeperSibling.remove(); | ||
2312 | return; | ||
2313 | } | ||
2314 | |||
2315 | // Found an empty txxx element - append tr. | ||
2316 | if ( deeperSibling.is( { tbody: 1, thead: 1, tfoot: 1 } ) ) | ||
2317 | deeperSibling = fixTableStructure( deeperSibling, 'tr', appendToStart ); | ||
2318 | |||
2319 | // Found an empty tr element - append td/th. | ||
2320 | if ( deeperSibling.is( 'tr' ) ) | ||
2321 | deeperSibling = fixTableStructure( deeperSibling, deeperSibling.getParent().is( 'thead' ) ? 'th' : 'td', appendToStart ); | ||
2322 | |||
2323 | // To avoid setting selection after bogus, remove it from the current cell. | ||
2324 | // We can safely do that, because we'll insert element into that cell. | ||
2325 | var bogus = deeperSibling.getBogus(); | ||
2326 | if ( bogus ) | ||
2327 | bogus.remove(); | ||
2328 | |||
2329 | range.moveToPosition( deeperSibling, appendToStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END ); | ||
2330 | }; | ||
2331 | } )(); | ||
2332 | |||
2333 | function mergeBlocksCollapsedSelection( editor, range, backspace, startPath ) { | ||
2334 | var startBlock = startPath.block; | ||
2335 | |||
2336 | // Selection must be collapsed and to be anchored in a block. | ||
2337 | if ( !startBlock ) | ||
2338 | return false; | ||
2339 | |||
2340 | // Exclude cases where, i.e. if pressed arrow key, selection | ||
2341 | // would move within the same block (merge inside a block). | ||
2342 | if ( !range[ backspace ? 'checkStartOfBlock' : 'checkEndOfBlock' ]() ) | ||
2343 | return false; | ||
2344 | |||
2345 | // Make sure, there's an editable position to put selection, | ||
2346 | // which i.e. would be used if pressed arrow key, but abort | ||
2347 | // if such position exists but means a selected non-editable element. | ||
2348 | if ( !range.moveToClosestEditablePosition( startBlock, !backspace ) || !range.collapsed ) | ||
2349 | return false; | ||
2350 | |||
2351 | // Handle special case, when block's sibling is a <hr>. Delete it and keep selection | ||
2352 | // in the same place (http://dev.ckeditor.com/ticket/11861#comment:9). | ||
2353 | if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT ) { | ||
2354 | var touched = range.startContainer.getChild( range.startOffset - ( backspace ? 1 : 0 ) ); | ||
2355 | if ( touched && touched.type == CKEDITOR.NODE_ELEMENT && touched.is( 'hr' ) ) { | ||
2356 | editor.fire( 'saveSnapshot' ); | ||
2357 | touched.remove(); | ||
2358 | return true; | ||
2359 | } | ||
2360 | } | ||
2361 | |||
2362 | var siblingBlock = range.startPath().block; | ||
2363 | |||
2364 | // Abort if an editable position exists, but either it's not | ||
2365 | // in a block or that block is the parent of the start block | ||
2366 | // (merging child into parent). | ||
2367 | if ( !siblingBlock || ( siblingBlock && siblingBlock.contains( startBlock ) ) ) | ||
2368 | return; | ||
2369 | |||
2370 | editor.fire( 'saveSnapshot' ); | ||
2371 | |||
2372 | // Remove bogus to avoid duplicated boguses. | ||
2373 | var bogus; | ||
2374 | if ( ( bogus = ( backspace ? siblingBlock : startBlock ).getBogus() ) ) | ||
2375 | bogus.remove(); | ||
2376 | |||
2377 | // Save selection. It will be restored. | ||
2378 | var selection = editor.getSelection(), | ||
2379 | bookmarks = selection.createBookmarks(); | ||
2380 | |||
2381 | // Merge blocks. | ||
2382 | ( backspace ? startBlock : siblingBlock ).moveChildren( backspace ? siblingBlock : startBlock, false ); | ||
2383 | |||
2384 | // Also merge children along with parents. | ||
2385 | startPath.lastElement.mergeSiblings(); | ||
2386 | |||
2387 | // Cut off removable branch of the DOM tree. | ||
2388 | pruneEmptyDisjointAncestors( startBlock, siblingBlock, !backspace ); | ||
2389 | |||
2390 | // Restore selection. | ||
2391 | selection.selectBookmarks( bookmarks ); | ||
2392 | |||
2393 | return true; | ||
2394 | } | ||
2395 | |||
2396 | function mergeBlocksNonCollapsedSelection( editor, range, startPath ) { | ||
2397 | var startBlock = startPath.block, | ||
2398 | endPath = range.endPath(), | ||
2399 | endBlock = endPath.block; | ||
2400 | |||
2401 | // Selection must be anchored in two different blocks. | ||
2402 | if ( !startBlock || !endBlock || startBlock.equals( endBlock ) ) | ||
2403 | return false; | ||
2404 | |||
2405 | editor.fire( 'saveSnapshot' ); | ||
2406 | |||
2407 | // Remove bogus to avoid duplicated boguses. | ||
2408 | var bogus; | ||
2409 | if ( ( bogus = startBlock.getBogus() ) ) | ||
2410 | bogus.remove(); | ||
2411 | |||
2412 | // Changing end container to element from text node (#12503). | ||
2413 | range.enlarge( CKEDITOR.ENLARGE_INLINE ); | ||
2414 | |||
2415 | // Delete range contents. Do NOT merge. Merging is weird. | ||
2416 | range.deleteContents(); | ||
2417 | |||
2418 | // If something has left of the block to be merged, clean it up. | ||
2419 | // It may happen when merging with list items. | ||
2420 | if ( endBlock.getParent() ) { | ||
2421 | // Move children to the first block. | ||
2422 | endBlock.moveChildren( startBlock, false ); | ||
2423 | |||
2424 | // ...and merge them if that's possible. | ||
2425 | startPath.lastElement.mergeSiblings(); | ||
2426 | |||
2427 | // If expanded selection, things are always merged like with BACKSPACE. | ||
2428 | pruneEmptyDisjointAncestors( startBlock, endBlock, true ); | ||
2429 | } | ||
2430 | |||
2431 | // Make sure the result selection is collapsed. | ||
2432 | range = editor.getSelection().getRanges()[ 0 ]; | ||
2433 | range.collapse( 1 ); | ||
2434 | |||
2435 | // Optimizing range containers from text nodes to elements (#12503). | ||
2436 | range.optimize(); | ||
2437 | if ( range.startContainer.getHtml() === '' ) { | ||
2438 | range.startContainer.appendBogus(); | ||
2439 | } | ||
2440 | |||
2441 | range.select(); | ||
2442 | |||
2443 | return true; | ||
2444 | } | ||
2445 | |||
2446 | // Finds the innermost child of common parent, which, | ||
2447 | // if removed, removes nothing but the contents of the element. | ||
2448 | // | ||
2449 | // before: <div><p><strong>first</strong></p><p>second</p></div> | ||
2450 | // after: <div><p>second</p></div> | ||
2451 | // | ||
2452 | // before: <div><p>x<strong>first</strong></p><p>second</p></div> | ||
2453 | // after: <div><p>x</p><p>second</p></div> | ||
2454 | // | ||
2455 | // isPruneToEnd=true | ||
2456 | // before: <div><p><strong>first</strong></p><p>second</p></div> | ||
2457 | // after: <div><p><strong>first</strong></p></div> | ||
2458 | // | ||
2459 | // @param {CKEDITOR.dom.element} first | ||
2460 | // @param {CKEDITOR.dom.element} second | ||
2461 | // @param {Boolean} isPruneToEnd | ||
2462 | function pruneEmptyDisjointAncestors( first, second, isPruneToEnd ) { | ||
2463 | var commonParent = first.getCommonAncestor( second ), | ||
2464 | node = isPruneToEnd ? second : first, | ||
2465 | removableParent = node; | ||
2466 | |||
2467 | while ( ( node = node.getParent() ) && !commonParent.equals( node ) && node.getChildCount() == 1 ) | ||
2468 | removableParent = node; | ||
2469 | |||
2470 | removableParent.remove(); | ||
2471 | } | ||
2472 | |||
2473 | // | ||
2474 | // Helpers for editable.getHtmlFromRange. | ||
2475 | // | ||
2476 | var getHtmlFromRangeHelpers = { | ||
2477 | eol: { | ||
2478 | detect: function( that, editable ) { | ||
2479 | var range = that.range, | ||
2480 | rangeStart = range.clone(), | ||
2481 | rangeEnd = range.clone(), | ||
2482 | |||
2483 | startPath = new CKEDITOR.dom.elementPath( range.startContainer, editable ), | ||
2484 | endPath = new CKEDITOR.dom.elementPath( range.endContainer, editable ); | ||
2485 | |||
2486 | // Note: checkBoundaryOfElement will not work on original range as CKEDITOR.START|END | ||
2487 | // means that range start|end must be literally anchored at block start|end, e.g. | ||
2488 | // | ||
2489 | // <p>a{</p><p>}b</p> | ||
2490 | // | ||
2491 | // will return false for both paragraphs but two similar ranges | ||
2492 | // | ||
2493 | // <p>a{}</p><p>{}b</p> | ||
2494 | // | ||
2495 | // will return true if checked separately. | ||
2496 | rangeStart.collapse( 1 ); | ||
2497 | rangeEnd.collapse(); | ||
2498 | |||
2499 | if ( startPath.block && rangeStart.checkBoundaryOfElement( startPath.block, CKEDITOR.END ) ) { | ||
2500 | range.setStartAfter( startPath.block ); | ||
2501 | that.prependEolBr = 1; | ||
2502 | } | ||
2503 | |||
2504 | if ( endPath.block && rangeEnd.checkBoundaryOfElement( endPath.block, CKEDITOR.START ) ) { | ||
2505 | range.setEndBefore( endPath.block ); | ||
2506 | that.appendEolBr = 1; | ||
2507 | } | ||
2508 | }, | ||
2509 | |||
2510 | fix: function( that, editable ) { | ||
2511 | var doc = editable.getDocument(), | ||
2512 | appended; | ||
2513 | |||
2514 | // Append <br data-cke-eol="1"> to the fragment. | ||
2515 | if ( that.appendEolBr ) { | ||
2516 | appended = this.createEolBr( doc ); | ||
2517 | that.fragment.append( appended ); | ||
2518 | } | ||
2519 | |||
2520 | // Prepend <br data-cke-eol="1"> to the fragment but avoid duplicates. Such | ||
2521 | // elements should never follow each other in DOM. | ||
2522 | if ( that.prependEolBr && ( !appended || appended.getPrevious() ) ) { | ||
2523 | that.fragment.append( this.createEolBr( doc ), 1 ); | ||
2524 | } | ||
2525 | }, | ||
2526 | |||
2527 | createEolBr: function( doc ) { | ||
2528 | return doc.createElement( 'br', { | ||
2529 | attributes: { | ||
2530 | 'data-cke-eol': 1 | ||
2531 | } | ||
2532 | } ); | ||
2533 | } | ||
2534 | }, | ||
2535 | |||
2536 | bogus: { | ||
2537 | exclude: function( that ) { | ||
2538 | var boundaryNodes = that.range.getBoundaryNodes(), | ||
2539 | startNode = boundaryNodes.startNode, | ||
2540 | endNode = boundaryNodes.endNode; | ||
2541 | |||
2542 | // If bogus is the last node in range but not the only node, exclude it. | ||
2543 | if ( endNode && isBogus( endNode ) && ( !startNode || !startNode.equals( endNode ) ) ) | ||
2544 | that.range.setEndBefore( endNode ); | ||
2545 | } | ||
2546 | }, | ||
2547 | |||
2548 | tree: { | ||
2549 | rebuild: function( that, editable ) { | ||
2550 | var range = that.range, | ||
2551 | node = range.getCommonAncestor(), | ||
2552 | |||
2553 | // A path relative to the common ancestor. | ||
2554 | commonPath = new CKEDITOR.dom.elementPath( node, editable ), | ||
2555 | startPath = new CKEDITOR.dom.elementPath( range.startContainer, editable ), | ||
2556 | endPath = new CKEDITOR.dom.elementPath( range.endContainer, editable ), | ||
2557 | limit; | ||
2558 | |||
2559 | if ( node.type == CKEDITOR.NODE_TEXT ) | ||
2560 | node = node.getParent(); | ||
2561 | |||
2562 | // Fix DOM of partially enclosed tables | ||
2563 | // <table><tbody><tr><td>a{b</td><td>c}d</td></tr></tbody></table> | ||
2564 | // Full table is returned | ||
2565 | // <table><tbody><tr><td>b</td><td>c</td></tr></tbody></table> | ||
2566 | // instead of | ||
2567 | // <td>b</td><td>c</td> | ||
2568 | if ( commonPath.blockLimit.is( { tr: 1, table: 1 } ) ) { | ||
2569 | var tableParent = commonPath.contains( 'table' ).getParent(); | ||
2570 | |||
2571 | limit = function( node ) { | ||
2572 | return !node.equals( tableParent ); | ||
2573 | }; | ||
2574 | } | ||
2575 | |||
2576 | // Fix DOM in the following case | ||
2577 | // <ol><li>a{b<ul><li>c}d</li></ul></li></ol> | ||
2578 | // Full list is returned | ||
2579 | // <ol><li>b<ul><li>c</li></ul></li></ol> | ||
2580 | // instead of | ||
2581 | // b<ul><li>c</li></ul> | ||
2582 | else if ( commonPath.block && commonPath.block.is( CKEDITOR.dtd.$listItem ) ) { | ||
2583 | var startList = startPath.contains( CKEDITOR.dtd.$list ), | ||
2584 | endList = endPath.contains( CKEDITOR.dtd.$list ); | ||
2585 | |||
2586 | if ( !startList.equals( endList ) ) { | ||
2587 | var listParent = commonPath.contains( CKEDITOR.dtd.$list ).getParent(); | ||
2588 | |||
2589 | limit = function( node ) { | ||
2590 | return !node.equals( listParent ); | ||
2591 | }; | ||
2592 | } | ||
2593 | } | ||
2594 | |||
2595 | // If not defined, use generic limit function. | ||
2596 | if ( !limit ) { | ||
2597 | limit = function( node ) { | ||
2598 | return !node.equals( commonPath.block ) && !node.equals( commonPath.blockLimit ); | ||
2599 | }; | ||
2600 | } | ||
2601 | |||
2602 | this.rebuildFragment( that, editable, node, limit ); | ||
2603 | }, | ||
2604 | |||
2605 | rebuildFragment: function( that, editable, node, checkLimit ) { | ||
2606 | var clone; | ||
2607 | |||
2608 | while ( node && !node.equals( editable ) && checkLimit( node ) ) { | ||
2609 | // Don't clone children. Preserve element ids. | ||
2610 | clone = node.clone( 0, 1 ); | ||
2611 | that.fragment.appendTo( clone ); | ||
2612 | that.fragment = clone; | ||
2613 | |||
2614 | node = node.getParent(); | ||
2615 | } | ||
2616 | } | ||
2617 | }, | ||
2618 | |||
2619 | cell: { | ||
2620 | // Handle range anchored in table row with a single cell enclosed: | ||
2621 | // <table><tbody><tr>[<td>a</td>]</tr></tbody></table> | ||
2622 | // becomes | ||
2623 | // <table><tbody><tr><td>{a}</td></tr></tbody></table> | ||
2624 | shrink: function( that ) { | ||
2625 | var range = that.range, | ||
2626 | startContainer = range.startContainer, | ||
2627 | endContainer = range.endContainer, | ||
2628 | startOffset = range.startOffset, | ||
2629 | endOffset = range.endOffset; | ||
2630 | |||
2631 | if ( startContainer.type == CKEDITOR.NODE_ELEMENT && startContainer.equals( endContainer ) && startContainer.is( 'tr' ) && ++startOffset == endOffset ) { | ||
2632 | range.shrink( CKEDITOR.SHRINK_TEXT ); | ||
2633 | } | ||
2634 | } | ||
2635 | } | ||
2636 | }; | ||
2637 | |||
2638 | // | ||
2639 | // Helpers for editable.extractHtmlFromRange. | ||
2640 | // | ||
2641 | var extractHtmlFromRangeHelpers = ( function() { | ||
2642 | function optimizeBookmarkNode( node, toStart ) { | ||
2643 | var parent = node.getParent(); | ||
2644 | |||
2645 | if ( parent.is( CKEDITOR.dtd.$inline ) ) | ||
2646 | node[ toStart ? 'insertBefore' : 'insertAfter' ]( parent ); | ||
2647 | } | ||
2648 | |||
2649 | function mergeElements( merged, startBookmark, endBookmark ) { | ||
2650 | optimizeBookmarkNode( startBookmark ); | ||
2651 | optimizeBookmarkNode( endBookmark, 1 ); | ||
2652 | |||
2653 | var next; | ||
2654 | while ( ( next = endBookmark.getNext() ) ) { | ||
2655 | next.insertAfter( startBookmark ); | ||
2656 | |||
2657 | // Update startBookmark after insertion to avoid the reversal of nodes (#13449). | ||
2658 | startBookmark = next; | ||
2659 | } | ||
2660 | |||
2661 | if ( isEmpty( merged ) ) | ||
2662 | merged.remove(); | ||
2663 | } | ||
2664 | |||
2665 | function getPath( startElement, root ) { | ||
2666 | return new CKEDITOR.dom.elementPath( startElement, root ); | ||
2667 | } | ||
2668 | |||
2669 | // Creates a range from a bookmark without removing the bookmark. | ||
2670 | function createRangeFromBookmark( root, bookmark ) { | ||
2671 | var range = new CKEDITOR.dom.range( root ); | ||
2672 | range.setStartAfter( bookmark.startNode ); | ||
2673 | range.setEndBefore( bookmark.endNode ); | ||
2674 | return range; | ||
2675 | } | ||
2676 | |||
2677 | var list = { | ||
2678 | detectMerge: function( that, editable ) { | ||
2679 | var range = createRangeFromBookmark( editable, that.bookmark ), | ||
2680 | startPath = range.startPath(), | ||
2681 | endPath = range.endPath(), | ||
2682 | |||
2683 | startList = startPath.contains( CKEDITOR.dtd.$list ), | ||
2684 | endList = endPath.contains( CKEDITOR.dtd.$list ); | ||
2685 | |||
2686 | that.mergeList = | ||
2687 | // Both lists must exist | ||
2688 | startList && endList && | ||
2689 | // ...and be of the same type | ||
2690 | // startList.getName() == endList.getName() && | ||
2691 | // ...and share the same parent (same level in the tree) | ||
2692 | startList.getParent().equals( endList.getParent() ) && | ||
2693 | // ...and must be different. | ||
2694 | !startList.equals( endList ); | ||
2695 | |||
2696 | that.mergeListItems = | ||
2697 | startPath.block && endPath.block && | ||
2698 | // Both containers must be list items | ||
2699 | startPath.block.is( CKEDITOR.dtd.$listItem ) && endPath.block.is( CKEDITOR.dtd.$listItem ); | ||
2700 | |||
2701 | // Create merge bookmark. | ||
2702 | if ( that.mergeList || that.mergeListItems ) { | ||
2703 | var rangeClone = range.clone(); | ||
2704 | |||
2705 | rangeClone.setStartBefore( that.bookmark.startNode ); | ||
2706 | rangeClone.setEndAfter( that.bookmark.endNode ); | ||
2707 | |||
2708 | that.mergeListBookmark = rangeClone.createBookmark(); | ||
2709 | } | ||
2710 | }, | ||
2711 | |||
2712 | merge: function( that, editable ) { | ||
2713 | if ( !that.mergeListBookmark ) | ||
2714 | return; | ||
2715 | |||
2716 | var startNode = that.mergeListBookmark.startNode, | ||
2717 | endNode = that.mergeListBookmark.endNode, | ||
2718 | |||
2719 | startPath = getPath( startNode, editable ), | ||
2720 | endPath = getPath( endNode, editable ); | ||
2721 | |||
2722 | if ( that.mergeList ) { | ||
2723 | var firstList = startPath.contains( CKEDITOR.dtd.$list ), | ||
2724 | secondList = endPath.contains( CKEDITOR.dtd.$list ); | ||
2725 | |||
2726 | if ( !firstList.equals( secondList ) ) { | ||
2727 | secondList.moveChildren( firstList ); | ||
2728 | secondList.remove(); | ||
2729 | } | ||
2730 | } | ||
2731 | |||
2732 | if ( that.mergeListItems ) { | ||
2733 | var firstListItem = startPath.contains( CKEDITOR.dtd.$listItem ), | ||
2734 | secondListItem = endPath.contains( CKEDITOR.dtd.$listItem ); | ||
2735 | |||
2736 | if ( !firstListItem.equals( secondListItem ) ) { | ||
2737 | mergeElements( secondListItem, startNode, endNode ); | ||
2738 | } | ||
2739 | } | ||
2740 | |||
2741 | // Remove bookmark nodes. | ||
2742 | startNode.remove(); | ||
2743 | endNode.remove(); | ||
2744 | } | ||
2745 | }; | ||
2746 | |||
2747 | var block = { | ||
2748 | // Detects whether blocks should be merged once contents are extracted. | ||
2749 | detectMerge: function( that, editable ) { | ||
2750 | // Don't merge blocks if lists or tables are already involved. | ||
2751 | if ( that.tableContentsRanges || that.mergeListBookmark ) | ||
2752 | return; | ||
2753 | |||
2754 | var rangeClone = new CKEDITOR.dom.range( editable ); | ||
2755 | |||
2756 | rangeClone.setStartBefore( that.bookmark.startNode ); | ||
2757 | rangeClone.setEndAfter( that.bookmark.endNode ); | ||
2758 | |||
2759 | that.mergeBlockBookmark = rangeClone.createBookmark(); | ||
2760 | }, | ||
2761 | |||
2762 | merge: function( that, editable ) { | ||
2763 | if ( !that.mergeBlockBookmark || that.purgeTableBookmark ) | ||
2764 | return; | ||
2765 | |||
2766 | var startNode = that.mergeBlockBookmark.startNode, | ||
2767 | endNode = that.mergeBlockBookmark.endNode, | ||
2768 | |||
2769 | startPath = getPath( startNode, editable ), | ||
2770 | endPath = getPath( endNode, editable ), | ||
2771 | |||
2772 | firstBlock = startPath.block, | ||
2773 | secondBlock = endPath.block; | ||
2774 | |||
2775 | if ( firstBlock && secondBlock && !firstBlock.equals( secondBlock ) ) { | ||
2776 | mergeElements( secondBlock, startNode, endNode ); | ||
2777 | } | ||
2778 | |||
2779 | // Remove bookmark nodes. | ||
2780 | startNode.remove(); | ||
2781 | endNode.remove(); | ||
2782 | } | ||
2783 | }; | ||
2784 | |||
2785 | var table = ( function() { | ||
2786 | var tableEditable = { td: 1, th: 1, caption: 1 }; | ||
2787 | |||
2788 | // Returns an array of ranges which should be entirely extracted. | ||
2789 | // | ||
2790 | // <table><tr>[<td>xx</td><td>y}y</td></tr></table> | ||
2791 | // will find: | ||
2792 | // <table><tr><td>[xx]</td><td>[y}y</td></tr></table> | ||
2793 | function findTableContentsRanges( range ) { | ||
2794 | // Leaving the below for debugging purposes. | ||
2795 | // | ||
2796 | // console.log( 'findTableContentsRanges' ); | ||
2797 | // console.log( bender.tools.range.getWithHtml( range.root, range ) ); | ||
2798 | |||
2799 | var contentsRanges = [], | ||
2800 | editableRange, | ||
2801 | walker = new CKEDITOR.dom.walker( range ), | ||
2802 | startCell = range.startPath().contains( tableEditable ), | ||
2803 | endCell = range.endPath().contains( tableEditable ), | ||
2804 | database = {}; | ||
2805 | |||
2806 | walker.guard = function( node, leaving ) { | ||
2807 | // Guard may be executed on some node boundaries multiple times, | ||
2808 | // what results in creating more than one range for each selected cell. (#12964) | ||
2809 | if ( node.type == CKEDITOR.NODE_ELEMENT ) { | ||
2810 | var key = 'visited_' + ( leaving ? 'out' : 'in' ); | ||
2811 | if ( node.getCustomData( key ) ) { | ||
2812 | return; | ||
2813 | } | ||
2814 | |||
2815 | CKEDITOR.dom.element.setMarker( database, node, key, 1 ); | ||
2816 | } | ||
2817 | |||
2818 | // Handle partial selection in a cell in which the range starts: | ||
2819 | // <td><p>x{xx</p></td>... | ||
2820 | // will store: | ||
2821 | // <td><p>x{xx</p>]</td> | ||
2822 | if ( leaving && startCell && node.equals( startCell ) ) { | ||
2823 | editableRange = range.clone(); | ||
2824 | editableRange.setEndAt( startCell, CKEDITOR.POSITION_BEFORE_END ); | ||
2825 | contentsRanges.push( editableRange ); | ||
2826 | return; | ||
2827 | } | ||
2828 | |||
2829 | // Handle partial selection in a cell in which the range ends. | ||
2830 | if ( !leaving && endCell && node.equals( endCell ) ) { | ||
2831 | editableRange = range.clone(); | ||
2832 | editableRange.setStartAt( endCell, CKEDITOR.POSITION_AFTER_START ); | ||
2833 | contentsRanges.push( editableRange ); | ||
2834 | return; | ||
2835 | } | ||
2836 | |||
2837 | // Handle all other cells visited by the walker. | ||
2838 | // We need to check whether the cell is disjoint with | ||
2839 | // the start and end cells to correctly handle case like: | ||
2840 | // <td>x{x</td><td><table>..<td>y}y</td>..</table></td> | ||
2841 | // without the check the second cell's content would be entirely removed. | ||
2842 | if ( !leaving && checkRemoveCellContents( node ) ) { | ||
2843 | editableRange = range.clone(); | ||
2844 | editableRange.selectNodeContents( node ); | ||
2845 | contentsRanges.push( editableRange ); | ||
2846 | } | ||
2847 | }; | ||
2848 | |||
2849 | walker.lastForward(); | ||
2850 | |||
2851 | // Clear all markers so next extraction will not be affected by this one. | ||
2852 | CKEDITOR.dom.element.clearAllMarkers( database ); | ||
2853 | |||
2854 | return contentsRanges; | ||
2855 | |||
2856 | function checkRemoveCellContents( node ) { | ||
2857 | return ( | ||
2858 | // Must be a cell. | ||
2859 | node.type == CKEDITOR.NODE_ELEMENT && node.is( tableEditable ) && | ||
2860 | // Must be disjoint with the range's startCell if exists. | ||
2861 | ( !startCell || checkDisjointNodes( node, startCell ) ) && | ||
2862 | // Must be disjoint with the range's endCell if exists. | ||
2863 | ( !endCell || checkDisjointNodes( node, endCell ) ) | ||
2864 | ); | ||
2865 | } | ||
2866 | } | ||
2867 | |||
2868 | // Returns a normalized common ancestor of a range. | ||
2869 | // If the real common ancestor is located somewhere in between a table and a td/th/caption, | ||
2870 | // then the table will be returned. | ||
2871 | function getNormalizedAncestor( range ) { | ||
2872 | var common = range.getCommonAncestor(); | ||
2873 | |||
2874 | if ( common.is( CKEDITOR.dtd.$tableContent ) && !common.is( tableEditable ) ) { | ||
2875 | common = common.getAscendant( 'table', true ); | ||
2876 | } | ||
2877 | |||
2878 | return common; | ||
2879 | } | ||
2880 | |||
2881 | // Check whether node1 and node2 are disjoint, so are: | ||
2882 | // * not identical, | ||
2883 | // * not contained in each other. | ||
2884 | function checkDisjointNodes( node1, node2 ) { | ||
2885 | var disallowedPositions = CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_IS_CONTAINED, | ||
2886 | pos = node1.getPosition( node2 ); | ||
2887 | |||
2888 | // Baaah... IDENTICAL is 0, so we can't simplify this ;/. | ||
2889 | return pos === CKEDITOR.POSITION_IDENTICAL ? | ||
2890 | false : | ||
2891 | ( ( pos & disallowedPositions ) === 0 ); | ||
2892 | } | ||
2893 | |||
2894 | return { | ||
2895 | // Detects whether to purge entire list. | ||
2896 | detectPurge: function( that ) { | ||
2897 | var range = that.range, | ||
2898 | walkerRange = range.clone(); | ||
2899 | |||
2900 | walkerRange.enlarge( CKEDITOR.ENLARGE_ELEMENT ); | ||
2901 | |||
2902 | var walker = new CKEDITOR.dom.walker( walkerRange ), | ||
2903 | editablesCount = 0; | ||
2904 | |||
2905 | // Count the number of table editables in the range. If there's more than one, | ||
2906 | // table MAY be removed completely (it's a cross-cell range). Otherwise, only | ||
2907 | // the contents of the cell are usually removed. | ||
2908 | walker.evaluator = function( node ) { | ||
2909 | if ( node.type == CKEDITOR.NODE_ELEMENT && node.is( tableEditable ) ) { | ||
2910 | ++editablesCount; | ||
2911 | } | ||
2912 | }; | ||
2913 | |||
2914 | walker.checkForward(); | ||
2915 | |||
2916 | if ( editablesCount > 1 ) { | ||
2917 | var startTable = range.startPath().contains( 'table' ), | ||
2918 | endTable = range.endPath().contains( 'table' ); | ||
2919 | |||
2920 | if ( startTable && endTable && range.checkBoundaryOfElement( startTable, CKEDITOR.START ) && range.checkBoundaryOfElement( endTable, CKEDITOR.END ) ) { | ||
2921 | var rangeClone = that.range.clone(); | ||
2922 | |||
2923 | rangeClone.setStartBefore( startTable ); | ||
2924 | rangeClone.setEndAfter( endTable ); | ||
2925 | |||
2926 | that.purgeTableBookmark = rangeClone.createBookmark(); | ||
2927 | } | ||
2928 | } | ||
2929 | }, | ||
2930 | |||
2931 | // The magic. | ||
2932 | // | ||
2933 | // This method tries to discover whether the range starts or ends somewhere in a table | ||
2934 | // (it is not interested whether the range contains a table, because in such case | ||
2935 | // the extractContents() methods does the job correctly). | ||
2936 | // If the range meets these criteria, then the method tries to discover and store the following: | ||
2937 | // | ||
2938 | // * that.tableSurroundingRange - a part of the range which is located outside of any table which | ||
2939 | // will be touched (note: when range is located in a single cell it does not touch the table). | ||
2940 | // This range can be placed at: | ||
2941 | // * at the beginning: <p>he{re</p><table>..]..</table> | ||
2942 | // * in the middle: <table>..[..</table><p>here</p><table>..]..</table> | ||
2943 | // * at the end: <table>..[..</table><p>he}re</p> | ||
2944 | // * that.tableContentsRanges - an array of ranges with contents of td/th/caption that should be removed. | ||
2945 | // This assures that calling extractContents() does not change the structure of the table(s). | ||
2946 | detectRanges: function( that, editable ) { | ||
2947 | var range = createRangeFromBookmark( editable, that.bookmark ), | ||
2948 | surroundingRange = range.clone(), | ||
2949 | leftRange, | ||
2950 | rightRange, | ||
2951 | |||
2952 | // Find a common ancestor and normalize it (so the following paths contain tables). | ||
2953 | commonAncestor = getNormalizedAncestor( range ), | ||
2954 | |||
2955 | // Create paths using the normalized ancestor, so tables beyond the context | ||
2956 | // of the input range are not found. | ||
2957 | startPath = new CKEDITOR.dom.elementPath( range.startContainer, commonAncestor ), | ||
2958 | endPath = new CKEDITOR.dom.elementPath( range.endContainer, commonAncestor ), | ||
2959 | |||
2960 | startTable = startPath.contains( 'table' ), | ||
2961 | endTable = endPath.contains( 'table' ), | ||
2962 | |||
2963 | tableContentsRanges; | ||
2964 | |||
2965 | // Nothing to do here - the range doesn't touch any table or | ||
2966 | // it contains a table, but that table is fully selected so it will be simply fully removed | ||
2967 | // by the normal algorithm. | ||
2968 | if ( !startTable && !endTable ) { | ||
2969 | return; | ||
2970 | } | ||
2971 | |||
2972 | // Handle two disjoint tables case: | ||
2973 | // <table>..[..</table><p>ab</p><table>..]..</table> | ||
2974 | // is handled as (respectively: findTableContents( left ), surroundingRange, findTableContents( right )): | ||
2975 | // <table>..[..</table>][<p>ab</p>][<table>..]..</table> | ||
2976 | // Check that tables are disjoint to exclude a case when start equals end or one is contained | ||
2977 | // in the other. | ||
2978 | if ( startTable && endTable && checkDisjointNodes( startTable, endTable ) ) { | ||
2979 | that.tableSurroundingRange = surroundingRange; | ||
2980 | surroundingRange.setStartAt( startTable, CKEDITOR.POSITION_AFTER_END ); | ||
2981 | surroundingRange.setEndAt( endTable, CKEDITOR.POSITION_BEFORE_START ); | ||
2982 | |||
2983 | leftRange = range.clone(); | ||
2984 | leftRange.setEndAt( startTable, CKEDITOR.POSITION_AFTER_END ); | ||
2985 | |||
2986 | rightRange = range.clone(); | ||
2987 | rightRange.setStartAt( endTable, CKEDITOR.POSITION_BEFORE_START ); | ||
2988 | |||
2989 | tableContentsRanges = findTableContentsRanges( leftRange ).concat( findTableContentsRanges( rightRange ) ); | ||
2990 | } | ||
2991 | // Divide the initial range into two parts: | ||
2992 | // * range which contains the part containing the table, | ||
2993 | // * surroundingRange which contains the part outside the table. | ||
2994 | // | ||
2995 | // The surroundingRange exists only if one of the range ends is | ||
2996 | // located outside the table. | ||
2997 | // | ||
2998 | // <p>a{b</p><table>..]..</table><p>cd</p> | ||
2999 | // becomes (respectively: surroundingRange, range): | ||
3000 | // <p>a{b</p>][<table>..]..</table><p>cd</p> | ||
3001 | else if ( !startTable ) { | ||
3002 | that.tableSurroundingRange = surroundingRange; | ||
3003 | surroundingRange.setEndAt( endTable, CKEDITOR.POSITION_BEFORE_START ); | ||
3004 | |||
3005 | range.setStartAt( endTable, CKEDITOR.POSITION_AFTER_START ); | ||
3006 | } | ||
3007 | // <p>ab</p><table>..[..</table><p>c}d</p> | ||
3008 | // becomes (respectively range, surroundingRange): | ||
3009 | // <p>ab</p><table>..[..</table>][<p>c}d</p> | ||
3010 | else if ( !endTable ) { | ||
3011 | that.tableSurroundingRange = surroundingRange; | ||
3012 | surroundingRange.setStartAt( startTable, CKEDITOR.POSITION_AFTER_END ); | ||
3013 | |||
3014 | range.setEndAt( startTable, CKEDITOR.POSITION_AFTER_END ); | ||
3015 | } | ||
3016 | |||
3017 | // Use already calculated or calculate for the remaining range. | ||
3018 | that.tableContentsRanges = tableContentsRanges ? tableContentsRanges : findTableContentsRanges( range ); | ||
3019 | |||
3020 | // Leaving the below for debugging purposes. | ||
3021 | // | ||
3022 | // if ( that.tableSurroundingRange ) { | ||
3023 | // console.log( 'tableSurroundingRange' ); | ||
3024 | // console.log( bender.tools.range.getWithHtml( that.tableSurroundingRange.root, that.tableSurroundingRange ) ); | ||
3025 | // } | ||
3026 | // | ||
3027 | // console.log( 'tableContentsRanges' ); | ||
3028 | // that.tableContentsRanges.forEach( function( range ) { | ||
3029 | // console.log( bender.tools.range.getWithHtml( range.root, range ) ); | ||
3030 | // } ); | ||
3031 | }, | ||
3032 | |||
3033 | deleteRanges: function( that ) { | ||
3034 | var range; | ||
3035 | |||
3036 | // Delete table cell contents. | ||
3037 | while ( ( range = that.tableContentsRanges.pop() ) ) { | ||
3038 | range.extractContents(); | ||
3039 | |||
3040 | if ( isEmpty( range.startContainer ) ) | ||
3041 | range.startContainer.appendBogus(); | ||
3042 | } | ||
3043 | |||
3044 | // Finally delete surroundings of the table. | ||
3045 | if ( that.tableSurroundingRange ) { | ||
3046 | that.tableSurroundingRange.extractContents(); | ||
3047 | } | ||
3048 | }, | ||
3049 | |||
3050 | purge: function( that ) { | ||
3051 | if ( !that.purgeTableBookmark ) | ||
3052 | return; | ||
3053 | |||
3054 | var doc = that.doc, | ||
3055 | range = that.range, | ||
3056 | rangeClone = range.clone(), | ||
3057 | // How about different enter modes? | ||
3058 | block = doc.createElement( 'p' ); | ||
3059 | |||
3060 | block.insertBefore( that.purgeTableBookmark.startNode ); | ||
3061 | |||
3062 | rangeClone.moveToBookmark( that.purgeTableBookmark ); | ||
3063 | rangeClone.deleteContents(); | ||
3064 | |||
3065 | that.range.moveToPosition( block, CKEDITOR.POSITION_AFTER_START ); | ||
3066 | } | ||
3067 | }; | ||
3068 | } )(); | ||
3069 | |||
3070 | return { | ||
3071 | list: list, | ||
3072 | block: block, | ||
3073 | table: table, | ||
3074 | |||
3075 | // Detects whether use "mergeThen" argument in range.extractContents(). | ||
3076 | detectExtractMerge: function( that ) { | ||
3077 | // Don't merge if playing with lists. | ||
3078 | return !( | ||
3079 | that.range.startPath().contains( CKEDITOR.dtd.$listItem ) && | ||
3080 | that.range.endPath().contains( CKEDITOR.dtd.$listItem ) | ||
3081 | ); | ||
3082 | }, | ||
3083 | |||
3084 | fixUneditableRangePosition: function( range ) { | ||
3085 | if ( !range.startContainer.getDtd()[ '#' ] ) { | ||
3086 | range.moveToClosestEditablePosition( null, true ); | ||
3087 | } | ||
3088 | }, | ||
3089 | |||
3090 | // Perform auto paragraphing if needed. | ||
3091 | autoParagraph: function( editor, range ) { | ||
3092 | var path = range.startPath(), | ||
3093 | fixBlock; | ||
3094 | |||
3095 | if ( shouldAutoParagraph( editor, path.block, path.blockLimit ) && ( fixBlock = autoParagraphTag( editor ) ) ) { | ||
3096 | fixBlock = range.document.createElement( fixBlock ); | ||
3097 | fixBlock.appendBogus(); | ||
3098 | range.insertNode( fixBlock ); | ||
3099 | range.moveToPosition( fixBlock, CKEDITOR.POSITION_AFTER_START ); | ||
3100 | } | ||
3101 | } | ||
3102 | }; | ||
3103 | } )(); | ||
3104 | |||
3105 | } )(); | ||
3106 | |||
3107 | /** | ||
3108 | * Whether the editor must output an empty value (`''`) if its content only consists | ||
3109 | * of an empty paragraph. | ||
3110 | * | ||
3111 | * config.ignoreEmptyParagraph = false; | ||
3112 | * | ||
3113 | * @cfg {Boolean} [ignoreEmptyParagraph=true] | ||
3114 | * @member CKEDITOR.config | ||
3115 | */ | ||
3116 | |||
3117 | /** | ||
3118 | * Event fired by the editor in order to get accessibility help label. | ||
3119 | * The event is responded to by a component which provides accessibility | ||
3120 | * help (i.e. the `a11yhelp` plugin) hence the editor is notified whether | ||
3121 | * accessibility help is available. | ||
3122 | * | ||
3123 | * Providing info: | ||
3124 | * | ||
3125 | * editor.on( 'ariaEditorHelpLabel', function( evt ) { | ||
3126 | * evt.data.label = editor.lang.common.editorHelp; | ||
3127 | * } ); | ||
3128 | * | ||
3129 | * Getting label: | ||
3130 | * | ||
3131 | * var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label; | ||
3132 | * | ||
3133 | * @since 4.4.3 | ||
3134 | * @event ariaEditorHelpLabel | ||
3135 | * @param {String} label The label to be used. | ||
3136 | * @member CKEDITOR.editor | ||
3137 | */ | ||
3138 | |||
3139 | /** | ||
3140 | * Event fired when the user double-clicks in the editable area. | ||
3141 | * The event allows to open a dialog window for a clicked element in a convenient way: | ||
3142 | * | ||
3143 | * editor.on( 'doubleclick', function( evt ) { | ||
3144 | * var element = evt.data.element; | ||
3145 | * | ||
3146 | * if ( element.is( 'table' ) ) | ||
3147 | * evt.data.dialog = 'tableProperties'; | ||
3148 | * } ); | ||
3149 | * | ||
3150 | * **Note:** To handle double-click on a widget use {@link CKEDITOR.plugins.widget#doubleclick}. | ||
3151 | * | ||
3152 | * @event doubleclick | ||
3153 | * @param data | ||
3154 | * @param {CKEDITOR.dom.element} data.element The double-clicked element. | ||
3155 | * @param {String} data.dialog The dialog window to be opened. If set by the listener, | ||
3156 | * the specified dialog window will be opened. | ||
3157 | * @member CKEDITOR.editor | ||
3158 | */ | ||
diff --git a/sources/core/editor.js b/sources/core/editor.js new file mode 100644 index 0000000..31188d2 --- /dev/null +++ b/sources/core/editor.js | |||
@@ -0,0 +1,2004 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 that represents an | ||
8 | * editor instance. | ||
9 | */ | ||
10 | |||
11 | ( function() { | ||
12 | // Override the basic constructor defined at editor_basic.js. | ||
13 | Editor.prototype = CKEDITOR.editor.prototype; | ||
14 | CKEDITOR.editor = Editor; | ||
15 | |||
16 | /** | ||
17 | * Represents an editor instance. This constructor should be rarely | ||
18 | * used in favor of the {@link CKEDITOR} editor creation functions. | ||
19 | * | ||
20 | * @class CKEDITOR.editor | ||
21 | * @mixins CKEDITOR.event | ||
22 | * @constructor Creates an editor class instance. | ||
23 | * @param {Object} [instanceConfig] Configuration values for this specific instance. | ||
24 | * @param {CKEDITOR.dom.element} [element] The DOM element upon which this editor | ||
25 | * will be created. | ||
26 | * @param {Number} [mode] The element creation mode to be used by this editor. | ||
27 | */ | ||
28 | function Editor( instanceConfig, element, mode ) { | ||
29 | // Call the CKEDITOR.event constructor to initialize this instance. | ||
30 | CKEDITOR.event.call( this ); | ||
31 | |||
32 | // Make a clone of the config object, to avoid having it touched by our code. (#9636) | ||
33 | instanceConfig = instanceConfig && CKEDITOR.tools.clone( instanceConfig ); | ||
34 | |||
35 | // if editor is created off one page element. | ||
36 | if ( element !== undefined ) { | ||
37 | // Asserting element and mode not null. | ||
38 | if ( !( element instanceof CKEDITOR.dom.element ) ) | ||
39 | throw new Error( 'Expect element of type CKEDITOR.dom.element.' ); | ||
40 | else if ( !mode ) | ||
41 | throw new Error( 'One of the element modes must be specified.' ); | ||
42 | |||
43 | if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && mode == CKEDITOR.ELEMENT_MODE_INLINE ) | ||
44 | throw new Error( 'Inline element mode is not supported on IE quirks.' ); | ||
45 | |||
46 | if ( !isSupportedElement( element, mode ) ) | ||
47 | throw new Error( 'The specified element mode is not supported on element: "' + element.getName() + '".' ); | ||
48 | |||
49 | /** | ||
50 | * The original host page element upon which the editor is created. It is only | ||
51 | * supposed to be provided by the particular editor creator and is not subject to | ||
52 | * be modified. | ||
53 | * | ||
54 | * @readonly | ||
55 | * @property {CKEDITOR.dom.element} | ||
56 | */ | ||
57 | this.element = element; | ||
58 | |||
59 | /** | ||
60 | * This property indicates the way this instance is associated with the {@link #element}. | ||
61 | * See also {@link CKEDITOR#ELEMENT_MODE_INLINE} and {@link CKEDITOR#ELEMENT_MODE_REPLACE}. | ||
62 | * | ||
63 | * @readonly | ||
64 | * @property {Number} | ||
65 | */ | ||
66 | this.elementMode = mode; | ||
67 | |||
68 | this.name = ( this.elementMode != CKEDITOR.ELEMENT_MODE_APPENDTO ) && ( element.getId() || element.getNameAtt() ); | ||
69 | } else { | ||
70 | this.elementMode = CKEDITOR.ELEMENT_MODE_NONE; | ||
71 | } | ||
72 | |||
73 | // Declare the private namespace. | ||
74 | this._ = {}; | ||
75 | |||
76 | this.commands = {}; | ||
77 | |||
78 | /** | ||
79 | * Contains all UI templates created for this editor instance. | ||
80 | * | ||
81 | * @readonly | ||
82 | * @property {Object} | ||
83 | */ | ||
84 | this.templates = {}; | ||
85 | |||
86 | /** | ||
87 | * A unique identifier of this editor instance. | ||
88 | * | ||
89 | * **Note:** It will be originated from the `id` or `name` | ||
90 | * attribute of the {@link #element}, otherwise a name pattern of | ||
91 | * `'editor{n}'` will be used. | ||
92 | * | ||
93 | * @readonly | ||
94 | * @property {String} | ||
95 | */ | ||
96 | this.name = this.name || genEditorName(); | ||
97 | |||
98 | /** | ||
99 | * A unique random string assigned to each editor instance on the page. | ||
100 | * | ||
101 | * @readonly | ||
102 | * @property {String} | ||
103 | */ | ||
104 | this.id = CKEDITOR.tools.getNextId(); | ||
105 | |||
106 | /** | ||
107 | * Indicates editor initialization status. The following statuses are available: | ||
108 | * | ||
109 | * * **unloaded**: The initial state — the editor instance was initialized, | ||
110 | * but its components (configuration, plugins, language files) are not loaded yet. | ||
111 | * * **loaded**: The editor components were loaded — see the {@link CKEDITOR.editor#loaded} event. | ||
112 | * * **ready**: The editor is fully initialized and ready — see the {@link CKEDITOR.editor#instanceReady} event. | ||
113 | * * **destroyed**: The editor was destroyed — see the {@link CKEDITOR.editor#method-destroy} method. | ||
114 | * | ||
115 | * @since 4.1 | ||
116 | * @readonly | ||
117 | * @property {String} | ||
118 | */ | ||
119 | this.status = 'unloaded'; | ||
120 | |||
121 | /** | ||
122 | * The configuration for this editor instance. It inherits all | ||
123 | * settings defined in {@link CKEDITOR.config}, combined with settings | ||
124 | * loaded from custom configuration files and those defined inline in | ||
125 | * the page when creating the editor. | ||
126 | * | ||
127 | * var editor = CKEDITOR.instances.editor1; | ||
128 | * alert( editor.config.skin ); // e.g. 'moono' | ||
129 | * | ||
130 | * @readonly | ||
131 | * @property {CKEDITOR.config} | ||
132 | */ | ||
133 | this.config = CKEDITOR.tools.prototypedCopy( CKEDITOR.config ); | ||
134 | |||
135 | /** | ||
136 | * The namespace containing UI features related to this editor instance. | ||
137 | * | ||
138 | * @readonly | ||
139 | * @property {CKEDITOR.ui} | ||
140 | */ | ||
141 | this.ui = new CKEDITOR.ui( this ); | ||
142 | |||
143 | /** | ||
144 | * Controls the focus state of this editor instance. This property | ||
145 | * is rarely used for normal API operations. It is mainly | ||
146 | * targeted at developers adding UI elements to the editor interface. | ||
147 | * | ||
148 | * @readonly | ||
149 | * @property {CKEDITOR.focusManager} | ||
150 | */ | ||
151 | this.focusManager = new CKEDITOR.focusManager( this ); | ||
152 | |||
153 | /** | ||
154 | * Controls keystroke typing in this editor instance. | ||
155 | * | ||
156 | * @readonly | ||
157 | * @property {CKEDITOR.keystrokeHandler} | ||
158 | */ | ||
159 | this.keystrokeHandler = new CKEDITOR.keystrokeHandler( this ); | ||
160 | |||
161 | // Make the editor update its command states on mode change. | ||
162 | this.on( 'readOnly', updateCommands ); | ||
163 | this.on( 'selectionChange', function( evt ) { | ||
164 | updateCommandsContext( this, evt.data.path ); | ||
165 | } ); | ||
166 | this.on( 'activeFilterChange', function() { | ||
167 | updateCommandsContext( this, this.elementPath(), true ); | ||
168 | } ); | ||
169 | this.on( 'mode', updateCommands ); | ||
170 | |||
171 | // Handle startup focus. | ||
172 | this.on( 'instanceReady', function() { | ||
173 | this.config.startupFocus && this.focus(); | ||
174 | } ); | ||
175 | |||
176 | CKEDITOR.fire( 'instanceCreated', null, this ); | ||
177 | |||
178 | // Add this new editor to the CKEDITOR.instances collections. | ||
179 | CKEDITOR.add( this ); | ||
180 | |||
181 | // Return the editor instance immediately to enable early stage event registrations. | ||
182 | CKEDITOR.tools.setTimeout( function() { | ||
183 | if ( this.status !== 'destroyed' ) { | ||
184 | initConfig( this, instanceConfig ); | ||
185 | } else { | ||
186 | CKEDITOR.warn( 'editor-incorrect-destroy' ); | ||
187 | } | ||
188 | }, 0, this ); | ||
189 | } | ||
190 | |||
191 | var nameCounter = 0; | ||
192 | |||
193 | function genEditorName() { | ||
194 | do { | ||
195 | var name = 'editor' + ( ++nameCounter ); | ||
196 | } | ||
197 | while ( CKEDITOR.instances[ name ] ); | ||
198 | |||
199 | return name; | ||
200 | } | ||
201 | |||
202 | // Asserting element DTD depending on mode. | ||
203 | function isSupportedElement( element, mode ) { | ||
204 | if ( mode == CKEDITOR.ELEMENT_MODE_INLINE ) | ||
205 | return element.is( CKEDITOR.dtd.$editable ) || element.is( 'textarea' ); | ||
206 | else if ( mode == CKEDITOR.ELEMENT_MODE_REPLACE ) | ||
207 | return !element.is( CKEDITOR.dtd.$nonBodyContent ); | ||
208 | return 1; | ||
209 | } | ||
210 | |||
211 | function updateCommands() { | ||
212 | var commands = this.commands, | ||
213 | name; | ||
214 | |||
215 | for ( name in commands ) | ||
216 | updateCommand( this, commands[ name ] ); | ||
217 | } | ||
218 | |||
219 | function updateCommand( editor, cmd ) { | ||
220 | cmd[ cmd.startDisabled ? 'disable' : editor.readOnly && !cmd.readOnly ? 'disable' : cmd.modes[ editor.mode ] ? 'enable' : 'disable' ](); | ||
221 | } | ||
222 | |||
223 | function updateCommandsContext( editor, path, forceRefresh ) { | ||
224 | // Commands cannot be refreshed without a path. In edge cases | ||
225 | // it may happen that there's no selection when this function is executed. | ||
226 | // For example when active filter is changed in #10877. | ||
227 | if ( !path ) | ||
228 | return; | ||
229 | |||
230 | var command, | ||
231 | name, | ||
232 | commands = editor.commands; | ||
233 | |||
234 | for ( name in commands ) { | ||
235 | command = commands[ name ]; | ||
236 | |||
237 | if ( forceRefresh || command.contextSensitive ) | ||
238 | command.refresh( editor, path ); | ||
239 | } | ||
240 | } | ||
241 | |||
242 | // ##### START: Config Privates | ||
243 | |||
244 | // These function loads custom configuration files and cache the | ||
245 | // CKEDITOR.editorConfig functions defined on them, so there is no need to | ||
246 | // download them more than once for several instances. | ||
247 | var loadConfigLoaded = {}; | ||
248 | |||
249 | function loadConfig( editor ) { | ||
250 | var customConfig = editor.config.customConfig; | ||
251 | |||
252 | // Check if there is a custom config to load. | ||
253 | if ( !customConfig ) | ||
254 | return false; | ||
255 | |||
256 | customConfig = CKEDITOR.getUrl( customConfig ); | ||
257 | |||
258 | var loadedConfig = loadConfigLoaded[ customConfig ] || ( loadConfigLoaded[ customConfig ] = {} ); | ||
259 | |||
260 | // If the custom config has already been downloaded, reuse it. | ||
261 | if ( loadedConfig.fn ) { | ||
262 | // Call the cached CKEDITOR.editorConfig defined in the custom | ||
263 | // config file for the editor instance depending on it. | ||
264 | loadedConfig.fn.call( editor, editor.config ); | ||
265 | |||
266 | // If there is no other customConfig in the chain, fire the | ||
267 | // "configLoaded" event. | ||
268 | if ( CKEDITOR.getUrl( editor.config.customConfig ) == customConfig || !loadConfig( editor ) ) | ||
269 | editor.fireOnce( 'customConfigLoaded' ); | ||
270 | } else { | ||
271 | // Load the custom configuration file. | ||
272 | // To resolve customConfig race conflicts, use scriptLoader#queue | ||
273 | // instead of scriptLoader#load (#6504). | ||
274 | CKEDITOR.scriptLoader.queue( customConfig, function() { | ||
275 | // If the CKEDITOR.editorConfig function has been properly | ||
276 | // defined in the custom configuration file, cache it. | ||
277 | if ( CKEDITOR.editorConfig ) | ||
278 | loadedConfig.fn = CKEDITOR.editorConfig; | ||
279 | else | ||
280 | loadedConfig.fn = function() {}; | ||
281 | |||
282 | // Call the load config again. This time the custom | ||
283 | // config is already cached and so it will get loaded. | ||
284 | loadConfig( editor ); | ||
285 | } ); | ||
286 | } | ||
287 | |||
288 | return true; | ||
289 | } | ||
290 | |||
291 | function initConfig( editor, instanceConfig ) { | ||
292 | // Setup the lister for the "customConfigLoaded" event. | ||
293 | editor.on( 'customConfigLoaded', function() { | ||
294 | if ( instanceConfig ) { | ||
295 | // Register the events that may have been set at the instance | ||
296 | // configuration object. | ||
297 | if ( instanceConfig.on ) { | ||
298 | for ( var eventName in instanceConfig.on ) { | ||
299 | editor.on( eventName, instanceConfig.on[ eventName ] ); | ||
300 | } | ||
301 | } | ||
302 | |||
303 | // Overwrite the settings from the in-page config. | ||
304 | CKEDITOR.tools.extend( editor.config, instanceConfig, true ); | ||
305 | |||
306 | delete editor.config.on; | ||
307 | } | ||
308 | |||
309 | onConfigLoaded( editor ); | ||
310 | } ); | ||
311 | |||
312 | // The instance config may override the customConfig setting to avoid | ||
313 | // loading the default ~/config.js file. | ||
314 | if ( instanceConfig && instanceConfig.customConfig != null ) | ||
315 | editor.config.customConfig = instanceConfig.customConfig; | ||
316 | |||
317 | // Load configs from the custom configuration files. | ||
318 | if ( !loadConfig( editor ) ) | ||
319 | editor.fireOnce( 'customConfigLoaded' ); | ||
320 | } | ||
321 | |||
322 | // ##### END: Config Privates | ||
323 | |||
324 | // Set config related properties. | ||
325 | function onConfigLoaded( editor ) { | ||
326 | var config = editor.config; | ||
327 | |||
328 | /** | ||
329 | * Indicates the read-only state of this editor. This is a read-only property. | ||
330 | * See also {@link CKEDITOR.editor#setReadOnly}. | ||
331 | * | ||
332 | * @since 3.6 | ||
333 | * @readonly | ||
334 | * @property {Boolean} | ||
335 | */ | ||
336 | editor.readOnly = isEditorReadOnly(); | ||
337 | |||
338 | function isEditorReadOnly() { | ||
339 | if ( config.readOnly ) { | ||
340 | return true; | ||
341 | } | ||
342 | |||
343 | if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) { | ||
344 | if ( editor.element.is( 'textarea' ) ) { | ||
345 | return editor.element.hasAttribute( 'disabled' ) || editor.element.hasAttribute( 'readonly' ); | ||
346 | } else { | ||
347 | return editor.element.isReadOnly(); | ||
348 | } | ||
349 | } else if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) { | ||
350 | return editor.element.hasAttribute( 'disabled' ) || editor.element.hasAttribute( 'readonly' ); | ||
351 | } | ||
352 | |||
353 | return false; | ||
354 | } | ||
355 | |||
356 | /** | ||
357 | * Indicates that the editor is running in an environment where | ||
358 | * no block elements are accepted inside the content. | ||
359 | * | ||
360 | * This can be for example inline editor based on an `<h1>` element. | ||
361 | * | ||
362 | * @readonly | ||
363 | * @property {Boolean} | ||
364 | */ | ||
365 | editor.blockless = editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ? | ||
366 | !( editor.element.is( 'textarea' ) || CKEDITOR.dtd[ editor.element.getName() ].p ) : | ||
367 | false; | ||
368 | |||
369 | /** | ||
370 | * The [tabbing navigation](http://en.wikipedia.org/wiki/Tabbing_navigation) order determined for this editor instance. | ||
371 | * This can be set by the <code>{@link CKEDITOR.config#tabIndex}</code> | ||
372 | * setting or taken from the `tabindex` attribute of the | ||
373 | * {@link #element} associated with the editor. | ||
374 | * | ||
375 | * alert( editor.tabIndex ); // e.g. 0 | ||
376 | * | ||
377 | * @readonly | ||
378 | * @property {Number} [=0] | ||
379 | */ | ||
380 | editor.tabIndex = config.tabIndex || editor.element && editor.element.getAttribute( 'tabindex' ) || 0; | ||
381 | |||
382 | editor.activeEnterMode = editor.enterMode = validateEnterMode( editor, config.enterMode ); | ||
383 | editor.activeShiftEnterMode = editor.shiftEnterMode = validateEnterMode( editor, config.shiftEnterMode ); | ||
384 | |||
385 | // Set CKEDITOR.skinName. Note that it is not possible to have | ||
386 | // different skins on the same page, so the last one to set it "wins". | ||
387 | if ( config.skin ) | ||
388 | CKEDITOR.skinName = config.skin; | ||
389 | |||
390 | // Fire the "configLoaded" event. | ||
391 | editor.fireOnce( 'configLoaded' ); | ||
392 | |||
393 | initComponents( editor ); | ||
394 | } | ||
395 | |||
396 | // Various other core components that read editor configuration. | ||
397 | function initComponents( editor ) { | ||
398 | // Documented in dataprocessor.js. | ||
399 | editor.dataProcessor = new CKEDITOR.htmlDataProcessor( editor ); | ||
400 | |||
401 | // Set activeFilter directly to avoid firing event. | ||
402 | editor.filter = editor.activeFilter = new CKEDITOR.filter( editor ); | ||
403 | |||
404 | loadSkin( editor ); | ||
405 | } | ||
406 | |||
407 | function loadSkin( editor ) { | ||
408 | CKEDITOR.skin.loadPart( 'editor', function() { | ||
409 | loadLang( editor ); | ||
410 | } ); | ||
411 | } | ||
412 | |||
413 | function loadLang( editor ) { | ||
414 | CKEDITOR.lang.load( editor.config.language, editor.config.defaultLanguage, function( languageCode, lang ) { | ||
415 | var configTitle = editor.config.title; | ||
416 | |||
417 | /** | ||
418 | * The code for the language resources that have been loaded | ||
419 | * for the user interface elements of this editor instance. | ||
420 | * | ||
421 | * alert( editor.langCode ); // e.g. 'en' | ||
422 | * | ||
423 | * @readonly | ||
424 | * @property {String} | ||
425 | */ | ||
426 | editor.langCode = languageCode; | ||
427 | |||
428 | /** | ||
429 | * An object that contains all language strings used by the editor interface. | ||
430 | * | ||
431 | * alert( editor.lang.basicstyles.bold ); // e.g. 'Negrito' (if the language is set to Portuguese) | ||
432 | * | ||
433 | * @readonly | ||
434 | * @property {Object} lang | ||
435 | */ | ||
436 | // As we'll be adding plugin specific entries that could come | ||
437 | // from different language code files, we need a copy of lang, | ||
438 | // not a direct reference to it. | ||
439 | editor.lang = CKEDITOR.tools.prototypedCopy( lang ); | ||
440 | |||
441 | /** | ||
442 | * Indicates the human-readable title of this editor. Although this is a read-only property, | ||
443 | * it can be initialized with {@link CKEDITOR.config#title}. | ||
444 | * | ||
445 | * **Note:** Please do not confuse this property with {@link CKEDITOR.editor#name editor.name} | ||
446 | * which identifies the instance in the {@link CKEDITOR#instances} literal. | ||
447 | * | ||
448 | * @since 4.2 | ||
449 | * @readonly | ||
450 | * @property {String/Boolean} | ||
451 | */ | ||
452 | editor.title = typeof configTitle == 'string' || configTitle === false ? configTitle : [ editor.lang.editor, editor.name ].join( ', ' ); | ||
453 | |||
454 | if ( !editor.config.contentsLangDirection ) { | ||
455 | // Fallback to either the editable element direction or editor UI direction depending on creators. | ||
456 | editor.config.contentsLangDirection = editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ? editor.element.getDirection( 1 ) : editor.lang.dir; | ||
457 | } | ||
458 | |||
459 | editor.fire( 'langLoaded' ); | ||
460 | |||
461 | preloadStylesSet( editor ); | ||
462 | } ); | ||
463 | } | ||
464 | |||
465 | // Preloads styles set file (config.stylesSet). | ||
466 | // If stylesSet was defined directly (by an array) | ||
467 | // this function will call loadPlugins fully synchronously. | ||
468 | // If stylesSet is a string (path) loadPlugins will | ||
469 | // be called asynchronously. | ||
470 | // In both cases - styles will be preload before plugins initialization. | ||
471 | function preloadStylesSet( editor ) { | ||
472 | editor.getStylesSet( function( styles ) { | ||
473 | // Wait for editor#loaded, so plugins could add their listeners. | ||
474 | // But listen with high priority to fire editor#stylesSet before editor#uiReady and editor#setData. | ||
475 | editor.once( 'loaded', function() { | ||
476 | // Note: we can't use fireOnce because this event may canceled and fired again. | ||
477 | editor.fire( 'stylesSet', { styles: styles } ); | ||
478 | }, null, null, 1 ); | ||
479 | |||
480 | loadPlugins( editor ); | ||
481 | } ); | ||
482 | } | ||
483 | |||
484 | function loadPlugins( editor ) { | ||
485 | var config = editor.config, | ||
486 | plugins = config.plugins, | ||
487 | extraPlugins = config.extraPlugins, | ||
488 | removePlugins = config.removePlugins; | ||
489 | |||
490 | if ( extraPlugins ) { | ||
491 | // Remove them first to avoid duplications. | ||
492 | var extraRegex = new RegExp( '(?:^|,)(?:' + extraPlugins.replace( /\s*,\s*/g, '|' ) + ')(?=,|$)', 'g' ); | ||
493 | plugins = plugins.replace( extraRegex, '' ); | ||
494 | |||
495 | plugins += ',' + extraPlugins; | ||
496 | } | ||
497 | |||
498 | if ( removePlugins ) { | ||
499 | var removeRegex = new RegExp( '(?:^|,)(?:' + removePlugins.replace( /\s*,\s*/g, '|' ) + ')(?=,|$)', 'g' ); | ||
500 | plugins = plugins.replace( removeRegex, '' ); | ||
501 | } | ||
502 | |||
503 | // Load the Adobe AIR plugin conditionally. | ||
504 | CKEDITOR.env.air && ( plugins += ',adobeair' ); | ||
505 | |||
506 | // Load all plugins defined in the "plugins" setting. | ||
507 | CKEDITOR.plugins.load( plugins.split( ',' ), function( plugins ) { | ||
508 | // The list of plugins. | ||
509 | var pluginsArray = []; | ||
510 | |||
511 | // The language code to get loaded for each plugin. Null | ||
512 | // entries will be appended for plugins with no language files. | ||
513 | var languageCodes = []; | ||
514 | |||
515 | // The list of URLs to language files. | ||
516 | var languageFiles = []; | ||
517 | |||
518 | /** | ||
519 | * An object that contains references to all plugins used by this | ||
520 | * editor instance. | ||
521 | * | ||
522 | * alert( editor.plugins.dialog.path ); // e.g. 'http://example.com/ckeditor/plugins/dialog/' | ||
523 | * | ||
524 | * // Check if a plugin is available. | ||
525 | * if ( editor.plugins.image ) { | ||
526 | * ... | ||
527 | * } | ||
528 | * | ||
529 | * @readonly | ||
530 | * @property {Object} | ||
531 | */ | ||
532 | editor.plugins = plugins; | ||
533 | |||
534 | // Loop through all plugins, to build the list of language | ||
535 | // files to get loaded. | ||
536 | // | ||
537 | // Check also whether any of loaded plugins doesn't require plugins | ||
538 | // defined in config.removePlugins. Throw non-blocking error if this happens. | ||
539 | for ( var pluginName in plugins ) { | ||
540 | var plugin = plugins[ pluginName ], | ||
541 | pluginLangs = plugin.lang, | ||
542 | lang = null, | ||
543 | requires = plugin.requires, | ||
544 | match, name; | ||
545 | |||
546 | // Transform it into a string, if it's not one. | ||
547 | if ( CKEDITOR.tools.isArray( requires ) ) | ||
548 | requires = requires.join( ',' ); | ||
549 | |||
550 | if ( requires && ( match = requires.match( removeRegex ) ) ) { | ||
551 | while ( ( name = match.pop() ) ) { | ||
552 | CKEDITOR.error( 'editor-plugin-required', { plugin: name.replace( ',', '' ), requiredBy: pluginName } ); | ||
553 | } | ||
554 | } | ||
555 | |||
556 | // If the plugin has "lang". | ||
557 | if ( pluginLangs && !editor.lang[ pluginName ] ) { | ||
558 | // Trasnform the plugin langs into an array, if it's not one. | ||
559 | if ( pluginLangs.split ) | ||
560 | pluginLangs = pluginLangs.split( ',' ); | ||
561 | |||
562 | // Resolve the plugin language. If the current language | ||
563 | // is not available, get English or the first one. | ||
564 | if ( CKEDITOR.tools.indexOf( pluginLangs, editor.langCode ) >= 0 ) | ||
565 | lang = editor.langCode; | ||
566 | else { | ||
567 | // The language code may have the locale information (zh-cn). | ||
568 | // Fall back to locale-less in that case (zh). | ||
569 | var langPart = editor.langCode.replace( /-.*/, '' ); | ||
570 | if ( langPart != editor.langCode && CKEDITOR.tools.indexOf( pluginLangs, langPart ) >= 0 ) | ||
571 | lang = langPart; | ||
572 | // Try the only "generic" option we have: English. | ||
573 | else if ( CKEDITOR.tools.indexOf( pluginLangs, 'en' ) >= 0 ) | ||
574 | lang = 'en'; | ||
575 | else | ||
576 | lang = pluginLangs[ 0 ]; | ||
577 | } | ||
578 | |||
579 | if ( !plugin.langEntries || !plugin.langEntries[ lang ] ) { | ||
580 | // Put the language file URL into the list of files to | ||
581 | // get downloaded. | ||
582 | languageFiles.push( CKEDITOR.getUrl( plugin.path + 'lang/' + lang + '.js' ) ); | ||
583 | } else { | ||
584 | editor.lang[ pluginName ] = plugin.langEntries[ lang ]; | ||
585 | lang = null; | ||
586 | } | ||
587 | } | ||
588 | |||
589 | // Save the language code, so we know later which | ||
590 | // language has been resolved to this plugin. | ||
591 | languageCodes.push( lang ); | ||
592 | |||
593 | pluginsArray.push( plugin ); | ||
594 | } | ||
595 | |||
596 | // Load all plugin specific language files in a row. | ||
597 | CKEDITOR.scriptLoader.load( languageFiles, function() { | ||
598 | // Initialize all plugins that have the "beforeInit" and "init" methods defined. | ||
599 | var methods = [ 'beforeInit', 'init', 'afterInit' ]; | ||
600 | for ( var m = 0; m < methods.length; m++ ) { | ||
601 | for ( var i = 0; i < pluginsArray.length; i++ ) { | ||
602 | var plugin = pluginsArray[ i ]; | ||
603 | |||
604 | // Uses the first loop to update the language entries also. | ||
605 | if ( m === 0 && languageCodes[ i ] && plugin.lang && plugin.langEntries ) | ||
606 | editor.lang[ plugin.name ] = plugin.langEntries[ languageCodes[ i ] ]; | ||
607 | |||
608 | // Call the plugin method (beforeInit and init). | ||
609 | if ( plugin[ methods[ m ] ] ) | ||
610 | plugin[ methods[ m ] ]( editor ); | ||
611 | } | ||
612 | } | ||
613 | |||
614 | editor.fireOnce( 'pluginsLoaded' ); | ||
615 | |||
616 | // Setup the configured keystrokes. | ||
617 | config.keystrokes && editor.setKeystroke( editor.config.keystrokes ); | ||
618 | |||
619 | // Setup the configured blocked keystrokes. | ||
620 | for ( i = 0; i < editor.config.blockedKeystrokes.length; i++ ) | ||
621 | editor.keystrokeHandler.blockedKeystrokes[ editor.config.blockedKeystrokes[ i ] ] = 1; | ||
622 | |||
623 | editor.status = 'loaded'; | ||
624 | editor.fireOnce( 'loaded' ); | ||
625 | CKEDITOR.fire( 'instanceLoaded', null, editor ); | ||
626 | } ); | ||
627 | } ); | ||
628 | } | ||
629 | |||
630 | // Send to data output back to editor's associated element. | ||
631 | function updateEditorElement() { | ||
632 | var element = this.element; | ||
633 | |||
634 | // Some editor creation mode will not have the | ||
635 | // associated element. | ||
636 | if ( element && this.elementMode != CKEDITOR.ELEMENT_MODE_APPENDTO ) { | ||
637 | var data = this.getData(); | ||
638 | |||
639 | if ( this.config.htmlEncodeOutput ) | ||
640 | data = CKEDITOR.tools.htmlEncode( data ); | ||
641 | |||
642 | if ( element.is( 'textarea' ) ) | ||
643 | element.setValue( data ); | ||
644 | else | ||
645 | element.setHtml( data ); | ||
646 | |||
647 | return true; | ||
648 | } | ||
649 | return false; | ||
650 | } | ||
651 | |||
652 | // Always returns ENTER_BR in case of blockless editor. | ||
653 | function validateEnterMode( editor, enterMode ) { | ||
654 | return editor.blockless ? CKEDITOR.ENTER_BR : enterMode; | ||
655 | } | ||
656 | |||
657 | // Create DocumentFragment from specified ranges. For now it handles only tables in Firefox | ||
658 | // and returns DocumentFragment from the 1. range for other cases. (#13884) | ||
659 | function createDocumentFragmentFromRanges( ranges, editable ) { | ||
660 | var docFragment = new CKEDITOR.dom.documentFragment(), | ||
661 | tableClone, | ||
662 | currentRow, | ||
663 | currentRowClone; | ||
664 | |||
665 | for ( var i = 0; i < ranges.length; i++ ) { | ||
666 | var range = ranges[ i ], | ||
667 | container = range.startContainer; | ||
668 | |||
669 | if ( container.getName && container.getName() == 'tr' ) { | ||
670 | if ( !tableClone ) { | ||
671 | tableClone = container.getAscendant( 'table' ).clone(); | ||
672 | tableClone.append( container.getAscendant( 'tbody' ).clone() ); | ||
673 | docFragment.append( tableClone ); | ||
674 | tableClone = tableClone.findOne( 'tbody' ); | ||
675 | } | ||
676 | |||
677 | if ( !( currentRow && currentRow.equals( container ) ) ) { | ||
678 | currentRow = container; | ||
679 | currentRowClone = container.clone(); | ||
680 | tableClone.append( currentRowClone ); | ||
681 | } | ||
682 | |||
683 | currentRowClone.append( range.cloneContents() ); | ||
684 | } else { | ||
685 | // If there was something else copied with table, | ||
686 | // append it to DocumentFragment. | ||
687 | docFragment.append( range.cloneContents() ); | ||
688 | } | ||
689 | } | ||
690 | |||
691 | if ( !tableClone ) { | ||
692 | return editable.getHtmlFromRange( ranges[ 0 ] ); | ||
693 | } | ||
694 | |||
695 | return docFragment; | ||
696 | } | ||
697 | |||
698 | CKEDITOR.tools.extend( CKEDITOR.editor.prototype, { | ||
699 | /** | ||
700 | * Adds a command definition to the editor instance. Commands added with | ||
701 | * this function can be executed later with the <code>{@link #execCommand}</code> method. | ||
702 | * | ||
703 | * editorInstance.addCommand( 'sample', { | ||
704 | * exec: function( editor ) { | ||
705 | * alert( 'Executing a command for the editor name "' + editor.name + '"!' ); | ||
706 | * } | ||
707 | * } ); | ||
708 | * | ||
709 | * @param {String} commandName The indentifier name of the command. | ||
710 | * @param {CKEDITOR.commandDefinition} commandDefinition The command definition. | ||
711 | */ | ||
712 | addCommand: function( commandName, commandDefinition ) { | ||
713 | commandDefinition.name = commandName.toLowerCase(); | ||
714 | var cmd = new CKEDITOR.command( this, commandDefinition ); | ||
715 | |||
716 | // Update command when mode is set. | ||
717 | // This guarantees that commands added before first editor#mode | ||
718 | // aren't immediately updated, but waits for editor#mode and that | ||
719 | // commands added later are immediately refreshed, even when added | ||
720 | // before instanceReady. #10103, #10249 | ||
721 | if ( this.mode ) | ||
722 | updateCommand( this, cmd ); | ||
723 | |||
724 | return this.commands[ commandName ] = cmd; | ||
725 | }, | ||
726 | |||
727 | /** | ||
728 | * Attaches the editor to a form to call {@link #updateElement} before form submission. | ||
729 | * This method is called by both creators ({@link CKEDITOR#replace replace} and | ||
730 | * {@link CKEDITOR#inline inline}), so there is no reason to call it manually. | ||
731 | * | ||
732 | * @private | ||
733 | */ | ||
734 | _attachToForm: function() { | ||
735 | var editor = this, | ||
736 | element = editor.element, | ||
737 | form = new CKEDITOR.dom.element( element.$.form ); | ||
738 | |||
739 | // If are replacing a textarea, we must | ||
740 | if ( element.is( 'textarea' ) ) { | ||
741 | if ( form ) { | ||
742 | form.on( 'submit', onSubmit ); | ||
743 | |||
744 | // Check if there is no element/elements input with name == "submit". | ||
745 | // If they exists they will overwrite form submit function (form.$.submit). | ||
746 | // If form.$.submit is overwritten we can not do anything with it. | ||
747 | if ( isFunction( form.$.submit ) ) { | ||
748 | // Setup the submit function because it doesn't fire the | ||
749 | // "submit" event. | ||
750 | form.$.submit = CKEDITOR.tools.override( form.$.submit, function( originalSubmit ) { | ||
751 | return function() { | ||
752 | onSubmit(); | ||
753 | |||
754 | // For IE, the DOM submit function is not a | ||
755 | // function, so we need third check. | ||
756 | if ( originalSubmit.apply ) | ||
757 | originalSubmit.apply( this ); | ||
758 | else | ||
759 | originalSubmit(); | ||
760 | }; | ||
761 | } ); | ||
762 | } | ||
763 | |||
764 | // Remove 'submit' events registered on form element before destroying.(#3988) | ||
765 | editor.on( 'destroy', function() { | ||
766 | form.removeListener( 'submit', onSubmit ); | ||
767 | } ); | ||
768 | } | ||
769 | } | ||
770 | |||
771 | function onSubmit( evt ) { | ||
772 | editor.updateElement(); | ||
773 | |||
774 | // #8031 If textarea had required attribute and editor is empty fire 'required' event and if | ||
775 | // it was cancelled, prevent submitting the form. | ||
776 | if ( editor._.required && !element.getValue() && editor.fire( 'required' ) === false ) { | ||
777 | // When user press save button event (evt) is undefined (see save plugin). | ||
778 | // This method works because it throws error so originalSubmit won't be called. | ||
779 | // Also this error won't be shown because it will be caught in save plugin. | ||
780 | evt.data.preventDefault(); | ||
781 | } | ||
782 | } | ||
783 | |||
784 | function isFunction( f ) { | ||
785 | // For IE8 typeof fun == object so we cannot use it. | ||
786 | return !!( f && f.call && f.apply ); | ||
787 | } | ||
788 | }, | ||
789 | |||
790 | /** | ||
791 | * Destroys the editor instance, releasing all resources used by it. | ||
792 | * If the editor replaced an element, the element will be recovered. | ||
793 | * | ||
794 | * alert( CKEDITOR.instances.editor1 ); // e.g. object | ||
795 | * CKEDITOR.instances.editor1.destroy(); | ||
796 | * alert( CKEDITOR.instances.editor1 ); // undefined | ||
797 | * | ||
798 | * @param {Boolean} [noUpdate] If the instance is replacing a DOM | ||
799 | * element, this parameter indicates whether or not to update the | ||
800 | * element with the instance content. | ||
801 | */ | ||
802 | destroy: function( noUpdate ) { | ||
803 | this.fire( 'beforeDestroy' ); | ||
804 | |||
805 | !noUpdate && updateEditorElement.call( this ); | ||
806 | |||
807 | this.editable( null ); | ||
808 | |||
809 | if ( this.filter ) { | ||
810 | this.filter.destroy(); | ||
811 | delete this.filter; | ||
812 | } | ||
813 | |||
814 | delete this.activeFilter; | ||
815 | |||
816 | this.status = 'destroyed'; | ||
817 | |||
818 | this.fire( 'destroy' ); | ||
819 | |||
820 | // Plug off all listeners to prevent any further events firing. | ||
821 | this.removeAllListeners(); | ||
822 | |||
823 | CKEDITOR.remove( this ); | ||
824 | CKEDITOR.fire( 'instanceDestroyed', null, this ); | ||
825 | }, | ||
826 | |||
827 | /** | ||
828 | * Returns an {@link CKEDITOR.dom.elementPath element path} for the selection in the editor. | ||
829 | * | ||
830 | * @param {CKEDITOR.dom.node} [startNode] From which the path should start, | ||
831 | * if not specified defaults to editor selection's | ||
832 | * start element yielded by {@link CKEDITOR.dom.selection#getStartElement}. | ||
833 | * @returns {CKEDITOR.dom.elementPath} | ||
834 | */ | ||
835 | elementPath: function( startNode ) { | ||
836 | if ( !startNode ) { | ||
837 | var sel = this.getSelection(); | ||
838 | if ( !sel ) | ||
839 | return null; | ||
840 | |||
841 | startNode = sel.getStartElement(); | ||
842 | } | ||
843 | |||
844 | return startNode ? new CKEDITOR.dom.elementPath( startNode, this.editable() ) : null; | ||
845 | }, | ||
846 | |||
847 | /** | ||
848 | * Shortcut to create a {@link CKEDITOR.dom.range} instance from the editable element. | ||
849 | * | ||
850 | * @returns {CKEDITOR.dom.range} The DOM range created if the editable has presented. | ||
851 | * @see CKEDITOR.dom.range | ||
852 | */ | ||
853 | createRange: function() { | ||
854 | var editable = this.editable(); | ||
855 | return editable ? new CKEDITOR.dom.range( editable ) : null; | ||
856 | }, | ||
857 | |||
858 | /** | ||
859 | * Executes a command associated with the editor. | ||
860 | * | ||
861 | * editorInstance.execCommand( 'bold' ); | ||
862 | * | ||
863 | * @param {String} commandName The indentifier name of the command. | ||
864 | * @param {Object} [data] The data to be passed to the command. | ||
865 | * @returns {Boolean} `true` if the command was executed | ||
866 | * successfully, otherwise `false`. | ||
867 | * @see CKEDITOR.editor#addCommand | ||
868 | */ | ||
869 | execCommand: function( commandName, data ) { | ||
870 | var command = this.getCommand( commandName ); | ||
871 | |||
872 | var eventData = { | ||
873 | name: commandName, | ||
874 | commandData: data, | ||
875 | command: command | ||
876 | }; | ||
877 | |||
878 | if ( command && command.state != CKEDITOR.TRISTATE_DISABLED ) { | ||
879 | if ( this.fire( 'beforeCommandExec', eventData ) !== false ) { | ||
880 | eventData.returnValue = command.exec( eventData.commandData ); | ||
881 | |||
882 | // Fire the 'afterCommandExec' immediately if command is synchronous. | ||
883 | if ( !command.async && this.fire( 'afterCommandExec', eventData ) !== false ) | ||
884 | return eventData.returnValue; | ||
885 | } | ||
886 | } | ||
887 | |||
888 | // throw 'Unknown command name "' + commandName + '"'; | ||
889 | return false; | ||
890 | }, | ||
891 | |||
892 | /** | ||
893 | * Gets one of the registered commands. Note that after registering a | ||
894 | * command definition with {@link #addCommand}, it is | ||
895 | * transformed internally into an instance of | ||
896 | * {@link CKEDITOR.command}, which will then be returned by this function. | ||
897 | * | ||
898 | * @param {String} commandName The name of the command to be returned. | ||
899 | * This is the same name that is used to register the command with `addCommand`. | ||
900 | * @returns {CKEDITOR.command} The command object identified by the provided name. | ||
901 | */ | ||
902 | getCommand: function( commandName ) { | ||
903 | return this.commands[ commandName ]; | ||
904 | }, | ||
905 | |||
906 | /** | ||
907 | * Gets the editor data. The data will be in "raw" format. It is the same | ||
908 | * data that is posted by the editor. | ||
909 | * | ||
910 | * if ( CKEDITOR.instances.editor1.getData() == '' ) | ||
911 | * alert( 'There is no data available.' ); | ||
912 | * | ||
913 | * @param {Boolean} internal If set to `true`, it will prevent firing the | ||
914 | * {@link CKEDITOR.editor#beforeGetData} and {@link CKEDITOR.editor#event-getData} events, so | ||
915 | * the real content of the editor will not be read and cached data will be returned. The method will work | ||
916 | * much faster, but this may result in the editor returning the data that is not up to date. This parameter | ||
917 | * should thus only be set to `true` when you are certain that the cached data is up to date. | ||
918 | * | ||
919 | * @returns {String} The editor data. | ||
920 | */ | ||
921 | getData: function( internal ) { | ||
922 | !internal && this.fire( 'beforeGetData' ); | ||
923 | |||
924 | var eventData = this._.data; | ||
925 | |||
926 | if ( typeof eventData != 'string' ) { | ||
927 | var element = this.element; | ||
928 | if ( element && this.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) | ||
929 | eventData = element.is( 'textarea' ) ? element.getValue() : element.getHtml(); | ||
930 | else | ||
931 | eventData = ''; | ||
932 | } | ||
933 | |||
934 | eventData = { dataValue: eventData }; | ||
935 | |||
936 | // Fire "getData" so data manipulation may happen. | ||
937 | !internal && this.fire( 'getData', eventData ); | ||
938 | |||
939 | return eventData.dataValue; | ||
940 | }, | ||
941 | |||
942 | /** | ||
943 | * Gets the "raw data" currently available in the editor. This is a | ||
944 | * fast method which returns the data as is, without processing, so it is | ||
945 | * not recommended to use it on resulting pages. Instead it can be used | ||
946 | * combined with the {@link #method-loadSnapshot} method in order | ||
947 | * to automatically save the editor data from time to time | ||
948 | * while the user is using the editor, to avoid data loss, without risking | ||
949 | * performance issues. | ||
950 | * | ||
951 | * alert( editor.getSnapshot() ); | ||
952 | * | ||
953 | * See also: | ||
954 | * | ||
955 | * * {@link CKEDITOR.editor#method-getData}. | ||
956 | * | ||
957 | * @returns {String} Editor "raw data". | ||
958 | */ | ||
959 | getSnapshot: function() { | ||
960 | var data = this.fire( 'getSnapshot' ); | ||
961 | |||
962 | if ( typeof data != 'string' ) { | ||
963 | var element = this.element; | ||
964 | |||
965 | if ( element && this.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) { | ||
966 | data = element.is( 'textarea' ) ? element.getValue() : element.getHtml(); | ||
967 | } | ||
968 | else { | ||
969 | // If we don't have a proper element, set data to an empty string, | ||
970 | // as this method is expected to return a string. (#13385) | ||
971 | data = ''; | ||
972 | } | ||
973 | } | ||
974 | |||
975 | return data; | ||
976 | }, | ||
977 | |||
978 | /** | ||
979 | * Loads "raw data" into the editor. The data is loaded with processing | ||
980 | * straight to the editing area. It should not be used as a way to load | ||
981 | * any kind of data, but instead in combination with | ||
982 | * {@link #method-getSnapshot}-produced data. | ||
983 | * | ||
984 | * var data = editor.getSnapshot(); | ||
985 | * editor.loadSnapshot( data ); | ||
986 | * | ||
987 | * @see CKEDITOR.editor#setData | ||
988 | */ | ||
989 | loadSnapshot: function( snapshot ) { | ||
990 | this.fire( 'loadSnapshot', snapshot ); | ||
991 | }, | ||
992 | |||
993 | /** | ||
994 | * Sets the editor data. The data must be provided in the "raw" format (HTML). | ||
995 | * | ||
996 | * Note that this method is asynchronous. The `callback` parameter must | ||
997 | * be used if interaction with the editor is needed after setting the data. | ||
998 | * | ||
999 | * CKEDITOR.instances.editor1.setData( '<p>This is the editor data.</p>' ); | ||
1000 | * | ||
1001 | * CKEDITOR.instances.editor1.setData( '<p>Some other editor data.</p>', { | ||
1002 | * callback: function() { | ||
1003 | * this.checkDirty(); // true | ||
1004 | * } | ||
1005 | * } ); | ||
1006 | * | ||
1007 | * Note: In **CKEditor 4.4.2** the signature of this method has changed. All arguments | ||
1008 | * except `data` were wrapped into the `options` object. However, backward compatibility | ||
1009 | * was preserved and it is still possible to use the `data, callback, internal` arguments. | ||
1010 | * | ||
1011 | * | ||
1012 | * @param {String} data The HTML code to replace current editor content. | ||
1013 | * @param {Object} [options] | ||
1014 | * @param {Boolean} [options.internal=false] Whether to suppress any event firing when copying data internally inside the editor. | ||
1015 | * @param {Function} [options.callback] Function to be called after `setData` is completed (on {@link #dataReady}). | ||
1016 | * @param {Boolean} [options.noSnapshot=false] If set to `true`, it will prevent recording an undo snapshot. | ||
1017 | * Introduced in CKEditor 4.4.2. | ||
1018 | */ | ||
1019 | setData: function( data, options, internal ) { | ||
1020 | var fireSnapshot = true, | ||
1021 | // Backward compatibility. | ||
1022 | callback = options, | ||
1023 | eventData; | ||
1024 | |||
1025 | if ( options && typeof options == 'object' ) { | ||
1026 | internal = options.internal; | ||
1027 | callback = options.callback; | ||
1028 | fireSnapshot = !options.noSnapshot; | ||
1029 | } | ||
1030 | |||
1031 | if ( !internal && fireSnapshot ) | ||
1032 | this.fire( 'saveSnapshot' ); | ||
1033 | |||
1034 | if ( callback || !internal ) { | ||
1035 | this.once( 'dataReady', function( evt ) { | ||
1036 | if ( !internal && fireSnapshot ) | ||
1037 | this.fire( 'saveSnapshot' ); | ||
1038 | |||
1039 | if ( callback ) | ||
1040 | callback.call( evt.editor ); | ||
1041 | } ); | ||
1042 | } | ||
1043 | |||
1044 | // Fire "setData" so data manipulation may happen. | ||
1045 | eventData = { dataValue: data }; | ||
1046 | !internal && this.fire( 'setData', eventData ); | ||
1047 | |||
1048 | this._.data = eventData.dataValue; | ||
1049 | |||
1050 | !internal && this.fire( 'afterSetData', eventData ); | ||
1051 | }, | ||
1052 | |||
1053 | /** | ||
1054 | * Puts or restores the editor into the read-only state. When in read-only, | ||
1055 | * the user is not able to change the editor content, but can still use | ||
1056 | * some editor features. This function sets the {@link #property-readOnly} | ||
1057 | * property of the editor, firing the {@link #event-readOnly} event. | ||
1058 | * | ||
1059 | * **Note:** The current editing area will be reloaded. | ||
1060 | * | ||
1061 | * @since 3.6 | ||
1062 | * @param {Boolean} [isReadOnly] Indicates that the editor must go | ||
1063 | * read-only (`true`, default) or be restored and made editable (`false`). | ||
1064 | */ | ||
1065 | setReadOnly: function( isReadOnly ) { | ||
1066 | isReadOnly = ( isReadOnly == null ) || isReadOnly; | ||
1067 | |||
1068 | if ( this.readOnly != isReadOnly ) { | ||
1069 | this.readOnly = isReadOnly; | ||
1070 | |||
1071 | // Block or release BACKSPACE key according to current read-only | ||
1072 | // state to prevent browser's history navigation (#9761). | ||
1073 | this.keystrokeHandler.blockedKeystrokes[ 8 ] = +isReadOnly; | ||
1074 | |||
1075 | this.editable().setReadOnly( isReadOnly ); | ||
1076 | |||
1077 | // Fire the readOnly event so the editor features can update | ||
1078 | // their state accordingly. | ||
1079 | this.fire( 'readOnly' ); | ||
1080 | } | ||
1081 | }, | ||
1082 | |||
1083 | /** | ||
1084 | * Inserts HTML code into the currently selected position in the editor in WYSIWYG mode. | ||
1085 | * | ||
1086 | * Example: | ||
1087 | * | ||
1088 | * CKEDITOR.instances.editor1.insertHtml( '<p>This is a new paragraph.</p>' ); | ||
1089 | * | ||
1090 | * Fires the {@link #event-insertHtml} and {@link #event-afterInsertHtml} events. The HTML is inserted | ||
1091 | * in the {@link #event-insertHtml} event's listener with a default priority (10) so you can add listeners with | ||
1092 | * lower or higher priorities in order to execute some code before or after the HTML is inserted. | ||
1093 | * | ||
1094 | * @param {String} html HTML code to be inserted into the editor. | ||
1095 | * @param {String} [mode='html'] The mode in which the HTML code will be inserted. One of the following: | ||
1096 | * | ||
1097 | * * `'html'` – The inserted content will completely override the styles at the selected position. | ||
1098 | * * `'unfiltered_html'` – Like `'html'` but the content is not filtered with {@link CKEDITOR.filter}. | ||
1099 | * * `'text'` – The inserted content will inherit the styles applied in | ||
1100 | * the selected position. This mode should be used when inserting "htmlified" plain text | ||
1101 | * (HTML without inline styles and styling elements like `<b>`, `<strong>`, `<span style="...">`). | ||
1102 | * | ||
1103 | * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range | ||
1104 | * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case). | ||
1105 | * Introduced in CKEditor 4.5. | ||
1106 | */ | ||
1107 | insertHtml: function( html, mode, range ) { | ||
1108 | this.fire( 'insertHtml', { dataValue: html, mode: mode, range: range } ); | ||
1109 | }, | ||
1110 | |||
1111 | /** | ||
1112 | * Inserts text content into the currently selected position in the | ||
1113 | * editor in WYSIWYG mode. The styles of the selected element will be applied to the inserted text. | ||
1114 | * Spaces around the text will be left untouched. | ||
1115 | * | ||
1116 | * CKEDITOR.instances.editor1.insertText( ' line1 \n\n line2' ); | ||
1117 | * | ||
1118 | * Fires the {@link #event-insertText} and {@link #event-afterInsertHtml} events. The text is inserted | ||
1119 | * in the {@link #event-insertText} event's listener with a default priority (10) so you can add listeners with | ||
1120 | * lower or higher priorities in order to execute some code before or after the text is inserted. | ||
1121 | * | ||
1122 | * @since 3.5 | ||
1123 | * @param {String} text Text to be inserted into the editor. | ||
1124 | */ | ||
1125 | insertText: function( text ) { | ||
1126 | this.fire( 'insertText', text ); | ||
1127 | }, | ||
1128 | |||
1129 | /** | ||
1130 | * Inserts an element into the currently selected position in the editor in WYSIWYG mode. | ||
1131 | * | ||
1132 | * var element = CKEDITOR.dom.element.createFromHtml( '<img src="hello.png" border="0" title="Hello" />' ); | ||
1133 | * CKEDITOR.instances.editor1.insertElement( element ); | ||
1134 | * | ||
1135 | * Fires the {@link #event-insertElement} event. The element is inserted in the listener with a default priority (10), | ||
1136 | * so you can add listeners with lower or higher priorities in order to execute some code before or after | ||
1137 | * the element is inserted. | ||
1138 | * | ||
1139 | * @param {CKEDITOR.dom.element} element The element to be inserted into the editor. | ||
1140 | */ | ||
1141 | insertElement: function( element ) { | ||
1142 | this.fire( 'insertElement', element ); | ||
1143 | }, | ||
1144 | |||
1145 | /** | ||
1146 | * Gets the selected HTML (it is returned as a {@link CKEDITOR.dom.documentFragment document fragment} | ||
1147 | * or a string). This method is designed to work as the user would expect the copy functionality to work. | ||
1148 | * For instance, if the following selection was made: | ||
1149 | * | ||
1150 | * <p>a<b>b{c}d</b>e</p> | ||
1151 | * | ||
1152 | * The following HTML will be returned: | ||
1153 | * | ||
1154 | * <b>c</b> | ||
1155 | * | ||
1156 | * As you can see, the information about the bold formatting was preserved, even though the selection was | ||
1157 | * anchored inside the `<b>` element. | ||
1158 | * | ||
1159 | * See also: | ||
1160 | * | ||
1161 | * * the {@link #extractSelectedHtml} method, | ||
1162 | * * the {@link CKEDITOR.editable#getHtmlFromRange} method. | ||
1163 | * | ||
1164 | * @since 4.5 | ||
1165 | * @param {Boolean} [toString] If `true`, then stringified HTML will be returned. | ||
1166 | * @returns {CKEDITOR.dom.documentFragment/String} | ||
1167 | */ | ||
1168 | getSelectedHtml: function( toString ) { | ||
1169 | var editable = this.editable(), | ||
1170 | selection = this.getSelection(), | ||
1171 | ranges = selection && selection.getRanges(); | ||
1172 | |||
1173 | if ( !editable || !ranges || ranges.length === 0 ) { | ||
1174 | return null; | ||
1175 | } | ||
1176 | |||
1177 | var docFragment = createDocumentFragmentFromRanges( ranges, editable ); | ||
1178 | |||
1179 | return toString ? docFragment.getHtml() : docFragment; | ||
1180 | }, | ||
1181 | |||
1182 | /** | ||
1183 | * Gets the selected HTML (it is returned as a {@link CKEDITOR.dom.documentFragment document fragment} | ||
1184 | * or a string) and removes the selected part of the DOM. This method is designed to work as the user would | ||
1185 | * expect the cut and delete functionalities to work. | ||
1186 | * | ||
1187 | * See also: | ||
1188 | * | ||
1189 | * * the {@link #getSelectedHtml} method, | ||
1190 | * * the {@link CKEDITOR.editable#extractHtmlFromRange} method. | ||
1191 | * | ||
1192 | * @since 4.5 | ||
1193 | * @param {Boolean} [toString] If `true`, then stringified HTML will be returned. | ||
1194 | * @param {Boolean} [removeEmptyBlock=false] Default `false` means that the function will keep an empty block (if the | ||
1195 | * entire content was removed) or it will create it (if a block element was removed) and set the selection in that block. | ||
1196 | * If `true`, the empty block will be removed or not created. In this case the function will not handle the selection. | ||
1197 | * @returns {CKEDITOR.dom.documentFragment/String/null} | ||
1198 | */ | ||
1199 | extractSelectedHtml: function( toString, removeEmptyBlock ) { | ||
1200 | var editable = this.editable(), | ||
1201 | ranges = this.getSelection().getRanges(); | ||
1202 | |||
1203 | if ( !editable || ranges.length === 0 ) { | ||
1204 | return null; | ||
1205 | } | ||
1206 | |||
1207 | var range = ranges[ 0 ], | ||
1208 | docFragment = editable.extractHtmlFromRange( range, removeEmptyBlock ); | ||
1209 | |||
1210 | if ( !removeEmptyBlock ) { | ||
1211 | this.getSelection().selectRanges( [ range ] ); | ||
1212 | } | ||
1213 | |||
1214 | return toString ? docFragment.getHtml() : docFragment; | ||
1215 | }, | ||
1216 | |||
1217 | /** | ||
1218 | * Moves the selection focus to the editing area space in the editor. | ||
1219 | */ | ||
1220 | focus: function() { | ||
1221 | this.fire( 'beforeFocus' ); | ||
1222 | }, | ||
1223 | |||
1224 | /** | ||
1225 | * Checks whether the current editor content contains changes when | ||
1226 | * compared to the content loaded into the editor at startup, or to | ||
1227 | * the content available in the editor when {@link #resetDirty} | ||
1228 | * was called. | ||
1229 | * | ||
1230 | * function beforeUnload( evt ) { | ||
1231 | * if ( CKEDITOR.instances.editor1.checkDirty() ) | ||
1232 | * return evt.returnValue = "You will lose the changes made in the editor."; | ||
1233 | * } | ||
1234 | * | ||
1235 | * if ( window.addEventListener ) | ||
1236 | * window.addEventListener( 'beforeunload', beforeUnload, false ); | ||
1237 | * else | ||
1238 | * window.attachEvent( 'onbeforeunload', beforeUnload ); | ||
1239 | * | ||
1240 | * @returns {Boolean} `true` if the content contains changes. | ||
1241 | */ | ||
1242 | checkDirty: function() { | ||
1243 | return this.status == 'ready' && this._.previousValue !== this.getSnapshot(); | ||
1244 | }, | ||
1245 | |||
1246 | /** | ||
1247 | * Resets the "dirty state" of the editor so subsequent calls to | ||
1248 | * {@link #checkDirty} will return `false` if the user will not | ||
1249 | * have made further changes to the content. | ||
1250 | * | ||
1251 | * alert( editor.checkDirty() ); // e.g. true | ||
1252 | * editor.resetDirty(); | ||
1253 | * alert( editor.checkDirty() ); // false | ||
1254 | */ | ||
1255 | resetDirty: function() { | ||
1256 | this._.previousValue = this.getSnapshot(); | ||
1257 | }, | ||
1258 | |||
1259 | /** | ||
1260 | * Updates the `<textarea>` element that was replaced by the editor with | ||
1261 | * the current data available in the editor. | ||
1262 | * | ||
1263 | * **Note:** This method will only affect those editor instances created | ||
1264 | * with the {@link CKEDITOR#ELEMENT_MODE_REPLACE} element mode or inline instances | ||
1265 | * bound to `<textarea>` elements. | ||
1266 | * | ||
1267 | * CKEDITOR.instances.editor1.updateElement(); | ||
1268 | * alert( document.getElementById( 'editor1' ).value ); // The current editor data. | ||
1269 | * | ||
1270 | * @see CKEDITOR.editor#element | ||
1271 | */ | ||
1272 | updateElement: function() { | ||
1273 | return updateEditorElement.call( this ); | ||
1274 | }, | ||
1275 | |||
1276 | /** | ||
1277 | * Assigns keystrokes associated with editor commands. | ||
1278 | * | ||
1279 | * editor.setKeystroke( CKEDITOR.CTRL + 115, 'save' ); // Assigned Ctrl+S to the "save" command. | ||
1280 | * editor.setKeystroke( CKEDITOR.CTRL + 115, false ); // Disabled Ctrl+S keystroke assignment. | ||
1281 | * editor.setKeystroke( [ | ||
1282 | * [ CKEDITOR.ALT + 122, false ], | ||
1283 | * [ CKEDITOR.CTRL + 121, 'link' ], | ||
1284 | * [ CKEDITOR.SHIFT + 120, 'bold' ] | ||
1285 | * ] ); | ||
1286 | * | ||
1287 | * This method may be used in the following cases: | ||
1288 | * | ||
1289 | * * By plugins (like `link` or `basicstyles`) to set their keystrokes when plugins are being loaded. | ||
1290 | * * During the runtime to modify existing keystrokes. | ||
1291 | * | ||
1292 | * The editor handles keystroke configuration in the following order: | ||
1293 | * | ||
1294 | * 1. Plugins use this method to define default keystrokes. | ||
1295 | * 2. Editor extends default keystrokes with {@link CKEDITOR.config#keystrokes}. | ||
1296 | * 3. Editor blocks keystrokes defined in {@link CKEDITOR.config#blockedKeystrokes}. | ||
1297 | * | ||
1298 | * You can then set new keystrokes using this method during the runtime. | ||
1299 | * | ||
1300 | * @since 4.0 | ||
1301 | * @param {Number/Array} keystroke A keystroke or an array of keystroke definitions. | ||
1302 | * @param {String/Boolean} [behavior] A command to be executed on the keystroke. | ||
1303 | */ | ||
1304 | setKeystroke: function() { | ||
1305 | var keystrokes = this.keystrokeHandler.keystrokes, | ||
1306 | newKeystrokes = CKEDITOR.tools.isArray( arguments[ 0 ] ) ? arguments[ 0 ] : [ [].slice.call( arguments, 0 ) ], | ||
1307 | keystroke, behavior; | ||
1308 | |||
1309 | for ( var i = newKeystrokes.length; i--; ) { | ||
1310 | keystroke = newKeystrokes[ i ]; | ||
1311 | behavior = 0; | ||
1312 | |||
1313 | // It may be a pair of: [ key, command ] | ||
1314 | if ( CKEDITOR.tools.isArray( keystroke ) ) { | ||
1315 | behavior = keystroke[ 1 ]; | ||
1316 | keystroke = keystroke[ 0 ]; | ||
1317 | } | ||
1318 | |||
1319 | if ( behavior ) | ||
1320 | keystrokes[ keystroke ] = behavior; | ||
1321 | else | ||
1322 | delete keystrokes[ keystroke ]; | ||
1323 | } | ||
1324 | }, | ||
1325 | |||
1326 | /** | ||
1327 | * Shorthand for {@link CKEDITOR.filter#addFeature}. | ||
1328 | * | ||
1329 | * @since 4.1 | ||
1330 | * @param {CKEDITOR.feature} feature See {@link CKEDITOR.filter#addFeature}. | ||
1331 | * @returns {Boolean} See {@link CKEDITOR.filter#addFeature}. | ||
1332 | */ | ||
1333 | addFeature: function( feature ) { | ||
1334 | return this.filter.addFeature( feature ); | ||
1335 | }, | ||
1336 | |||
1337 | /** | ||
1338 | * Sets the active filter ({@link #activeFilter}). Fires the {@link #activeFilterChange} event. | ||
1339 | * | ||
1340 | * // Set active filter which allows only 4 elements. | ||
1341 | * // Buttons like Bold, Italic will be disabled. | ||
1342 | * var filter = new CKEDITOR.filter( 'p strong em br' ); | ||
1343 | * editor.setActiveFilter( filter ); | ||
1344 | * | ||
1345 | * Setting a new filter will also change the {@link #setActiveEnterMode active Enter modes} to the first values | ||
1346 | * allowed by the new filter (see {@link CKEDITOR.filter#getAllowedEnterMode}). | ||
1347 | * | ||
1348 | * @since 4.3 | ||
1349 | * @param {CKEDITOR.filter} filter Filter instance or a falsy value (e.g. `null`) to reset to the default one. | ||
1350 | */ | ||
1351 | setActiveFilter: function( filter ) { | ||
1352 | if ( !filter ) | ||
1353 | filter = this.filter; | ||
1354 | |||
1355 | if ( this.activeFilter !== filter ) { | ||
1356 | this.activeFilter = filter; | ||
1357 | this.fire( 'activeFilterChange' ); | ||
1358 | |||
1359 | // Reset active filter to the main one - it resets enter modes, too. | ||
1360 | if ( filter === this.filter ) | ||
1361 | this.setActiveEnterMode( null, null ); | ||
1362 | else | ||
1363 | this.setActiveEnterMode( | ||
1364 | filter.getAllowedEnterMode( this.enterMode ), | ||
1365 | filter.getAllowedEnterMode( this.shiftEnterMode, true ) | ||
1366 | ); | ||
1367 | } | ||
1368 | }, | ||
1369 | |||
1370 | /** | ||
1371 | * Sets the active Enter modes: ({@link #enterMode} and {@link #shiftEnterMode}). | ||
1372 | * Fires the {@link #activeEnterModeChange} event. | ||
1373 | * | ||
1374 | * Prior to CKEditor 4.3 Enter modes were static and it was enough to check {@link CKEDITOR.config#enterMode} | ||
1375 | * and {@link CKEDITOR.config#shiftEnterMode} when implementing a feature which should depend on the Enter modes. | ||
1376 | * Since CKEditor 4.3 these options are source of initial: | ||
1377 | * | ||
1378 | * * static {@link #enterMode} and {@link #shiftEnterMode} values, | ||
1379 | * * dynamic {@link #activeEnterMode} and {@link #activeShiftEnterMode} values. | ||
1380 | * | ||
1381 | * However, the dynamic Enter modes can be changed during runtime by using this method, to reflect the selection context. | ||
1382 | * For example, if selection is moved to the {@link CKEDITOR.plugins.widget widget}'s nested editable which | ||
1383 | * is a {@link #blockless blockless one}, then the active Enter modes should be changed to {@link CKEDITOR#ENTER_BR} | ||
1384 | * (in this case [Widget System](#!/guide/dev_widgets) takes care of that). | ||
1385 | * | ||
1386 | * **Note:** This method should not be used to configure the editor – use {@link CKEDITOR.config#enterMode} and | ||
1387 | * {@link CKEDITOR.config#shiftEnterMode} instead. This method should only be used to dynamically change | ||
1388 | * Enter modes during runtime based on selection changes. | ||
1389 | * Keep in mind that changed Enter mode may be overwritten by another plugin/feature when it decided that | ||
1390 | * the changed context requires this. | ||
1391 | * | ||
1392 | * **Note:** In case of blockless editor (inline editor based on an element which cannot contain block elements | ||
1393 | * — see {@link CKEDITOR.editor#blockless}) only {@link CKEDITOR#ENTER_BR} is a valid Enter mode. Therefore | ||
1394 | * this method will not allow to set other values. | ||
1395 | * | ||
1396 | * **Note:** Changing the {@link #activeFilter active filter} may cause the Enter mode to change if default Enter modes | ||
1397 | * are not allowed by the new filter. | ||
1398 | * | ||
1399 | * @since 4.3 | ||
1400 | * @param {Number} enterMode One of {@link CKEDITOR#ENTER_P}, {@link CKEDITOR#ENTER_DIV}, {@link CKEDITOR#ENTER_BR}. | ||
1401 | * Pass falsy value (e.g. `null`) to reset the Enter mode to the default value ({@link #enterMode} and/or {@link #shiftEnterMode}). | ||
1402 | * @param {Number} shiftEnterMode See the `enterMode` argument. | ||
1403 | */ | ||
1404 | setActiveEnterMode: function( enterMode, shiftEnterMode ) { | ||
1405 | // Validate passed modes or use default ones (validated on init). | ||
1406 | enterMode = enterMode ? validateEnterMode( this, enterMode ) : this.enterMode; | ||
1407 | shiftEnterMode = shiftEnterMode ? validateEnterMode( this, shiftEnterMode ) : this.shiftEnterMode; | ||
1408 | |||
1409 | if ( this.activeEnterMode != enterMode || this.activeShiftEnterMode != shiftEnterMode ) { | ||
1410 | this.activeEnterMode = enterMode; | ||
1411 | this.activeShiftEnterMode = shiftEnterMode; | ||
1412 | this.fire( 'activeEnterModeChange' ); | ||
1413 | } | ||
1414 | }, | ||
1415 | |||
1416 | /** | ||
1417 | * Shows a notification to the user. | ||
1418 | * | ||
1419 | * If the [Notification](http://ckeditor.com/addons/notification) plugin is not enabled, this function shows | ||
1420 | * a normal alert with the given `message`. The `type` and `progressOrDuration` parameters are supported | ||
1421 | * only by the Notification plugin. | ||
1422 | * | ||
1423 | * If the Notification plugin is enabled, this method creates and shows a new notification. | ||
1424 | * By default the notification is shown over the editor content, in the viewport if it is possible. | ||
1425 | * | ||
1426 | * See {@link CKEDITOR.plugins.notification}. | ||
1427 | * | ||
1428 | * @since 4.5 | ||
1429 | * @member CKEDITOR.editor | ||
1430 | * @param {String} message The message displayed in the notification. | ||
1431 | * @param {String} [type='info'] The type of the notification. Can be `'info'`, `'warning'`, `'success'` or `'progress'`. | ||
1432 | * @param {Number} [progressOrDuration] If the type is `progress`, the third parameter may be a progress from `0` to `1` | ||
1433 | * (defaults to `0`). Otherwise the third parameter may be a notification duration denoting after how many milliseconds | ||
1434 | * the notification should be closed automatically. `0` means that the notification will not close automatically and the user | ||
1435 | * needs to close it manually. See {@link CKEDITOR.plugins.notification#duration}. | ||
1436 | * Note that `warning` notifications will not be closed automatically. | ||
1437 | * @returns {CKEDITOR.plugins.notification} Created and shown notification. | ||
1438 | */ | ||
1439 | showNotification: function( message ) { | ||
1440 | alert( message ); // jshint ignore:line | ||
1441 | } | ||
1442 | } ); | ||
1443 | } )(); | ||
1444 | |||
1445 | /** | ||
1446 | * The editor has no associated element. | ||
1447 | * | ||
1448 | * @readonly | ||
1449 | * @property {Number} [=0] | ||
1450 | * @member CKEDITOR | ||
1451 | */ | ||
1452 | CKEDITOR.ELEMENT_MODE_NONE = 0; | ||
1453 | |||
1454 | /** | ||
1455 | * The element is to be replaced by the editor instance. | ||
1456 | * | ||
1457 | * @readonly | ||
1458 | * @property {Number} [=1] | ||
1459 | * @member CKEDITOR | ||
1460 | */ | ||
1461 | CKEDITOR.ELEMENT_MODE_REPLACE = 1; | ||
1462 | |||
1463 | /** | ||
1464 | * The editor is to be created inside the element. | ||
1465 | * | ||
1466 | * @readonly | ||
1467 | * @property {Number} [=2] | ||
1468 | * @member CKEDITOR | ||
1469 | */ | ||
1470 | CKEDITOR.ELEMENT_MODE_APPENDTO = 2; | ||
1471 | |||
1472 | /** | ||
1473 | * The editor is to be attached to the element, using it as the editing block. | ||
1474 | * | ||
1475 | * @readonly | ||
1476 | * @property {Number} [=3] | ||
1477 | * @member CKEDITOR | ||
1478 | */ | ||
1479 | CKEDITOR.ELEMENT_MODE_INLINE = 3; | ||
1480 | |||
1481 | /** | ||
1482 | * Whether to escape HTML when the editor updates the original input element. | ||
1483 | * | ||
1484 | * config.htmlEncodeOutput = true; | ||
1485 | * | ||
1486 | * @since 3.1 | ||
1487 | * @cfg {Boolean} [htmlEncodeOutput=false] | ||
1488 | * @member CKEDITOR.config | ||
1489 | */ | ||
1490 | |||
1491 | /** | ||
1492 | * If `true`, makes the editor start in read-only state. Otherwise, it will check | ||
1493 | * if the linked `<textarea>` element has the `disabled` attribute. | ||
1494 | * | ||
1495 | * Read more in the [documentation](#!/guide/dev_readonly) | ||
1496 | * and see the [SDK sample](http://sdk.ckeditor.com/samples/readonly.html). | ||
1497 | * | ||
1498 | * config.readOnly = true; | ||
1499 | * | ||
1500 | * @since 3.6 | ||
1501 | * @cfg {Boolean} [readOnly=false] | ||
1502 | * @member CKEDITOR.config | ||
1503 | * @see CKEDITOR.editor#setReadOnly | ||
1504 | */ | ||
1505 | |||
1506 | /** | ||
1507 | * Whether an editable element should have focus when the editor is loading for the first time. | ||
1508 | * | ||
1509 | * config.startupFocus = true; | ||
1510 | * | ||
1511 | * @cfg {Boolean} [startupFocus=false] | ||
1512 | * @member CKEDITOR.config | ||
1513 | */ | ||
1514 | |||
1515 | /** | ||
1516 | * Customizes the {@link CKEDITOR.editor#title human-readable title} of this editor. This title is displayed in | ||
1517 | * tooltips and impacts various [accessibility aspects](#!/guide/dev_a11y-section-announcing-the-editor-on-the-page), | ||
1518 | * e.g. it is commonly used by screen readers for distinguishing editor instances and for navigation. | ||
1519 | * Accepted values are a string or `false`. | ||
1520 | * | ||
1521 | * **Note:** When `config.title` is set globally, the same value will be applied to all editor instances | ||
1522 | * loaded with this config. This may adversely affect accessibility as screen reader users will be unable | ||
1523 | * to distinguish particular editor instances and navigate between them. | ||
1524 | * | ||
1525 | * **Note:** Setting `config.title = false` may also impair accessibility in a similar way. | ||
1526 | * | ||
1527 | * **Note:** Please do not confuse this property with {@link CKEDITOR.editor#name} | ||
1528 | * which identifies the instance in the {@link CKEDITOR#instances} literal. | ||
1529 | * | ||
1530 | * // Sets the title to 'My WYSIWYG editor.'. The original title of the element (if it exists) | ||
1531 | * // will be restored once the editor instance is destroyed. | ||
1532 | * config.title = 'My WYSIWYG editor.'; | ||
1533 | * | ||
1534 | * // Do not touch the title. If the element already has a title, it remains unchanged. | ||
1535 | * // Also if no `title` attribute exists, nothing new will be added. | ||
1536 | * config.title = false; | ||
1537 | * | ||
1538 | * See also: | ||
1539 | * | ||
1540 | * * CKEDITOR.editor#name | ||
1541 | * * CKEDITOR.editor#title | ||
1542 | * | ||
1543 | * @since 4.2 | ||
1544 | * @cfg {String/Boolean} [title=based on editor.name] | ||
1545 | * @member CKEDITOR.config | ||
1546 | */ | ||
1547 | |||
1548 | /** | ||
1549 | * Sets listeners on editor events. | ||
1550 | * | ||
1551 | * **Note:** This property can only be set in the `config` object passed directly | ||
1552 | * to {@link CKEDITOR#replace}, {@link CKEDITOR#inline}, and other creators. | ||
1553 | * | ||
1554 | * CKEDITOR.replace( 'editor1', { | ||
1555 | * on: { | ||
1556 | * instanceReady: function() { | ||
1557 | * alert( this.name ); // 'editor1' | ||
1558 | * }, | ||
1559 | * | ||
1560 | * key: function() { | ||
1561 | * // ... | ||
1562 | * } | ||
1563 | * } | ||
1564 | * } ); | ||
1565 | * | ||
1566 | * @cfg {Object} on | ||
1567 | * @member CKEDITOR.config | ||
1568 | */ | ||
1569 | |||
1570 | /** | ||
1571 | * The outermost element in the DOM tree in which the editable element resides. It is provided | ||
1572 | * by a specific editor creator after the editor UI is created and is not intended to | ||
1573 | * be modified. | ||
1574 | * | ||
1575 | * var editor = CKEDITOR.instances.editor1; | ||
1576 | * alert( editor.container.getName() ); // 'span' | ||
1577 | * | ||
1578 | * @readonly | ||
1579 | * @property {CKEDITOR.dom.element} container | ||
1580 | */ | ||
1581 | |||
1582 | /** | ||
1583 | * The document that stores the editor content. | ||
1584 | * | ||
1585 | * * For the classic (`iframe`-based) editor it is equal to the document inside the | ||
1586 | * `iframe` containing the editable element. | ||
1587 | * * For the inline editor it is equal to {@link CKEDITOR#document}. | ||
1588 | * | ||
1589 | * The document object is available after the {@link #contentDom} event is fired | ||
1590 | * and may be invalidated when the {@link #contentDomUnload} event is fired | ||
1591 | * (classic editor only). | ||
1592 | * | ||
1593 | * editor.on( 'contentDom', function() { | ||
1594 | * console.log( editor.document ); | ||
1595 | * } ); | ||
1596 | * | ||
1597 | * @readonly | ||
1598 | * @property {CKEDITOR.dom.document} document | ||
1599 | */ | ||
1600 | |||
1601 | /** | ||
1602 | * The window instance related to the {@link #document} property. | ||
1603 | * | ||
1604 | * It is always equal to the `editor.document.getWindow()`. | ||
1605 | * | ||
1606 | * See the {@link #document} property documentation. | ||
1607 | * | ||
1608 | * @readonly | ||
1609 | * @property {CKEDITOR.dom.window} window | ||
1610 | */ | ||
1611 | |||
1612 | /** | ||
1613 | * The main filter instance used for input data filtering, data | ||
1614 | * transformations, and activation of features. | ||
1615 | * | ||
1616 | * It points to a {@link CKEDITOR.filter} instance set up based on | ||
1617 | * editor configuration. | ||
1618 | * | ||
1619 | * @since 4.1 | ||
1620 | * @readonly | ||
1621 | * @property {CKEDITOR.filter} filter | ||
1622 | */ | ||
1623 | |||
1624 | /** | ||
1625 | * The active filter instance which should be used in the current context (location selection). | ||
1626 | * This instance will be used to make a decision which commands, buttons and other | ||
1627 | * {@link CKEDITOR.feature features} can be enabled. | ||
1628 | * | ||
1629 | * By default it equals the {@link #filter} and it can be changed by the {@link #setActiveFilter} method. | ||
1630 | * | ||
1631 | * editor.on( 'activeFilterChange', function() { | ||
1632 | * if ( editor.activeFilter.check( 'cite' ) ) | ||
1633 | * // Do something when <cite> was enabled - e.g. enable a button. | ||
1634 | * else | ||
1635 | * // Otherwise do something else. | ||
1636 | * } ); | ||
1637 | * | ||
1638 | * See also the {@link #setActiveEnterMode} method for an explanation of dynamic settings. | ||
1639 | * | ||
1640 | * @since 4.3 | ||
1641 | * @readonly | ||
1642 | * @property {CKEDITOR.filter} activeFilter | ||
1643 | */ | ||
1644 | |||
1645 | /** | ||
1646 | * The main (static) Enter mode which is a validated version of the {@link CKEDITOR.config#enterMode} setting. | ||
1647 | * Currently only one rule exists — {@link #blockless blockless editors} may have | ||
1648 | * Enter modes set only to {@link CKEDITOR#ENTER_BR}. | ||
1649 | * | ||
1650 | * @since 4.3 | ||
1651 | * @readonly | ||
1652 | * @property {Number} enterMode | ||
1653 | */ | ||
1654 | |||
1655 | /** | ||
1656 | * See the {@link #enterMode} property. | ||
1657 | * | ||
1658 | * @since 4.3 | ||
1659 | * @readonly | ||
1660 | * @property {Number} shiftEnterMode | ||
1661 | */ | ||
1662 | |||
1663 | /** | ||
1664 | * The dynamic Enter mode which should be used in the current context (selection location). | ||
1665 | * By default it equals the {@link #enterMode} and it can be changed by the {@link #setActiveEnterMode} method. | ||
1666 | * | ||
1667 | * See also the {@link #setActiveEnterMode} method for an explanation of dynamic settings. | ||
1668 | * | ||
1669 | * @since 4.3 | ||
1670 | * @readonly | ||
1671 | * @property {Number} activeEnterMode | ||
1672 | */ | ||
1673 | |||
1674 | /** | ||
1675 | * See the {@link #activeEnterMode} property. | ||
1676 | * | ||
1677 | * @since 4.3 | ||
1678 | * @readonly | ||
1679 | * @property {Number} activeShiftEnterMode | ||
1680 | */ | ||
1681 | |||
1682 | /** | ||
1683 | * Event fired by the {@link #setActiveFilter} method when the {@link #activeFilter} is changed. | ||
1684 | * | ||
1685 | * @since 4.3 | ||
1686 | * @event activeFilterChange | ||
1687 | */ | ||
1688 | |||
1689 | /** | ||
1690 | * Event fired by the {@link #setActiveEnterMode} method when any of the active Enter modes is changed. | ||
1691 | * See also the {@link #activeEnterMode} and {@link #activeShiftEnterMode} properties. | ||
1692 | * | ||
1693 | * @since 4.3 | ||
1694 | * @event activeEnterModeChange | ||
1695 | */ | ||
1696 | |||
1697 | /** | ||
1698 | * Event fired when a CKEDITOR instance is created, but still before initializing it. | ||
1699 | * To interact with a fully initialized instance, use the | ||
1700 | * {@link CKEDITOR#instanceReady} event instead. | ||
1701 | * | ||
1702 | * @event instanceCreated | ||
1703 | * @member CKEDITOR | ||
1704 | * @param {CKEDITOR.editor} editor The editor instance that has been created. | ||
1705 | */ | ||
1706 | |||
1707 | /** | ||
1708 | * Event fired when CKEDITOR instance's components (configuration, languages and plugins) are fully | ||
1709 | * loaded and initialized. However, the editor will be fully ready for interaction | ||
1710 | * on {@link CKEDITOR#instanceReady}. | ||
1711 | * | ||
1712 | * @event instanceLoaded | ||
1713 | * @member CKEDITOR | ||
1714 | * @param {CKEDITOR.editor} editor This editor instance that has been loaded. | ||
1715 | */ | ||
1716 | |||
1717 | /** | ||
1718 | * Event fired when a CKEDITOR instance is destroyed. | ||
1719 | * | ||
1720 | * @event instanceDestroyed | ||
1721 | * @member CKEDITOR | ||
1722 | * @param {CKEDITOR.editor} editor The editor instance that has been destroyed. | ||
1723 | */ | ||
1724 | |||
1725 | /** | ||
1726 | * Event fired when a CKEDITOR instance is created, fully initialized and ready for interaction. | ||
1727 | * | ||
1728 | * @event instanceReady | ||
1729 | * @member CKEDITOR | ||
1730 | * @param {CKEDITOR.editor} editor The editor instance that has been created. | ||
1731 | */ | ||
1732 | |||
1733 | /** | ||
1734 | * Event fired when the language is loaded into the editor instance. | ||
1735 | * | ||
1736 | * @since 3.6.1 | ||
1737 | * @event langLoaded | ||
1738 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1739 | */ | ||
1740 | |||
1741 | /** | ||
1742 | * Event fired when all plugins are loaded and initialized into the editor instance. | ||
1743 | * | ||
1744 | * @event pluginsLoaded | ||
1745 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1746 | */ | ||
1747 | |||
1748 | /** | ||
1749 | * Event fired when the styles set is loaded. During the editor initialization | ||
1750 | * phase the {@link #getStylesSet} method returns only styles that | ||
1751 | * are already loaded, which may not include e.g. styles parsed | ||
1752 | * by the `stylesheetparser` plugin. Thus, to be notified when all | ||
1753 | * styles are ready, you can listen on this event. | ||
1754 | * | ||
1755 | * @since 4.1 | ||
1756 | * @event stylesSet | ||
1757 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1758 | * @param {Array} styles An array of styles definitions. | ||
1759 | */ | ||
1760 | |||
1761 | /** | ||
1762 | * Event fired before the command execution when {@link #execCommand} is called. | ||
1763 | * | ||
1764 | * @event beforeCommandExec | ||
1765 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1766 | * @param data | ||
1767 | * @param {String} data.name The command name. | ||
1768 | * @param {Object} data.commandData The data to be sent to the command. This | ||
1769 | * can be manipulated by the event listener. | ||
1770 | * @param {CKEDITOR.command} data.command The command itself. | ||
1771 | */ | ||
1772 | |||
1773 | /** | ||
1774 | * Event fired after the command execution when {@link #execCommand} is called. | ||
1775 | * | ||
1776 | * @event afterCommandExec | ||
1777 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1778 | * @param data | ||
1779 | * @param {String} data.name The command name. | ||
1780 | * @param {Object} data.commandData The data sent to the command. | ||
1781 | * @param {CKEDITOR.command} data.command The command itself. | ||
1782 | * @param {Object} data.returnValue The value returned by the command execution. | ||
1783 | */ | ||
1784 | |||
1785 | /** | ||
1786 | * Event fired when a custom configuration file is loaded, before the final | ||
1787 | * configuration initialization. | ||
1788 | * | ||
1789 | * Custom configuration files can be loaded thorugh the | ||
1790 | * {@link CKEDITOR.config#customConfig} setting. Several files can be loaded | ||
1791 | * by changing this setting. | ||
1792 | * | ||
1793 | * @event customConfigLoaded | ||
1794 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1795 | */ | ||
1796 | |||
1797 | /** | ||
1798 | * Event fired once the editor configuration is ready (loaded and processed). | ||
1799 | * | ||
1800 | * @event configLoaded | ||
1801 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1802 | */ | ||
1803 | |||
1804 | /** | ||
1805 | * Event fired when this editor instance is destroyed. The editor at this | ||
1806 | * point is not usable and this event should be used to perform the clean-up | ||
1807 | * in any plugin. | ||
1808 | * | ||
1809 | * @event destroy | ||
1810 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1811 | */ | ||
1812 | |||
1813 | /** | ||
1814 | * Internal event to get the current data. | ||
1815 | * | ||
1816 | * @event beforeGetData | ||
1817 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1818 | */ | ||
1819 | |||
1820 | /** | ||
1821 | * Internal event to perform the {@link #method-getSnapshot} call. | ||
1822 | * | ||
1823 | * @event getSnapshot | ||
1824 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1825 | */ | ||
1826 | |||
1827 | /** | ||
1828 | * Internal event to perform the {@link #method-loadSnapshot} call. | ||
1829 | * | ||
1830 | * @event loadSnapshot | ||
1831 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1832 | * @param {String} data The data that will be used. | ||
1833 | */ | ||
1834 | |||
1835 | /** | ||
1836 | * Event fired before the {@link #method-getData} call returns, allowing for additional manipulation. | ||
1837 | * | ||
1838 | * @event getData | ||
1839 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1840 | * @param data | ||
1841 | * @param {String} data.dataValue The data that will be returned. | ||
1842 | */ | ||
1843 | |||
1844 | /** | ||
1845 | * Event fired before the {@link #method-setData} call is executed, allowing for additional manipulation. | ||
1846 | * | ||
1847 | * @event setData | ||
1848 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1849 | * @param data | ||
1850 | * @param {String} data.dataValue The data that will be used. | ||
1851 | */ | ||
1852 | |||
1853 | /** | ||
1854 | * Event fired at the end of the {@link #method-setData} call execution. Usually it is better to use the | ||
1855 | * {@link #dataReady} event. | ||
1856 | * | ||
1857 | * @event afterSetData | ||
1858 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1859 | * @param data | ||
1860 | * @param {String} data.dataValue The data that has been set. | ||
1861 | */ | ||
1862 | |||
1863 | /** | ||
1864 | * Event fired as an indicator of the editor data loading. It may be the result of | ||
1865 | * calling {@link #method-setData} explicitly or an internal | ||
1866 | * editor function, like the editor editing mode switching (move to Source and back). | ||
1867 | * | ||
1868 | * @event dataReady | ||
1869 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1870 | */ | ||
1871 | |||
1872 | /** | ||
1873 | * Event fired when the CKEDITOR instance is completely created, fully initialized | ||
1874 | * and ready for interaction. | ||
1875 | * | ||
1876 | * @event instanceReady | ||
1877 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1878 | */ | ||
1879 | |||
1880 | /** | ||
1881 | * Event fired when editor components (configuration, languages and plugins) are fully | ||
1882 | * loaded and initialized. However, the editor will be fully ready to for interaction | ||
1883 | * on {@link #instanceReady}. | ||
1884 | * | ||
1885 | * @event loaded | ||
1886 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1887 | */ | ||
1888 | |||
1889 | /** | ||
1890 | * Event fired by the {@link #method-insertHtml} method. See the method documentation for more information | ||
1891 | * about how this event can be used. | ||
1892 | * | ||
1893 | * @event insertHtml | ||
1894 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1895 | * @param data | ||
1896 | * @param {String} data.mode The mode in which the data is inserted (see {@link #method-insertHtml}). | ||
1897 | * @param {String} data.dataValue The HTML code to insert. | ||
1898 | * @param {CKEDITOR.dom.range} [data.range] See {@link #method-insertHtml}'s `range` parameter. | ||
1899 | */ | ||
1900 | |||
1901 | /** | ||
1902 | * Event fired by the {@link #method-insertText} method. See the method documentation for more information | ||
1903 | * about how this event can be used. | ||
1904 | * | ||
1905 | * @event insertText | ||
1906 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1907 | * @param {String} data The text to insert. | ||
1908 | */ | ||
1909 | |||
1910 | /** | ||
1911 | * Event fired by the {@link #method-insertElement} method. See the method documentation for more information | ||
1912 | * about how this event can be used. | ||
1913 | * | ||
1914 | * @event insertElement | ||
1915 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1916 | * @param {CKEDITOR.dom.element} data The element to insert. | ||
1917 | */ | ||
1918 | |||
1919 | /** | ||
1920 | * Event fired after data insertion using the {@link #method-insertHtml}, {@link CKEDITOR.editable#insertHtml}, | ||
1921 | * or {@link CKEDITOR.editable#insertHtmlIntoRange} methods. | ||
1922 | * | ||
1923 | * @since 4.5 | ||
1924 | * @event afterInsertHtml | ||
1925 | * @param data | ||
1926 | * @param {CKEDITOR.dom.range} [data.intoRange] If set, the HTML was not inserted into the current selection, but into | ||
1927 | * the specified range. This property is set if the {@link CKEDITOR.editable#insertHtmlIntoRange} method was used, | ||
1928 | * but not if for the {@link CKEDITOR.editable#insertHtml} method. | ||
1929 | */ | ||
1930 | |||
1931 | /** | ||
1932 | * Event fired after the {@link #property-readOnly} property changes. | ||
1933 | * | ||
1934 | * @since 3.6 | ||
1935 | * @event readOnly | ||
1936 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1937 | */ | ||
1938 | |||
1939 | /** | ||
1940 | * Event fired when a UI template is added to the editor instance. It makes | ||
1941 | * it possible to bring customizations to the template source. | ||
1942 | * | ||
1943 | * @event template | ||
1944 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1945 | * @param data | ||
1946 | * @param {String} data.name The template name. | ||
1947 | * @param {String} data.source The source data for this template. | ||
1948 | */ | ||
1949 | |||
1950 | /** | ||
1951 | * Event fired when the editor content (its DOM structure) is ready. | ||
1952 | * It is similar to the native `DOMContentLoaded` event, but it applies to | ||
1953 | * the editor content. It is also the first event fired after | ||
1954 | * the {@link CKEDITOR.editable} is initialized. | ||
1955 | * | ||
1956 | * This event is particularly important for classic (`iframe`-based) | ||
1957 | * editor, because on editor initialization and every time the data are set | ||
1958 | * (by {@link CKEDITOR.editor#method-setData}) content DOM structure | ||
1959 | * is rebuilt. Thus, e.g. you need to attach DOM event listeners | ||
1960 | * on editable one more time. | ||
1961 | * | ||
1962 | * For inline editor this event is fired only once — when the | ||
1963 | * editor is initialized for the first time. This is because setting | ||
1964 | * editor content does not cause editable destruction and creation. | ||
1965 | * | ||
1966 | * The {@link #contentDom} event goes along with {@link #contentDomUnload} | ||
1967 | * which is fired before the content DOM structure is destroyed. This is the | ||
1968 | * right moment to detach content DOM event listener. Otherwise | ||
1969 | * browsers like IE or Opera may throw exceptions when accessing | ||
1970 | * elements from the detached document. | ||
1971 | * | ||
1972 | * **Note:** {@link CKEDITOR.editable#attachListener} is a convenient | ||
1973 | * way to attach listeners that will be detached on {@link #contentDomUnload}. | ||
1974 | * | ||
1975 | * editor.on( 'contentDom', function() { | ||
1976 | * var editable = editor.editable(); | ||
1977 | * | ||
1978 | * editable.attachListener( editable, 'click', function() { | ||
1979 | * console.log( 'The editable was clicked.' ); | ||
1980 | * }); | ||
1981 | * }); | ||
1982 | * | ||
1983 | * @event contentDom | ||
1984 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1985 | */ | ||
1986 | |||
1987 | /** | ||
1988 | * Event fired before the content DOM structure is destroyed. | ||
1989 | * See {@link #contentDom} documentation for more details. | ||
1990 | * | ||
1991 | * @event contentDomUnload | ||
1992 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1993 | */ | ||
1994 | |||
1995 | /** | ||
1996 | * Event fired when the content DOM changes and some of the references as well as | ||
1997 | * the native DOM event listeners could be lost. | ||
1998 | * This event is useful when it is important to keep track of references | ||
1999 | * to elements in the editable content from code. | ||
2000 | * | ||
2001 | * @since 4.3 | ||
2002 | * @event contentDomInvalidated | ||
2003 | * @param {CKEDITOR.editor} editor This editor instance. | ||
2004 | */ | ||
diff --git a/sources/core/editor_basic.js b/sources/core/editor_basic.js new file mode 100644 index 0000000..14f3446 --- /dev/null +++ b/sources/core/editor_basic.js | |||
@@ -0,0 +1,36 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | if ( !CKEDITOR.editor ) { | ||
7 | // Documented at editor.js. | ||
8 | CKEDITOR.editor = function() { | ||
9 | // Push this editor to the pending list. It'll be processed later once | ||
10 | // the full editor code is loaded. | ||
11 | CKEDITOR._.pending.push( [ this, arguments ] ); | ||
12 | |||
13 | // Call the CKEDITOR.event constructor to initialize this instance. | ||
14 | CKEDITOR.event.call( this ); | ||
15 | }; | ||
16 | |||
17 | // Both fire and fireOnce will always pass this editor instance as the | ||
18 | // "editor" param in CKEDITOR.event.fire. So, we override it to do that | ||
19 | // automaticaly. | ||
20 | CKEDITOR.editor.prototype.fire = function( eventName, data ) { | ||
21 | if ( eventName in { instanceReady: 1, loaded: 1 } ) | ||
22 | this[ eventName ] = true; | ||
23 | |||
24 | return CKEDITOR.event.prototype.fire.call( this, eventName, data, this ); | ||
25 | }; | ||
26 | |||
27 | CKEDITOR.editor.prototype.fireOnce = function( eventName, data ) { | ||
28 | if ( eventName in { instanceReady: 1, loaded: 1 } ) | ||
29 | this[ eventName ] = true; | ||
30 | |||
31 | return CKEDITOR.event.prototype.fireOnce.call( this, eventName, data, this ); | ||
32 | }; | ||
33 | |||
34 | // "Inherit" (copy actually) from CKEDITOR.event. | ||
35 | CKEDITOR.event.implementOn( CKEDITOR.editor.prototype ); | ||
36 | } | ||
diff --git a/sources/core/env.js b/sources/core/env.js new file mode 100644 index 0000000..4410ce9 --- /dev/null +++ b/sources/core/env.js | |||
@@ -0,0 +1,361 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.env} object which contains | ||
8 | * environment and browser information. | ||
9 | */ | ||
10 | |||
11 | if ( !CKEDITOR.env ) { | ||
12 | /** | ||
13 | * Environment and browser information. | ||
14 | * | ||
15 | * @class CKEDITOR.env | ||
16 | * @singleton | ||
17 | */ | ||
18 | CKEDITOR.env = ( function() { | ||
19 | var agent = navigator.userAgent.toLowerCase(), | ||
20 | edge = agent.match( /edge[ \/](\d+.?\d*)/ ), | ||
21 | trident = agent.indexOf( 'trident/' ) > -1, | ||
22 | ie = !!( edge || trident ); | ||
23 | |||
24 | var env = { | ||
25 | /** | ||
26 | * Indicates that CKEditor is running in Internet Explorer. | ||
27 | * | ||
28 | * if ( CKEDITOR.env.ie ) | ||
29 | * alert( 'I\'m running in IE!' ); | ||
30 | * | ||
31 | * **Note:** This property is also set to `true` if CKEditor is running | ||
32 | * in {@link #edge Microsoft Edge}. | ||
33 | * | ||
34 | * @property {Boolean} | ||
35 | */ | ||
36 | ie: ie, | ||
37 | |||
38 | /** | ||
39 | * Indicates that CKEditor is running in Microsoft Edge. | ||
40 | * | ||
41 | * if ( CKEDITOR.env.edge ) | ||
42 | * alert( 'I\'m running in Edge!' ); | ||
43 | * | ||
44 | * See also {@link #ie}. | ||
45 | * | ||
46 | * @since 4.5 | ||
47 | * @property {Boolean} | ||
48 | */ | ||
49 | edge: !!edge, | ||
50 | |||
51 | /** | ||
52 | * Indicates that CKEditor is running in a WebKit-based browser, like Safari, | ||
53 | * or Blink-based browser, like Chrome. | ||
54 | * | ||
55 | * if ( CKEDITOR.env.webkit ) | ||
56 | * alert( 'I\'m running in a WebKit browser!' ); | ||
57 | * | ||
58 | * @property {Boolean} | ||
59 | */ | ||
60 | webkit: !ie && ( agent.indexOf( ' applewebkit/' ) > -1 ), | ||
61 | |||
62 | /** | ||
63 | * Indicates that CKEditor is running in Adobe AIR. | ||
64 | * | ||
65 | * if ( CKEDITOR.env.air ) | ||
66 | * alert( 'I\'m on AIR!' ); | ||
67 | * | ||
68 | * @property {Boolean} | ||
69 | */ | ||
70 | air: ( agent.indexOf( ' adobeair/' ) > -1 ), | ||
71 | |||
72 | /** | ||
73 | * Indicates that CKEditor is running on Macintosh. | ||
74 | * | ||
75 | * if ( CKEDITOR.env.mac ) | ||
76 | * alert( 'I love apples!'' ); | ||
77 | * | ||
78 | * @property {Boolean} | ||
79 | */ | ||
80 | mac: ( agent.indexOf( 'macintosh' ) > -1 ), | ||
81 | |||
82 | /** | ||
83 | * Indicates that CKEditor is running in a Quirks Mode environment. | ||
84 | * | ||
85 | * if ( CKEDITOR.env.quirks ) | ||
86 | * alert( 'Nooooo!' ); | ||
87 | * | ||
88 | * Internet Explorer 10 introduced the _New Quirks Mode_, which is similar to the _Quirks Mode_ | ||
89 | * implemented in other modern browsers and defined in the HTML5 specification. It can be handled | ||
90 | * as the Standards mode, so the value of this property will be set to `false`. | ||
91 | * | ||
92 | * The _Internet Explorer 5 Quirks_ mode which is still available in Internet Explorer 10+ | ||
93 | * sets this value to `true` and {@link #version} to `7`. | ||
94 | * | ||
95 | * Read more: [IEBlog](http://blogs.msdn.com/b/ie/archive/2011/12/14/interoperable-html5-quirks-mode-in-ie10.aspx) | ||
96 | * | ||
97 | * @property {Boolean} | ||
98 | */ | ||
99 | quirks: ( document.compatMode == 'BackCompat' && ( !document.documentMode || document.documentMode < 10 ) ), | ||
100 | |||
101 | /** | ||
102 | * Indicates that CKEditor is running in a mobile environemnt. | ||
103 | * | ||
104 | * if ( CKEDITOR.env.mobile ) | ||
105 | * alert( 'I\'m running with CKEditor today!' ); | ||
106 | * | ||
107 | * @deprecated | ||
108 | * @property {Boolean} | ||
109 | */ | ||
110 | mobile: ( agent.indexOf( 'mobile' ) > -1 ), | ||
111 | |||
112 | /** | ||
113 | * Indicates that CKEditor is running on Apple iPhone/iPad/iPod devices. | ||
114 | * | ||
115 | * if ( CKEDITOR.env.iOS ) | ||
116 | * alert( 'I like little apples!' ); | ||
117 | * | ||
118 | * @property {Boolean} | ||
119 | */ | ||
120 | iOS: /(ipad|iphone|ipod)/.test( agent ), | ||
121 | |||
122 | /** | ||
123 | * Indicates that the browser has a custom domain enabled. This has | ||
124 | * been set with `document.domain`. | ||
125 | * | ||
126 | * if ( CKEDITOR.env.isCustomDomain() ) | ||
127 | * alert( 'I\'m in a custom domain!' ); | ||
128 | * | ||
129 | * @returns {Boolean} `true` if a custom domain is enabled. | ||
130 | * @deprecated | ||
131 | */ | ||
132 | isCustomDomain: function() { | ||
133 | if ( !this.ie ) | ||
134 | return false; | ||
135 | |||
136 | var domain = document.domain, | ||
137 | hostname = window.location.hostname; | ||
138 | |||
139 | return domain != hostname && domain != ( '[' + hostname + ']' ); // IPv6 IP support (#5434) | ||
140 | }, | ||
141 | |||
142 | /** | ||
143 | * Indicates that the page is running under an encrypted connection. | ||
144 | * | ||
145 | * if ( CKEDITOR.env.secure ) | ||
146 | * alert( 'I\'m on SSL!' ); | ||
147 | * | ||
148 | * @returns {Boolean} `true` if the page has an encrypted connection. | ||
149 | */ | ||
150 | secure: location.protocol == 'https:' | ||
151 | }; | ||
152 | |||
153 | /** | ||
154 | * Indicates that CKEditor is running in a Gecko-based browser, like | ||
155 | * Firefox. | ||
156 | * | ||
157 | * if ( CKEDITOR.env.gecko ) | ||
158 | * alert( 'I\'m riding a gecko!' ); | ||
159 | * | ||
160 | * @property {Boolean} | ||
161 | */ | ||
162 | env.gecko = ( navigator.product == 'Gecko' && !env.webkit && !env.ie ); | ||
163 | |||
164 | /** | ||
165 | * Indicates that CKEditor is running in a Blink-based browser like Chrome. | ||
166 | * | ||
167 | * if ( CKEDITOR.env.chrome ) | ||
168 | * alert( 'I\'m running in Chrome!' ); | ||
169 | * | ||
170 | * @property {Boolean} chrome | ||
171 | */ | ||
172 | |||
173 | /** | ||
174 | * Indicates that CKEditor is running in Safari (including the mobile version). | ||
175 | * | ||
176 | * if ( CKEDITOR.env.safari ) | ||
177 | * alert( 'I\'m on Safari!' ); | ||
178 | * | ||
179 | * @property {Boolean} safari | ||
180 | */ | ||
181 | if ( env.webkit ) { | ||
182 | if ( agent.indexOf( 'chrome' ) > -1 ) | ||
183 | env.chrome = true; | ||
184 | else | ||
185 | env.safari = true; | ||
186 | } | ||
187 | |||
188 | var version = 0; | ||
189 | |||
190 | // Internet Explorer 6.0+ | ||
191 | if ( env.ie ) { | ||
192 | // We use env.version for feature detection, so set it properly. | ||
193 | if ( edge ) { | ||
194 | version = parseFloat( edge[ 1 ] ); | ||
195 | } else if ( env.quirks || !document.documentMode ) { | ||
196 | version = parseFloat( agent.match( /msie (\d+)/ )[ 1 ] ); | ||
197 | } else { | ||
198 | version = document.documentMode; | ||
199 | } | ||
200 | |||
201 | // Deprecated features available just for backwards compatibility. | ||
202 | env.ie9Compat = version == 9; | ||
203 | env.ie8Compat = version == 8; | ||
204 | env.ie7Compat = version == 7; | ||
205 | env.ie6Compat = version < 7 || env.quirks; | ||
206 | |||
207 | /** | ||
208 | * Indicates that CKEditor is running in an IE6-like environment, which | ||
209 | * includes IE6 itself as well as IE7, IE8 and IE9 in Quirks Mode. | ||
210 | * | ||
211 | * @deprecated | ||
212 | * @property {Boolean} ie6Compat | ||
213 | */ | ||
214 | |||
215 | /** | ||
216 | * Indicates that CKEditor is running in an IE7-like environment, which | ||
217 | * includes IE7 itself and IE8's IE7 Document Mode. | ||
218 | * | ||
219 | * @deprecated | ||
220 | * @property {Boolean} ie7Compat | ||
221 | */ | ||
222 | |||
223 | /** | ||
224 | * Indicates that CKEditor is running in Internet Explorer 8 on | ||
225 | * Standards Mode. | ||
226 | * | ||
227 | * @deprecated | ||
228 | * @property {Boolean} ie8Compat | ||
229 | */ | ||
230 | |||
231 | /** | ||
232 | * Indicates that CKEditor is running in Internet Explorer 9 on | ||
233 | * Standards Mode. | ||
234 | * | ||
235 | * @deprecated | ||
236 | * @property {Boolean} ie9Compat | ||
237 | */ | ||
238 | } | ||
239 | |||
240 | // Gecko. | ||
241 | if ( env.gecko ) { | ||
242 | var geckoRelease = agent.match( /rv:([\d\.]+)/ ); | ||
243 | if ( geckoRelease ) { | ||
244 | geckoRelease = geckoRelease[ 1 ].split( '.' ); | ||
245 | version = geckoRelease[ 0 ] * 10000 + ( geckoRelease[ 1 ] || 0 ) * 100 + ( geckoRelease[ 2 ] || 0 ) * 1; | ||
246 | } | ||
247 | } | ||
248 | |||
249 | // Adobe AIR 1.0+ | ||
250 | // Checked before Safari because AIR have the WebKit rich text editor | ||
251 | // features from Safari 3.0.4, but the version reported is 420. | ||
252 | if ( env.air ) | ||
253 | version = parseFloat( agent.match( / adobeair\/(\d+)/ )[ 1 ] ); | ||
254 | |||
255 | // WebKit 522+ (Safari 3+) | ||
256 | if ( env.webkit ) | ||
257 | version = parseFloat( agent.match( / applewebkit\/(\d+)/ )[ 1 ] ); | ||
258 | |||
259 | /** | ||
260 | * Contains the browser version. | ||
261 | * | ||
262 | * For Gecko-based browsers (like Firefox) it contains the revision | ||
263 | * number with first three parts concatenated with a padding zero | ||
264 | * (e.g. for revision 1.9.0.2 we have 10900). | ||
265 | * | ||
266 | * For WebKit-based browsers (like Safari and Chrome) it contains the | ||
267 | * WebKit build version (e.g. 522). | ||
268 | * | ||
269 | * For IE browsers, it matches the "Document Mode". | ||
270 | * | ||
271 | * if ( CKEDITOR.env.ie && CKEDITOR.env.version <= 6 ) | ||
272 | * alert( 'Ouch!' ); | ||
273 | * | ||
274 | * @property {Number} | ||
275 | */ | ||
276 | env.version = version; | ||
277 | |||
278 | /** | ||
279 | * Since CKEditor 4.5 this property is a blacklist of browsers incompatible with CKEditor. It means that it is | ||
280 | * set to `false` only in browsers that are known to be incompatible. Before CKEditor 4.5 this | ||
281 | * property was a whitelist of browsers that were known to be compatible with CKEditor. | ||
282 | * | ||
283 | * The reason for this change is the rising fragmentation of the browser market (especially the mobile segment). | ||
284 | * It became too complicated to check in which new environments CKEditor is going to work. | ||
285 | * | ||
286 | * In order to enable CKEditor 4.4.x and below in unsupported environments see the | ||
287 | * [Enabling CKEditor in Unsupported Environments](#!/guide/dev_unsupported_environments) article. | ||
288 | * | ||
289 | * if ( CKEDITOR.env.isCompatible ) | ||
290 | * alert( 'Your browser is not known to be incompatible with CKEditor!' ); | ||
291 | * | ||
292 | * @property {Boolean} | ||
293 | */ | ||
294 | env.isCompatible = | ||
295 | // IE 7+ (IE 7 is not supported, but IE Compat Mode is and it is recognized as IE7). | ||
296 | !( env.ie && version < 7 ) && | ||
297 | // Firefox 4.0+. | ||
298 | !( env.gecko && version < 40000 ) && | ||
299 | // Chrome 6+, Safari 5.1+, iOS 5+. | ||
300 | !( env.webkit && version < 534 ); | ||
301 | |||
302 | /** | ||
303 | * Indicates that CKEditor is running in the HiDPI environment. | ||
304 | * | ||
305 | * if ( CKEDITOR.env.hidpi ) | ||
306 | * alert( 'You are using a screen with high pixel density.' ); | ||
307 | * | ||
308 | * @property {Boolean} | ||
309 | */ | ||
310 | env.hidpi = window.devicePixelRatio >= 2; | ||
311 | |||
312 | /** | ||
313 | * Indicates that CKEditor is running in a browser which uses a bogus | ||
314 | * `<br>` filler in order to correctly display caret in empty blocks. | ||
315 | * | ||
316 | * @since 4.3 | ||
317 | * @property {Boolean} | ||
318 | */ | ||
319 | env.needsBrFiller = env.gecko || env.webkit || ( env.ie && version > 10 ); | ||
320 | |||
321 | /** | ||
322 | * Indicates that CKEditor is running in a browser which needs a | ||
323 | * non-breaking space filler in order to correctly display caret in empty blocks. | ||
324 | * | ||
325 | * @since 4.3 | ||
326 | * @property {Boolean} | ||
327 | */ | ||
328 | env.needsNbspFiller = env.ie && version < 11; | ||
329 | |||
330 | /** | ||
331 | * A CSS class that denotes the browser where CKEditor runs and is appended | ||
332 | * to the HTML element that contains the editor. It makes it easier to apply | ||
333 | * browser-specific styles to editor instances. | ||
334 | * | ||
335 | * myDiv.className = CKEDITOR.env.cssClass; | ||
336 | * | ||
337 | * @property {String} | ||
338 | */ | ||
339 | env.cssClass = 'cke_browser_' + ( env.ie ? 'ie' : env.gecko ? 'gecko' : env.webkit ? 'webkit' : 'unknown' ); | ||
340 | |||
341 | if ( env.quirks ) | ||
342 | env.cssClass += ' cke_browser_quirks'; | ||
343 | |||
344 | if ( env.ie ) | ||
345 | env.cssClass += ' cke_browser_ie' + ( env.quirks ? '6 cke_browser_iequirks' : env.version ); | ||
346 | |||
347 | if ( env.air ) | ||
348 | env.cssClass += ' cke_browser_air'; | ||
349 | |||
350 | if ( env.iOS ) | ||
351 | env.cssClass += ' cke_browser_ios'; | ||
352 | |||
353 | if ( env.hidpi ) | ||
354 | env.cssClass += ' cke_hidpi'; | ||
355 | |||
356 | return env; | ||
357 | } )(); | ||
358 | } | ||
359 | |||
360 | // PACKAGER_RENAME( CKEDITOR.env ) | ||
361 | // PACKAGER_RENAME( CKEDITOR.env.ie ) | ||
diff --git a/sources/core/event.js b/sources/core/event.js new file mode 100644 index 0000000..0dd1f41 --- /dev/null +++ b/sources/core/event.js | |||
@@ -0,0 +1,389 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.event} class, which serves as the | ||
8 | * base for classes and objects that require event handling features. | ||
9 | */ | ||
10 | |||
11 | if ( !CKEDITOR.event ) { | ||
12 | /** | ||
13 | * Creates an event class instance. This constructor is rarely used, being | ||
14 | * the {@link #implementOn} function used in class prototypes directly | ||
15 | * instead. | ||
16 | * | ||
17 | * This is a base class for classes and objects that require event | ||
18 | * handling features. | ||
19 | * | ||
20 | * Do not confuse this class with {@link CKEDITOR.dom.event} which is | ||
21 | * instead used for DOM events. The CKEDITOR.event class implements the | ||
22 | * internal event system used by the CKEditor to fire API related events. | ||
23 | * | ||
24 | * @class | ||
25 | * @constructor Creates an event class instance. | ||
26 | */ | ||
27 | CKEDITOR.event = function() {}; | ||
28 | |||
29 | /** | ||
30 | * Implements the {@link CKEDITOR.event} features in an object. | ||
31 | * | ||
32 | * var myObject = { message: 'Example' }; | ||
33 | * CKEDITOR.event.implementOn( myObject ); | ||
34 | * | ||
35 | * myObject.on( 'testEvent', function() { | ||
36 | * alert( this.message ); | ||
37 | * } ); | ||
38 | * myObject.fire( 'testEvent' ); // 'Example' | ||
39 | * | ||
40 | * @static | ||
41 | * @param {Object} targetObject The object into which implement the features. | ||
42 | */ | ||
43 | CKEDITOR.event.implementOn = function( targetObject ) { | ||
44 | var eventProto = CKEDITOR.event.prototype; | ||
45 | |||
46 | for ( var prop in eventProto ) { | ||
47 | if ( targetObject[ prop ] == null ) | ||
48 | targetObject[ prop ] = eventProto[ prop ]; | ||
49 | } | ||
50 | }; | ||
51 | |||
52 | CKEDITOR.event.prototype = ( function() { | ||
53 | // Returns the private events object for a given object. | ||
54 | var getPrivate = function( obj ) { | ||
55 | var _ = ( obj.getPrivate && obj.getPrivate() ) || obj._ || ( obj._ = {} ); | ||
56 | return _.events || ( _.events = {} ); | ||
57 | }; | ||
58 | |||
59 | var eventEntry = function( eventName ) { | ||
60 | this.name = eventName; | ||
61 | this.listeners = []; | ||
62 | }; | ||
63 | |||
64 | eventEntry.prototype = { | ||
65 | // Get the listener index for a specified function. | ||
66 | // Returns -1 if not found. | ||
67 | getListenerIndex: function( listenerFunction ) { | ||
68 | for ( var i = 0, listeners = this.listeners; i < listeners.length; i++ ) { | ||
69 | if ( listeners[ i ].fn == listenerFunction ) | ||
70 | return i; | ||
71 | } | ||
72 | return -1; | ||
73 | } | ||
74 | }; | ||
75 | |||
76 | // Retrieve the event entry on the event host (create it if needed). | ||
77 | function getEntry( name ) { | ||
78 | // Get the event entry (create it if needed). | ||
79 | var events = getPrivate( this ); | ||
80 | return events[ name ] || ( events[ name ] = new eventEntry( name ) ); | ||
81 | } | ||
82 | |||
83 | return { | ||
84 | /** | ||
85 | * Predefine some intrinsic properties on a specific event name. | ||
86 | * | ||
87 | * @param {String} name The event name | ||
88 | * @param meta | ||
89 | * @param [meta.errorProof=false] Whether the event firing should catch error thrown from a per listener call. | ||
90 | */ | ||
91 | define: function( name, meta ) { | ||
92 | var entry = getEntry.call( this, name ); | ||
93 | CKEDITOR.tools.extend( entry, meta, true ); | ||
94 | }, | ||
95 | |||
96 | /** | ||
97 | * Registers a listener to a specific event in the current object. | ||
98 | * | ||
99 | * someObject.on( 'someEvent', function() { | ||
100 | * alert( this == someObject ); // true | ||
101 | * } ); | ||
102 | * | ||
103 | * someObject.on( 'someEvent', function() { | ||
104 | * alert( this == anotherObject ); // true | ||
105 | * }, anotherObject ); | ||
106 | * | ||
107 | * someObject.on( 'someEvent', function( event ) { | ||
108 | * alert( event.listenerData ); // 'Example' | ||
109 | * }, null, 'Example' ); | ||
110 | * | ||
111 | * someObject.on( 'someEvent', function() { ... } ); // 2nd called | ||
112 | * someObject.on( 'someEvent', function() { ... }, null, null, 100 ); // 3rd called | ||
113 | * someObject.on( 'someEvent', function() { ... }, null, null, 1 ); // 1st called | ||
114 | * | ||
115 | * @param {String} eventName The event name to which listen. | ||
116 | * @param {Function} listenerFunction The function listening to the | ||
117 | * event. A single {@link CKEDITOR.eventInfo} object instanced | ||
118 | * is passed to this function containing all the event data. | ||
119 | * @param {Object} [scopeObj] The object used to scope the listener | ||
120 | * call (the `this` object). If omitted, the current object is used. | ||
121 | * @param {Object} [listenerData] Data to be sent as the | ||
122 | * {@link CKEDITOR.eventInfo#listenerData} when calling the | ||
123 | * listener. | ||
124 | * @param {Number} [priority=10] The listener priority. Lower priority | ||
125 | * listeners are called first. Listeners with the same priority | ||
126 | * value are called in registration order. | ||
127 | * @returns {Object} An object containing the `removeListener` | ||
128 | * function, which can be used to remove the listener at any time. | ||
129 | */ | ||
130 | on: function( eventName, listenerFunction, scopeObj, listenerData, priority ) { | ||
131 | // Create the function to be fired for this listener. | ||
132 | function listenerFirer( editor, publisherData, stopFn, cancelFn ) { | ||
133 | var ev = { | ||
134 | name: eventName, | ||
135 | sender: this, | ||
136 | editor: editor, | ||
137 | data: publisherData, | ||
138 | listenerData: listenerData, | ||
139 | stop: stopFn, | ||
140 | cancel: cancelFn, | ||
141 | removeListener: removeListener | ||
142 | }; | ||
143 | |||
144 | var ret = listenerFunction.call( scopeObj, ev ); | ||
145 | |||
146 | return ret === false ? false : ev.data; | ||
147 | } | ||
148 | |||
149 | function removeListener() { | ||
150 | me.removeListener( eventName, listenerFunction ); | ||
151 | } | ||
152 | |||
153 | var event = getEntry.call( this, eventName ); | ||
154 | |||
155 | if ( event.getListenerIndex( listenerFunction ) < 0 ) { | ||
156 | // Get the listeners. | ||
157 | var listeners = event.listeners; | ||
158 | |||
159 | // Fill the scope. | ||
160 | if ( !scopeObj ) | ||
161 | scopeObj = this; | ||
162 | |||
163 | // Default the priority, if needed. | ||
164 | if ( isNaN( priority ) ) | ||
165 | priority = 10; | ||
166 | |||
167 | var me = this; | ||
168 | |||
169 | listenerFirer.fn = listenerFunction; | ||
170 | listenerFirer.priority = priority; | ||
171 | |||
172 | // Search for the right position for this new listener, based on its | ||
173 | // priority. | ||
174 | for ( var i = listeners.length - 1; i >= 0; i-- ) { | ||
175 | // Find the item which should be before the new one. | ||
176 | if ( listeners[ i ].priority <= priority ) { | ||
177 | // Insert the listener in the array. | ||
178 | listeners.splice( i + 1, 0, listenerFirer ); | ||
179 | return { removeListener: removeListener }; | ||
180 | } | ||
181 | } | ||
182 | |||
183 | // If no position has been found (or zero length), put it in | ||
184 | // the front of list. | ||
185 | listeners.unshift( listenerFirer ); | ||
186 | } | ||
187 | |||
188 | return { removeListener: removeListener }; | ||
189 | }, | ||
190 | |||
191 | /** | ||
192 | * Similiar with {@link #on} but the listener will be called only once upon the next event firing. | ||
193 | * | ||
194 | * @see CKEDITOR.event#on | ||
195 | */ | ||
196 | once: function() { | ||
197 | var args = Array.prototype.slice.call( arguments ), | ||
198 | fn = args[ 1 ]; | ||
199 | |||
200 | args[ 1 ] = function( evt ) { | ||
201 | evt.removeListener(); | ||
202 | return fn.apply( this, arguments ); | ||
203 | }; | ||
204 | |||
205 | return this.on.apply( this, args ); | ||
206 | }, | ||
207 | |||
208 | /** | ||
209 | * @static | ||
210 | * @property {Boolean} useCapture | ||
211 | * @todo | ||
212 | */ | ||
213 | |||
214 | /** | ||
215 | * Register event handler under the capturing stage on supported target. | ||
216 | */ | ||
217 | capture: function() { | ||
218 | CKEDITOR.event.useCapture = 1; | ||
219 | var retval = this.on.apply( this, arguments ); | ||
220 | CKEDITOR.event.useCapture = 0; | ||
221 | return retval; | ||
222 | }, | ||
223 | |||
224 | /** | ||
225 | * Fires an specific event in the object. All registered listeners are | ||
226 | * called at this point. | ||
227 | * | ||
228 | * someObject.on( 'someEvent', function() { ... } ); | ||
229 | * someObject.on( 'someEvent', function() { ... } ); | ||
230 | * someObject.fire( 'someEvent' ); // Both listeners are called. | ||
231 | * | ||
232 | * someObject.on( 'someEvent', function( event ) { | ||
233 | * alert( event.data ); // 'Example' | ||
234 | * } ); | ||
235 | * someObject.fire( 'someEvent', 'Example' ); | ||
236 | * | ||
237 | * @method | ||
238 | * @param {String} eventName The event name to fire. | ||
239 | * @param {Object} [data] Data to be sent as the | ||
240 | * {@link CKEDITOR.eventInfo#data} when calling the listeners. | ||
241 | * @param {CKEDITOR.editor} [editor] The editor instance to send as the | ||
242 | * {@link CKEDITOR.eventInfo#editor} when calling the listener. | ||
243 | * @returns {Boolean/Object} A boolean indicating that the event is to be | ||
244 | * canceled, or data returned by one of the listeners. | ||
245 | */ | ||
246 | fire: ( function() { | ||
247 | // Create the function that marks the event as stopped. | ||
248 | var stopped = 0; | ||
249 | var stopEvent = function() { | ||
250 | stopped = 1; | ||
251 | }; | ||
252 | |||
253 | // Create the function that marks the event as canceled. | ||
254 | var canceled = 0; | ||
255 | var cancelEvent = function() { | ||
256 | canceled = 1; | ||
257 | }; | ||
258 | |||
259 | return function( eventName, data, editor ) { | ||
260 | // Get the event entry. | ||
261 | var event = getPrivate( this )[ eventName ]; | ||
262 | |||
263 | // Save the previous stopped and cancelled states. We may | ||
264 | // be nesting fire() calls. | ||
265 | var previousStopped = stopped, | ||
266 | previousCancelled = canceled; | ||
267 | |||
268 | // Reset the stopped and canceled flags. | ||
269 | stopped = canceled = 0; | ||
270 | |||
271 | if ( event ) { | ||
272 | var listeners = event.listeners; | ||
273 | |||
274 | if ( listeners.length ) { | ||
275 | // As some listeners may remove themselves from the | ||
276 | // event, the original array length is dinamic. So, | ||
277 | // let's make a copy of all listeners, so we are | ||
278 | // sure we'll call all of them. | ||
279 | listeners = listeners.slice( 0 ); | ||
280 | |||
281 | var retData; | ||
282 | // Loop through all listeners. | ||
283 | for ( var i = 0; i < listeners.length; i++ ) { | ||
284 | // Call the listener, passing the event data. | ||
285 | if ( event.errorProof ) { | ||
286 | try { | ||
287 | retData = listeners[ i ].call( this, editor, data, stopEvent, cancelEvent ); | ||
288 | } catch ( er ) {} | ||
289 | } else { | ||
290 | retData = listeners[ i ].call( this, editor, data, stopEvent, cancelEvent ); | ||
291 | } | ||
292 | |||
293 | if ( retData === false ) | ||
294 | canceled = 1; | ||
295 | else if ( typeof retData != 'undefined' ) | ||
296 | data = retData; | ||
297 | |||
298 | // No further calls is stopped or canceled. | ||
299 | if ( stopped || canceled ) | ||
300 | break; | ||
301 | } | ||
302 | } | ||
303 | } | ||
304 | |||
305 | var ret = canceled ? false : ( typeof data == 'undefined' ? true : data ); | ||
306 | |||
307 | // Restore the previous stopped and canceled states. | ||
308 | stopped = previousStopped; | ||
309 | canceled = previousCancelled; | ||
310 | |||
311 | return ret; | ||
312 | }; | ||
313 | } )(), | ||
314 | |||
315 | /** | ||
316 | * Fires an specific event in the object, releasing all listeners | ||
317 | * registered to that event. The same listeners are not called again on | ||
318 | * successive calls of it or of {@link #fire}. | ||
319 | * | ||
320 | * someObject.on( 'someEvent', function() { ... } ); | ||
321 | * someObject.fire( 'someEvent' ); // Above listener called. | ||
322 | * someObject.fireOnce( 'someEvent' ); // Above listener called. | ||
323 | * someObject.fire( 'someEvent' ); // No listeners called. | ||
324 | * | ||
325 | * @param {String} eventName The event name to fire. | ||
326 | * @param {Object} [data] Data to be sent as the | ||
327 | * {@link CKEDITOR.eventInfo#data} when calling the listeners. | ||
328 | * @param {CKEDITOR.editor} [editor] The editor instance to send as the | ||
329 | * {@link CKEDITOR.eventInfo#editor} when calling the listener. | ||
330 | * @returns {Boolean/Object} A booloan indicating that the event is to be | ||
331 | * canceled, or data returned by one of the listeners. | ||
332 | */ | ||
333 | fireOnce: function( eventName, data, editor ) { | ||
334 | var ret = this.fire( eventName, data, editor ); | ||
335 | delete getPrivate( this )[ eventName ]; | ||
336 | return ret; | ||
337 | }, | ||
338 | |||
339 | /** | ||
340 | * Unregisters a listener function from being called at the specified | ||
341 | * event. No errors are thrown if the listener has not been registered previously. | ||
342 | * | ||
343 | * var myListener = function() { ... }; | ||
344 | * someObject.on( 'someEvent', myListener ); | ||
345 | * someObject.fire( 'someEvent' ); // myListener called. | ||
346 | * someObject.removeListener( 'someEvent', myListener ); | ||
347 | * someObject.fire( 'someEvent' ); // myListener not called. | ||
348 | * | ||
349 | * @param {String} eventName The event name. | ||
350 | * @param {Function} listenerFunction The listener function to unregister. | ||
351 | */ | ||
352 | removeListener: function( eventName, listenerFunction ) { | ||
353 | // Get the event entry. | ||
354 | var event = getPrivate( this )[ eventName ]; | ||
355 | |||
356 | if ( event ) { | ||
357 | var index = event.getListenerIndex( listenerFunction ); | ||
358 | if ( index >= 0 ) | ||
359 | event.listeners.splice( index, 1 ); | ||
360 | } | ||
361 | }, | ||
362 | |||
363 | /** | ||
364 | * Remove all existing listeners on this object, for cleanup purpose. | ||
365 | */ | ||
366 | removeAllListeners: function() { | ||
367 | var events = getPrivate( this ); | ||
368 | for ( var i in events ) | ||
369 | delete events[ i ]; | ||
370 | }, | ||
371 | |||
372 | /** | ||
373 | * Checks if there is any listener registered to a given event. | ||
374 | * | ||
375 | * var myListener = function() { ... }; | ||
376 | * someObject.on( 'someEvent', myListener ); | ||
377 | * alert( someObject.hasListeners( 'someEvent' ) ); // true | ||
378 | * alert( someObject.hasListeners( 'noEvent' ) ); // false | ||
379 | * | ||
380 | * @param {String} eventName The event name. | ||
381 | * @returns {Boolean} | ||
382 | */ | ||
383 | hasListeners: function( eventName ) { | ||
384 | var event = getPrivate( this )[ eventName ]; | ||
385 | return ( event && event.listeners.length > 0 ); | ||
386 | } | ||
387 | }; | ||
388 | } )(); | ||
389 | } | ||
diff --git a/sources/core/eventInfo.js b/sources/core/eventInfo.js new file mode 100644 index 0000000..e1cd65a --- /dev/null +++ b/sources/core/eventInfo.js | |||
@@ -0,0 +1,115 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Defines the "virtual" {@link CKEDITOR.eventInfo} class, which | ||
8 | * contains the defintions of the event object passed to event listeners. | ||
9 | * This file is for documentation purposes only. | ||
10 | */ | ||
11 | |||
12 | /** | ||
13 | * Virtual class that illustrates the features of the event object to be | ||
14 | * passed to event listeners by a {@link CKEDITOR.event} based object. | ||
15 | * | ||
16 | * This class is not really part of the API. | ||
17 | * | ||
18 | * @class CKEDITOR.eventInfo | ||
19 | * @abstract | ||
20 | */ | ||
21 | |||
22 | /** | ||
23 | * The event name. | ||
24 | * | ||
25 | * someObject.on( 'someEvent', function( event ) { | ||
26 | * alert( event.name ); // 'someEvent' | ||
27 | * } ); | ||
28 | * someObject.fire( 'someEvent' ); | ||
29 | * | ||
30 | * @property {String} name | ||
31 | */ | ||
32 | |||
33 | /** | ||
34 | * The object that publishes (sends) the event. | ||
35 | * | ||
36 | * someObject.on( 'someEvent', function( event ) { | ||
37 | * alert( event.sender == someObject ); // true | ||
38 | * } ); | ||
39 | * someObject.fire( 'someEvent' ); | ||
40 | * | ||
41 | * @property sender | ||
42 | */ | ||
43 | |||
44 | /** | ||
45 | * The editor instance that holds the sender. May be the same as sender. May be | ||
46 | * null if the sender is not part of an editor instance, like a component | ||
47 | * running in standalone mode. | ||
48 | * | ||
49 | * myButton.on( 'someEvent', function( event ) { | ||
50 | * alert( event.editor == myEditor ); // true | ||
51 | * } ); | ||
52 | * myButton.fire( 'someEvent', null, myEditor ); | ||
53 | * | ||
54 | * @property {CKEDITOR.editor} editor | ||
55 | */ | ||
56 | |||
57 | /** | ||
58 | * Any kind of additional data. Its format and usage is event dependent. | ||
59 | * | ||
60 | * someObject.on( 'someEvent', function( event ) { | ||
61 | * alert( event.data ); // 'Example' | ||
62 | * } ); | ||
63 | * someObject.fire( 'someEvent', 'Example' ); | ||
64 | * | ||
65 | * @property data | ||
66 | */ | ||
67 | |||
68 | /** | ||
69 | * Any extra data appended during the listener registration. | ||
70 | * | ||
71 | * someObject.on( 'someEvent', function( event ) { | ||
72 | * alert( event.listenerData ); // 'Example' | ||
73 | * }, null, 'Example' ); | ||
74 | * | ||
75 | * @property listenerData | ||
76 | */ | ||
77 | |||
78 | /** | ||
79 | * Indicates that no further listeners are to be called. | ||
80 | * | ||
81 | * someObject.on( 'someEvent', function( event ) { | ||
82 | * event.stop(); | ||
83 | * } ); | ||
84 | * someObject.on( 'someEvent', function( event ) { | ||
85 | * // This one will not be called. | ||
86 | * } ); | ||
87 | * alert( someObject.fire( 'someEvent' ) ); // true | ||
88 | * | ||
89 | * @method stop | ||
90 | */ | ||
91 | |||
92 | /** | ||
93 | * Indicates that the event is to be cancelled (if cancelable). | ||
94 | * | ||
95 | * someObject.on( 'someEvent', function( event ) { | ||
96 | * event.cancel(); | ||
97 | * } ); | ||
98 | * someObject.on( 'someEvent', function( event ) { | ||
99 | * // This one will not be called. | ||
100 | * } ); | ||
101 | * alert( someObject.fire( 'someEvent' ) ); // false | ||
102 | * | ||
103 | * @method cancel | ||
104 | */ | ||
105 | |||
106 | /** | ||
107 | * Removes the current listener. | ||
108 | * | ||
109 | * someObject.on( 'someEvent', function( event ) { | ||
110 | * event.removeListener(); | ||
111 | * // Now this function won't be called again by 'someEvent'. | ||
112 | * } ); | ||
113 | * | ||
114 | * @method removeListener | ||
115 | */ | ||
diff --git a/sources/core/filter.js b/sources/core/filter.js new file mode 100644 index 0000000..e9d5a37 --- /dev/null +++ b/sources/core/filter.js | |||
@@ -0,0 +1,2440 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | ( function() { | ||
7 | 'use strict'; | ||
8 | |||
9 | var DTD = CKEDITOR.dtd, | ||
10 | // processElement flag - means that element has been somehow modified. | ||
11 | FILTER_ELEMENT_MODIFIED = 1, | ||
12 | // processElement flag - meaning explained in CKEDITOR.FILTER_SKIP_TREE doc. | ||
13 | FILTER_SKIP_TREE = 2, | ||
14 | copy = CKEDITOR.tools.copy, | ||
15 | trim = CKEDITOR.tools.trim, | ||
16 | TEST_VALUE = 'cke-test', | ||
17 | enterModeTags = [ '', 'p', 'br', 'div' ]; | ||
18 | |||
19 | /** | ||
20 | * A flag indicating that the current element and all its ancestors | ||
21 | * should not be filtered. | ||
22 | * | ||
23 | * See {@link CKEDITOR.filter#addElementCallback} for more details. | ||
24 | * | ||
25 | * @since 4.4 | ||
26 | * @readonly | ||
27 | * @property {Number} [=2] | ||
28 | * @member CKEDITOR | ||
29 | */ | ||
30 | CKEDITOR.FILTER_SKIP_TREE = FILTER_SKIP_TREE; | ||
31 | |||
32 | /** | ||
33 | * Highly configurable class which implements input data filtering mechanisms | ||
34 | * and core functions used for the activation of editor features. | ||
35 | * | ||
36 | * A filter instance is always available under the {@link CKEDITOR.editor#filter} | ||
37 | * property and is used by the editor in its core features like filtering input data, | ||
38 | * applying data transformations, validating whether a feature may be enabled for | ||
39 | * the current setup. It may be configured in two ways: | ||
40 | * | ||
41 | * * By the user, with the {@link CKEDITOR.config#allowedContent} setting. | ||
42 | * * Automatically, by loaded features (toolbar items, commands, etc.). | ||
43 | * | ||
44 | * In both cases additional allowed content rules may be added by | ||
45 | * setting the {@link CKEDITOR.config#extraAllowedContent} | ||
46 | * configuration option. | ||
47 | * | ||
48 | * **Note**: Filter rules will be extended with the following elements | ||
49 | * depending on the {@link CKEDITOR.config#enterMode} and | ||
50 | * {@link CKEDITOR.config#shiftEnterMode} settings: | ||
51 | * | ||
52 | * * `'p'` – for {@link CKEDITOR#ENTER_P}, | ||
53 | * * `'div'` – for {@link CKEDITOR#ENTER_DIV}, | ||
54 | * * `'br'` – for {@link CKEDITOR#ENTER_BR}. | ||
55 | * | ||
56 | * **Read more** about the Advanced Content Filter in [guides](#!/guide/dev_advanced_content_filter). | ||
57 | * | ||
58 | * Filter may also be used as a standalone instance by passing | ||
59 | * {@link CKEDITOR.filter.allowedContentRules} instead of {@link CKEDITOR.editor} | ||
60 | * to the constructor: | ||
61 | * | ||
62 | * var filter = new CKEDITOR.filter( 'b' ); | ||
63 | * | ||
64 | * filter.check( 'b' ); // -> true | ||
65 | * filter.check( 'i' ); // -> false | ||
66 | * filter.allow( 'i' ); | ||
67 | * filter.check( 'i' ); // -> true | ||
68 | * | ||
69 | * @since 4.1 | ||
70 | * @class | ||
71 | * @constructor Creates a filter class instance. | ||
72 | * @param {CKEDITOR.editor/CKEDITOR.filter.allowedContentRules} editorOrRules | ||
73 | */ | ||
74 | CKEDITOR.filter = function( editorOrRules ) { | ||
75 | /** | ||
76 | * Whether custom {@link CKEDITOR.config#allowedContent} was set. | ||
77 | * | ||
78 | * This property does not apply to the standalone filter. | ||
79 | * | ||
80 | * @readonly | ||
81 | * @property {Boolean} customConfig | ||
82 | */ | ||
83 | |||
84 | /** | ||
85 | * Array of rules added by the {@link #allow} method (including those | ||
86 | * loaded from {@link CKEDITOR.config#allowedContent} and | ||
87 | * {@link CKEDITOR.config#extraAllowedContent}). | ||
88 | * | ||
89 | * Rules in this array are in unified allowed content rules format. | ||
90 | * | ||
91 | * This property is useful for debugging issues with rules string parsing | ||
92 | * or for checking which rules were automatically added by editor features. | ||
93 | * | ||
94 | * @readonly | ||
95 | */ | ||
96 | this.allowedContent = []; | ||
97 | |||
98 | /** | ||
99 | * Array of rules added by the {@link #disallow} method (including those | ||
100 | * loaded from {@link CKEDITOR.config#disallowedContent}). | ||
101 | * | ||
102 | * Rules in this array are in unified disallowed content rules format. | ||
103 | * | ||
104 | * This property is useful for debugging issues with rules string parsing | ||
105 | * or for checking which rules were automatically added by editor features. | ||
106 | * | ||
107 | * @since 4.4 | ||
108 | * @readonly | ||
109 | */ | ||
110 | this.disallowedContent = []; | ||
111 | |||
112 | /** | ||
113 | * Array of element callbacks. See {@link #addElementCallback}. | ||
114 | * | ||
115 | * @readonly | ||
116 | * @property {Function[]} [=null] | ||
117 | */ | ||
118 | this.elementCallbacks = null; | ||
119 | |||
120 | /** | ||
121 | * Whether the filter is disabled. | ||
122 | * | ||
123 | * To disable the filter, set {@link CKEDITOR.config#allowedContent} to `true` | ||
124 | * or use the {@link #disable} method. | ||
125 | * | ||
126 | * @readonly | ||
127 | */ | ||
128 | this.disabled = false; | ||
129 | |||
130 | /** | ||
131 | * Editor instance if not a standalone filter. | ||
132 | * | ||
133 | * @readonly | ||
134 | * @property {CKEDITOR.editor} [=null] | ||
135 | */ | ||
136 | this.editor = null; | ||
137 | |||
138 | /** | ||
139 | * Filter's unique id. It can be used to find filter instance in | ||
140 | * {@link CKEDITOR.filter#instances CKEDITOR.filter.instance} object. | ||
141 | * | ||
142 | * @since 4.3 | ||
143 | * @readonly | ||
144 | * @property {Number} id | ||
145 | */ | ||
146 | this.id = CKEDITOR.tools.getNextNumber(); | ||
147 | |||
148 | this._ = { | ||
149 | // Optimized allowed content rules. | ||
150 | allowedRules: { | ||
151 | elements: {}, | ||
152 | generic: [] | ||
153 | }, | ||
154 | // Optimized disallowed content rules. | ||
155 | disallowedRules: { | ||
156 | elements: {}, | ||
157 | generic: [] | ||
158 | }, | ||
159 | // Object: element name => array of transformations groups. | ||
160 | transformations: {}, | ||
161 | cachedTests: {} | ||
162 | }; | ||
163 | |||
164 | // Register filter instance. | ||
165 | CKEDITOR.filter.instances[ this.id ] = this; | ||
166 | |||
167 | if ( editorOrRules instanceof CKEDITOR.editor ) { | ||
168 | var editor = this.editor = editorOrRules; | ||
169 | this.customConfig = true; | ||
170 | |||
171 | var allowedContent = editor.config.allowedContent; | ||
172 | |||
173 | // Disable filter completely by setting config.allowedContent = true. | ||
174 | if ( allowedContent === true ) { | ||
175 | this.disabled = true; | ||
176 | return; | ||
177 | } | ||
178 | |||
179 | if ( !allowedContent ) | ||
180 | this.customConfig = false; | ||
181 | |||
182 | this.allow( allowedContent, 'config', 1 ); | ||
183 | this.allow( editor.config.extraAllowedContent, 'extra', 1 ); | ||
184 | |||
185 | // Enter modes should extend filter rules (ENTER_P adds 'p' rule, etc.). | ||
186 | this.allow( enterModeTags[ editor.enterMode ] + ' ' + enterModeTags[ editor.shiftEnterMode ], 'default', 1 ); | ||
187 | |||
188 | this.disallow( editor.config.disallowedContent ); | ||
189 | } | ||
190 | // Rules object passed in editorOrRules argument - initialize standalone filter. | ||
191 | else { | ||
192 | this.customConfig = false; | ||
193 | this.allow( editorOrRules, 'default', 1 ); | ||
194 | } | ||
195 | }; | ||
196 | |||
197 | /** | ||
198 | * Object containing all filter instances stored under their | ||
199 | * {@link #id} properties. | ||
200 | * | ||
201 | * var filter = new CKEDITOR.filter( 'p' ); | ||
202 | * filter === CKEDITOR.filter.instances[ filter.id ]; | ||
203 | * | ||
204 | * @since 4.3 | ||
205 | * @static | ||
206 | * @property instances | ||
207 | */ | ||
208 | CKEDITOR.filter.instances = {}; | ||
209 | |||
210 | CKEDITOR.filter.prototype = { | ||
211 | /** | ||
212 | * Adds allowed content rules to the filter. | ||
213 | * | ||
214 | * Read about rules formats in [Allowed Content Rules guide](#!/guide/dev_allowed_content_rules). | ||
215 | * | ||
216 | * // Add a basic rule for custom image feature (e.g. 'MyImage' button). | ||
217 | * editor.filter.allow( 'img[!src,alt]', 'MyImage' ); | ||
218 | * | ||
219 | * // Add rules for two header styles allowed by 'HeadersCombo'. | ||
220 | * var header1Style = new CKEDITOR.style( { element: 'h1' } ), | ||
221 | * header2Style = new CKEDITOR.style( { element: 'h2' } ); | ||
222 | * editor.filter.allow( [ header1Style, header2Style ], 'HeadersCombo' ); | ||
223 | * | ||
224 | * @param {CKEDITOR.filter.allowedContentRules} newRules Rule(s) to be added. | ||
225 | * @param {String} [featureName] Name of a feature that allows this content (most often plugin/button/command name). | ||
226 | * @param {Boolean} [overrideCustom] By default this method will reject any rules | ||
227 | * if {@link CKEDITOR.config#allowedContent} is defined to avoid overriding it. | ||
228 | * Pass `true` to force rules addition. | ||
229 | * @returns {Boolean} Whether the rules were accepted. | ||
230 | */ | ||
231 | allow: function( newRules, featureName, overrideCustom ) { | ||
232 | // Check arguments and constraints. Clear cache. | ||
233 | if ( !beforeAddingRule( this, newRules, overrideCustom ) ) | ||
234 | return false; | ||
235 | |||
236 | var i, ret; | ||
237 | |||
238 | if ( typeof newRules == 'string' ) | ||
239 | newRules = parseRulesString( newRules ); | ||
240 | else if ( newRules instanceof CKEDITOR.style ) { | ||
241 | // If style has the cast method defined, use it and abort. | ||
242 | if ( newRules.toAllowedContentRules ) | ||
243 | return this.allow( newRules.toAllowedContentRules( this.editor ), featureName, overrideCustom ); | ||
244 | |||
245 | newRules = convertStyleToRules( newRules ); | ||
246 | } else if ( CKEDITOR.tools.isArray( newRules ) ) { | ||
247 | for ( i = 0; i < newRules.length; ++i ) | ||
248 | ret = this.allow( newRules[ i ], featureName, overrideCustom ); | ||
249 | return ret; // Return last status. | ||
250 | } | ||
251 | |||
252 | addAndOptimizeRules( this, newRules, featureName, this.allowedContent, this._.allowedRules ); | ||
253 | |||
254 | return true; | ||
255 | }, | ||
256 | |||
257 | /** | ||
258 | * Applies this filter to passed {@link CKEDITOR.htmlParser.fragment} or {@link CKEDITOR.htmlParser.element}. | ||
259 | * The result of filtering is a DOM tree without disallowed content. | ||
260 | * | ||
261 | * // Create standalone filter passing 'p' and 'b' elements. | ||
262 | * var filter = new CKEDITOR.filter( 'p b' ), | ||
263 | * // Parse HTML string to pseudo DOM structure. | ||
264 | * fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p><b>foo</b> <i>bar</i></p>' ), | ||
265 | * writer = new CKEDITOR.htmlParser.basicWriter(); | ||
266 | * | ||
267 | * filter.applyTo( fragment ); | ||
268 | * fragment.writeHtml( writer ); | ||
269 | * writer.getHtml(); // -> '<p><b>foo</b> bar</p>' | ||
270 | * | ||
271 | * @param {CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} fragment Node to be filtered. | ||
272 | * @param {Boolean} [toHtml] Set to `true` if the filter is used together with {@link CKEDITOR.htmlDataProcessor#toHtml}. | ||
273 | * @param {Boolean} [transformOnly] If set to `true` only transformations will be applied. Content | ||
274 | * will not be filtered with allowed content rules. | ||
275 | * @param {Number} [enterMode] Enter mode used by the filter when deciding how to strip disallowed element. | ||
276 | * Defaults to {@link CKEDITOR.editor#activeEnterMode} for a editor's filter or to {@link CKEDITOR#ENTER_P} for standalone filter. | ||
277 | * @returns {Boolean} Whether some part of the `fragment` was removed by the filter. | ||
278 | */ | ||
279 | applyTo: function( fragment, toHtml, transformOnly, enterMode ) { | ||
280 | if ( this.disabled ) | ||
281 | return false; | ||
282 | |||
283 | var that = this, | ||
284 | toBeRemoved = [], | ||
285 | protectedRegexs = this.editor && this.editor.config.protectedSource, | ||
286 | processRetVal, | ||
287 | isModified = false, | ||
288 | filterOpts = { | ||
289 | doFilter: !transformOnly, | ||
290 | doTransform: true, | ||
291 | doCallbacks: true, | ||
292 | toHtml: toHtml | ||
293 | }; | ||
294 | |||
295 | // Filter all children, skip root (fragment or editable-like wrapper used by data processor). | ||
296 | fragment.forEach( function( el ) { | ||
297 | if ( el.type == CKEDITOR.NODE_ELEMENT ) { | ||
298 | // Do not filter element with data-cke-filter="off" and all their descendants. | ||
299 | if ( el.attributes[ 'data-cke-filter' ] == 'off' ) | ||
300 | return false; | ||
301 | |||
302 | // (#10260) Don't touch elements like spans with data-cke-* attribute since they're | ||
303 | // responsible e.g. for placing markers, bookmarks, odds and stuff. | ||
304 | // We love 'em and we don't wanna lose anything during the filtering. | ||
305 | // '|' is to avoid tricky joints like data-="foo" + cke-="bar". Yes, they're possible. | ||
306 | // | ||
307 | // NOTE: data-cke-* assigned elements are preserved only when filter is used with | ||
308 | // htmlDataProcessor.toHtml because we don't want to protect them when outputting data | ||
309 | // (toDataFormat). | ||
310 | if ( toHtml && el.name == 'span' && ~CKEDITOR.tools.objectKeys( el.attributes ).join( '|' ).indexOf( 'data-cke-' ) ) | ||
311 | return; | ||
312 | |||
313 | processRetVal = processElement( that, el, toBeRemoved, filterOpts ); | ||
314 | if ( processRetVal & FILTER_ELEMENT_MODIFIED ) | ||
315 | isModified = true; | ||
316 | else if ( processRetVal & FILTER_SKIP_TREE ) | ||
317 | return false; | ||
318 | } | ||
319 | else if ( el.type == CKEDITOR.NODE_COMMENT && el.value.match( /^\{cke_protected\}(?!\{C\})/ ) ) { | ||
320 | if ( !processProtectedElement( that, el, protectedRegexs, filterOpts ) ) | ||
321 | toBeRemoved.push( el ); | ||
322 | } | ||
323 | }, null, true ); | ||
324 | |||
325 | if ( toBeRemoved.length ) | ||
326 | isModified = true; | ||
327 | |||
328 | var node, element, check, | ||
329 | toBeChecked = [], | ||
330 | enterTag = enterModeTags[ enterMode || ( this.editor ? this.editor.enterMode : CKEDITOR.ENTER_P ) ], | ||
331 | parentDtd; | ||
332 | |||
333 | // Remove elements in reverse order - from leaves to root, to avoid conflicts. | ||
334 | while ( ( node = toBeRemoved.pop() ) ) { | ||
335 | if ( node.type == CKEDITOR.NODE_ELEMENT ) | ||
336 | removeElement( node, enterTag, toBeChecked ); | ||
337 | // This is a comment securing rejected element - remove it completely. | ||
338 | else | ||
339 | node.remove(); | ||
340 | } | ||
341 | |||
342 | // Check elements that have been marked as possibly invalid. | ||
343 | while ( ( check = toBeChecked.pop() ) ) { | ||
344 | element = check.el; | ||
345 | // Element has been already removed. | ||
346 | if ( !element.parent ) | ||
347 | continue; | ||
348 | |||
349 | // Handle custom elements as inline elements (#12683). | ||
350 | parentDtd = DTD[ element.parent.name ] || DTD.span; | ||
351 | |||
352 | switch ( check.check ) { | ||
353 | // Check if element itself is correct. | ||
354 | case 'it': | ||
355 | // Check if element included in $removeEmpty has no children. | ||
356 | if ( DTD.$removeEmpty[ element.name ] && !element.children.length ) | ||
357 | removeElement( element, enterTag, toBeChecked ); | ||
358 | // Check if that is invalid element. | ||
359 | else if ( !validateElement( element ) ) | ||
360 | removeElement( element, enterTag, toBeChecked ); | ||
361 | break; | ||
362 | |||
363 | // Check if element is in correct context. If not - remove element. | ||
364 | case 'el-up': | ||
365 | // Check if e.g. li is a child of body after ul has been removed. | ||
366 | if ( element.parent.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT && !parentDtd[ element.name ] ) | ||
367 | removeElement( element, enterTag, toBeChecked ); | ||
368 | break; | ||
369 | |||
370 | // Check if element is in correct context. If not - remove parent. | ||
371 | case 'parent-down': | ||
372 | if ( element.parent.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT && !parentDtd[ element.name ] ) | ||
373 | removeElement( element.parent, enterTag, toBeChecked ); | ||
374 | break; | ||
375 | } | ||
376 | } | ||
377 | |||
378 | return isModified; | ||
379 | }, | ||
380 | |||
381 | /** | ||
382 | * Checks whether a {@link CKEDITOR.feature} can be enabled. Unlike {@link #addFeature}, | ||
383 | * this method always checks the feature, even when the default configuration | ||
384 | * for {@link CKEDITOR.config#allowedContent} is used. | ||
385 | * | ||
386 | * // TODO example | ||
387 | * | ||
388 | * @param {CKEDITOR.feature} feature The feature to be tested. | ||
389 | * @returns {Boolean} Whether this feature can be enabled. | ||
390 | */ | ||
391 | checkFeature: function( feature ) { | ||
392 | if ( this.disabled ) | ||
393 | return true; | ||
394 | |||
395 | if ( !feature ) | ||
396 | return true; | ||
397 | |||
398 | // Some features may want to register other features. | ||
399 | // E.g. a button may return a command bound to it. | ||
400 | if ( feature.toFeature ) | ||
401 | feature = feature.toFeature( this.editor ); | ||
402 | |||
403 | return !feature.requiredContent || this.check( feature.requiredContent ); | ||
404 | }, | ||
405 | |||
406 | /** | ||
407 | * Disables Advanced Content Filter. | ||
408 | * | ||
409 | * This method is meant to be used by plugins which are not | ||
410 | * compatible with the filter and in other cases in which the filter | ||
411 | * has to be disabled during the initialization phase or runtime. | ||
412 | * | ||
413 | * In other cases the filter can be disabled by setting | ||
414 | * {@link CKEDITOR.config#allowedContent} to `true`. | ||
415 | */ | ||
416 | disable: function() { | ||
417 | this.disabled = true; | ||
418 | }, | ||
419 | |||
420 | /** | ||
421 | * Adds disallowed content rules to the filter. | ||
422 | * | ||
423 | * Read about rules formats in the [Allowed Content Rules guide](#!/guide/dev_allowed_content_rules). | ||
424 | * | ||
425 | * // Disallow all styles on the image elements. | ||
426 | * editor.filter.disallow( 'img{*}' ); | ||
427 | * | ||
428 | * // Disallow all span and div elements. | ||
429 | * editor.filter.disallow( 'span div' ); | ||
430 | * | ||
431 | * @since 4.4 | ||
432 | * @param {CKEDITOR.filter.disallowedContentRules} newRules Rule(s) to be added. | ||
433 | */ | ||
434 | disallow: function( newRules ) { | ||
435 | // Check arguments and constraints. Clear cache. | ||
436 | // Note: we pass true in the 3rd argument, because disallow() should never | ||
437 | // be blocked by custom configuration. | ||
438 | if ( !beforeAddingRule( this, newRules, true ) ) | ||
439 | return false; | ||
440 | |||
441 | if ( typeof newRules == 'string' ) | ||
442 | newRules = parseRulesString( newRules ); | ||
443 | |||
444 | addAndOptimizeRules( this, newRules, null, this.disallowedContent, this._.disallowedRules ); | ||
445 | |||
446 | return true; | ||
447 | }, | ||
448 | |||
449 | /** | ||
450 | * Adds an array of {@link CKEDITOR.feature} content forms. All forms | ||
451 | * will then be transformed to the first form which is allowed by the filter. | ||
452 | * | ||
453 | * editor.filter.allow( 'i; span{!font-style}' ); | ||
454 | * editor.filter.addContentForms( [ | ||
455 | * 'em', | ||
456 | * 'i', | ||
457 | * [ 'span', function( el ) { | ||
458 | * return el.styles[ 'font-style' ] == 'italic'; | ||
459 | * } ] | ||
460 | * ] ); | ||
461 | * // Now <em> and <span style="font-style:italic"> will be replaced with <i> | ||
462 | * // because this is the first allowed form. | ||
463 | * // <span> is allowed too, but it is the last form and | ||
464 | * // additionaly, the editor cannot transform an element based on | ||
465 | * // the array+function form). | ||
466 | * | ||
467 | * This method is used by the editor to register {@link CKEDITOR.feature#contentForms} | ||
468 | * when adding a feature with {@link #addFeature} or {@link CKEDITOR.editor#addFeature}. | ||
469 | * | ||
470 | * @param {Array} forms The content forms of a feature. | ||
471 | */ | ||
472 | addContentForms: function( forms ) { | ||
473 | if ( this.disabled ) | ||
474 | return; | ||
475 | |||
476 | if ( !forms ) | ||
477 | return; | ||
478 | |||
479 | var i, form, | ||
480 | transfGroups = [], | ||
481 | preferredForm; | ||
482 | |||
483 | // First, find preferred form - this is, first allowed. | ||
484 | for ( i = 0; i < forms.length && !preferredForm; ++i ) { | ||
485 | form = forms[ i ]; | ||
486 | |||
487 | // Check only strings and styles - array format isn't supported by #check(). | ||
488 | if ( ( typeof form == 'string' || form instanceof CKEDITOR.style ) && this.check( form ) ) | ||
489 | preferredForm = form; | ||
490 | } | ||
491 | |||
492 | // This feature doesn't have preferredForm, so ignore it. | ||
493 | if ( !preferredForm ) | ||
494 | return; | ||
495 | |||
496 | for ( i = 0; i < forms.length; ++i ) | ||
497 | transfGroups.push( getContentFormTransformationGroup( forms[ i ], preferredForm ) ); | ||
498 | |||
499 | this.addTransformations( transfGroups ); | ||
500 | }, | ||
501 | |||
502 | /** | ||
503 | * Adds a callback which will be executed on every element | ||
504 | * that the filter reaches when filtering, before the element is filtered. | ||
505 | * | ||
506 | * By returning {@link CKEDITOR#FILTER_SKIP_TREE} it is possible to | ||
507 | * skip filtering of the current element and all its ancestors. | ||
508 | * | ||
509 | * editor.filter.addElementCallback( function( el ) { | ||
510 | * if ( el.hasClass( 'protected' ) ) | ||
511 | * return CKEDITOR.FILTER_SKIP_TREE; | ||
512 | * } ); | ||
513 | * | ||
514 | * **Note:** At this stage the element passed to the callback does not | ||
515 | * contain `attributes`, `classes`, and `styles` properties which are available | ||
516 | * temporarily on later stages of the filtering process. Therefore you need to | ||
517 | * use the pure {@link CKEDITOR.htmlParser.element} interface. | ||
518 | * | ||
519 | * @since 4.4 | ||
520 | * @param {Function} callback The callback to be executed. | ||
521 | */ | ||
522 | addElementCallback: function( callback ) { | ||
523 | // We want to keep it a falsy value, to speed up finding whether there are any callbacks. | ||
524 | if ( !this.elementCallbacks ) | ||
525 | this.elementCallbacks = []; | ||
526 | |||
527 | this.elementCallbacks.push( callback ); | ||
528 | }, | ||
529 | |||
530 | /** | ||
531 | * Checks whether a feature can be enabled for the HTML restrictions in place | ||
532 | * for the current CKEditor instance, based on the HTML code the feature might | ||
533 | * generate and the minimal HTML code the feature needs to be able to generate. | ||
534 | * | ||
535 | * // TODO example | ||
536 | * | ||
537 | * @param {CKEDITOR.feature} feature | ||
538 | * @returns {Boolean} Whether this feature can be enabled. | ||
539 | */ | ||
540 | addFeature: function( feature ) { | ||
541 | if ( this.disabled ) | ||
542 | return true; | ||
543 | |||
544 | if ( !feature ) | ||
545 | return true; | ||
546 | |||
547 | // Some features may want to register other features. | ||
548 | // E.g. a button may return a command bound to it. | ||
549 | if ( feature.toFeature ) | ||
550 | feature = feature.toFeature( this.editor ); | ||
551 | |||
552 | // If default configuration (will be checked inside #allow()), | ||
553 | // then add allowed content rules. | ||
554 | this.allow( feature.allowedContent, feature.name ); | ||
555 | |||
556 | this.addTransformations( feature.contentTransformations ); | ||
557 | this.addContentForms( feature.contentForms ); | ||
558 | |||
559 | // If custom configuration or any DACRs, then check if required content is allowed. | ||
560 | if ( feature.requiredContent && ( this.customConfig || this.disallowedContent.length ) ) | ||
561 | return this.check( feature.requiredContent ); | ||
562 | |||
563 | return true; | ||
564 | }, | ||
565 | |||
566 | /** | ||
567 | * Adds an array of content transformation groups. One group | ||
568 | * may contain many transformation rules, but only the first | ||
569 | * matching rule in a group is executed. | ||
570 | * | ||
571 | * A single transformation rule is an object with four properties: | ||
572 | * | ||
573 | * * `check` (optional) – if set and {@link CKEDITOR.filter} does | ||
574 | * not accept this {@link CKEDITOR.filter.contentRule}, this transformation rule | ||
575 | * will not be executed (it does not *match*). This value is passed | ||
576 | * to {@link #check}. | ||
577 | * * `element` (optional) – this string property tells the filter on which | ||
578 | * element this transformation can be run. It is optional, because | ||
579 | * the element name can be obtained from `check` (if it is a String format) | ||
580 | * or `left` (if it is a {@link CKEDITOR.style} instance). | ||
581 | * * `left` (optional) – a function accepting an element or a {@link CKEDITOR.style} | ||
582 | * instance verifying whether the transformation should be | ||
583 | * executed on this specific element. If it returns `false` or if an element | ||
584 | * does not match this style, this transformation rule does not *match*. | ||
585 | * * `right` – a function accepting an element and {@link CKEDITOR.filter.transformationsTools} | ||
586 | * or a string containing the name of the {@link CKEDITOR.filter.transformationsTools} method | ||
587 | * that should be called on an element. | ||
588 | * | ||
589 | * A shorthand format is also available. A transformation rule can be defined by | ||
590 | * a single string `'check:right'`. The string before `':'` will be used as | ||
591 | * the `check` property and the second part as the `right` property. | ||
592 | * | ||
593 | * Transformation rules can be grouped. The filter will try to apply | ||
594 | * the first rule in a group. If it *matches*, the filter will ignore subsequent rules and | ||
595 | * will move to the next group. If it does not *match*, the next rule will be checked. | ||
596 | * | ||
597 | * Examples: | ||
598 | * | ||
599 | * editor.filter.addTransformations( [ | ||
600 | * // First group. | ||
601 | * [ | ||
602 | * // First rule. If table{width} is allowed, it | ||
603 | * // executes {@link CKEDITOR.filter.transformationsTools#sizeToStyle} on a table element. | ||
604 | * 'table{width}: sizeToStyle', | ||
605 | * // Second rule should not be executed if the first was. | ||
606 | * 'table[width]: sizeToAttribute' | ||
607 | * ], | ||
608 | * // Second group. | ||
609 | * [ | ||
610 | * // This rule will add the foo="1" attribute to all images that | ||
611 | * // do not have it. | ||
612 | * { | ||
613 | * element: 'img', | ||
614 | * left: function( el ) { | ||
615 | * return !el.attributes.foo; | ||
616 | * }, | ||
617 | * right: function( el, tools ) { | ||
618 | * el.attributes.foo = '1'; | ||
619 | * } | ||
620 | * } | ||
621 | * ] | ||
622 | * ] ); | ||
623 | * | ||
624 | * // Case 1: | ||
625 | * // config.allowedContent = 'table{height,width}; tr td'. | ||
626 | * // | ||
627 | * // '<table style="height:100px; width:200px">...</table>' -> '<table style="height:100px; width:200px">...</table>' | ||
628 | * // '<table height="100" width="200">...</table>' -> '<table style="height:100px; width:200px">...</table>' | ||
629 | * | ||
630 | * // Case 2: | ||
631 | * // config.allowedContent = 'table[height,width]; tr td'. | ||
632 | * // | ||
633 | * // '<table style="height:100px; width:200px">...</table>' -> '<table height="100" width="200">...</table>' | ||
634 | * // '<table height="100" width="200">...</table>' -> '<table height="100" width="200"">...</table>' | ||
635 | * | ||
636 | * // Case 3: | ||
637 | * // config.allowedContent = 'table{width,height}[height,width]; tr td'. | ||
638 | * // | ||
639 | * // '<table style="height:100px; width:200px">...</table>' -> '<table style="height:100px; width:200px">...</table>' | ||
640 | * // '<table height="100" width="200">...</table>' -> '<table style="height:100px; width:200px">...</table>' | ||
641 | * // | ||
642 | * // Note: Both forms are allowed (size set by style and by attributes), but only | ||
643 | * // the first transformation is applied — the size is always transformed to a style. | ||
644 | * // This is because only the first transformation matching allowed content rules is applied. | ||
645 | * | ||
646 | * This method is used by the editor to add {@link CKEDITOR.feature#contentTransformations} | ||
647 | * when adding a feature by {@link #addFeature} or {@link CKEDITOR.editor#addFeature}. | ||
648 | * | ||
649 | * @param {Array} transformations | ||
650 | */ | ||
651 | addTransformations: function( transformations ) { | ||
652 | if ( this.disabled ) | ||
653 | return; | ||
654 | |||
655 | if ( !transformations ) | ||
656 | return; | ||
657 | |||
658 | var optimized = this._.transformations, | ||
659 | group, i; | ||
660 | |||
661 | for ( i = 0; i < transformations.length; ++i ) { | ||
662 | group = optimizeTransformationsGroup( transformations[ i ] ); | ||
663 | |||
664 | if ( !optimized[ group.name ] ) | ||
665 | optimized[ group.name ] = []; | ||
666 | |||
667 | optimized[ group.name ].push( group.rules ); | ||
668 | } | ||
669 | }, | ||
670 | |||
671 | /** | ||
672 | * Checks whether the content defined in the `test` argument is allowed | ||
673 | * by this filter. | ||
674 | * | ||
675 | * If `strictCheck` is set to `false` (default value), this method checks | ||
676 | * if all parts of the `test` (styles, attributes, and classes) are | ||
677 | * accepted by the filter. If `strictCheck` is set to `true`, the test | ||
678 | * must also contain the required attributes, styles, and classes. | ||
679 | * | ||
680 | * For example: | ||
681 | * | ||
682 | * // Rule: 'img[!src,alt]'. | ||
683 | * filter.check( 'img[alt]' ); // -> true | ||
684 | * filter.check( 'img[alt]', true, true ); // -> false | ||
685 | * | ||
686 | * Second `check()` call returned `false` because `src` is required. | ||
687 | * | ||
688 | * **Note:** The `test` argument is of {@link CKEDITOR.filter.contentRule} type, which is | ||
689 | * a limited version of {@link CKEDITOR.filter.allowedContentRules}. Read more about it | ||
690 | * in the {@link CKEDITOR.filter.contentRule}'s documentation. | ||
691 | * | ||
692 | * @param {CKEDITOR.filter.contentRule} test | ||
693 | * @param {Boolean} [applyTransformations=true] Whether to use registered transformations. | ||
694 | * @param {Boolean} [strictCheck] Whether the filter should check if an element with exactly | ||
695 | * these properties is allowed. | ||
696 | * @returns {Boolean} Returns `true` if the content is allowed. | ||
697 | */ | ||
698 | check: function( test, applyTransformations, strictCheck ) { | ||
699 | if ( this.disabled ) | ||
700 | return true; | ||
701 | |||
702 | // If rules are an array, expand it and return the logical OR value of | ||
703 | // the rules. | ||
704 | if ( CKEDITOR.tools.isArray( test ) ) { | ||
705 | for ( var i = test.length ; i-- ; ) { | ||
706 | if ( this.check( test[ i ], applyTransformations, strictCheck ) ) | ||
707 | return true; | ||
708 | } | ||
709 | return false; | ||
710 | } | ||
711 | |||
712 | var element, result, cacheKey; | ||
713 | |||
714 | if ( typeof test == 'string' ) { | ||
715 | cacheKey = test + '<' + ( applyTransformations === false ? '0' : '1' ) + ( strictCheck ? '1' : '0' ) + '>'; | ||
716 | |||
717 | // Check if result of this check hasn't been already cached. | ||
718 | if ( cacheKey in this._.cachedChecks ) | ||
719 | return this._.cachedChecks[ cacheKey ]; | ||
720 | |||
721 | // Create test element from string. | ||
722 | element = mockElementFromString( test ); | ||
723 | } else { | ||
724 | // Create test element from CKEDITOR.style. | ||
725 | element = mockElementFromStyle( test ); | ||
726 | } | ||
727 | |||
728 | // Make a deep copy. | ||
729 | var clone = CKEDITOR.tools.clone( element ), | ||
730 | toBeRemoved = [], | ||
731 | transformations; | ||
732 | |||
733 | // Apply transformations to original element. | ||
734 | // Transformations will be applied to clone by the filter function. | ||
735 | if ( applyTransformations !== false && ( transformations = this._.transformations[ element.name ] ) ) { | ||
736 | for ( i = 0; i < transformations.length; ++i ) | ||
737 | applyTransformationsGroup( this, element, transformations[ i ] ); | ||
738 | |||
739 | // Transformations could modify styles or classes, so they need to be copied | ||
740 | // to attributes object. | ||
741 | updateAttributes( element ); | ||
742 | } | ||
743 | |||
744 | // Filter clone of mocked element. | ||
745 | processElement( this, clone, toBeRemoved, { | ||
746 | doFilter: true, | ||
747 | doTransform: applyTransformations !== false, | ||
748 | skipRequired: !strictCheck, | ||
749 | skipFinalValidation: !strictCheck | ||
750 | } ); | ||
751 | |||
752 | // Element has been marked for removal. | ||
753 | if ( toBeRemoved.length > 0 ) { | ||
754 | result = false; | ||
755 | // Compare only left to right, because clone may be only trimmed version of original element. | ||
756 | } else if ( !CKEDITOR.tools.objectCompare( element.attributes, clone.attributes, true ) ) { | ||
757 | result = false; | ||
758 | } else { | ||
759 | result = true; | ||
760 | } | ||
761 | |||
762 | // Cache result of this test - we can build cache only for string tests. | ||
763 | if ( typeof test == 'string' ) | ||
764 | this._.cachedChecks[ cacheKey ] = result; | ||
765 | |||
766 | return result; | ||
767 | }, | ||
768 | |||
769 | /** | ||
770 | * Returns first enter mode allowed by this filter rules. Modes are checked in `p`, `div`, `br` order. | ||
771 | * If none of tags is allowed this method will return {@link CKEDITOR#ENTER_BR}. | ||
772 | * | ||
773 | * @since 4.3 | ||
774 | * @param {Number} defaultMode The default mode which will be checked as the first one. | ||
775 | * @param {Boolean} [reverse] Whether to check modes in reverse order (used for shift enter mode). | ||
776 | * @returns {Number} Allowed enter mode. | ||
777 | */ | ||
778 | getAllowedEnterMode: ( function() { | ||
779 | var tagsToCheck = [ 'p', 'div', 'br' ], | ||
780 | enterModes = { | ||
781 | p: CKEDITOR.ENTER_P, | ||
782 | div: CKEDITOR.ENTER_DIV, | ||
783 | br: CKEDITOR.ENTER_BR | ||
784 | }; | ||
785 | |||
786 | return function( defaultMode, reverse ) { | ||
787 | // Clone the array first. | ||
788 | var tags = tagsToCheck.slice(), | ||
789 | tag; | ||
790 | |||
791 | // Check the default mode first. | ||
792 | if ( this.check( enterModeTags[ defaultMode ] ) ) | ||
793 | return defaultMode; | ||
794 | |||
795 | // If not reverse order, reverse array so we can pop() from it. | ||
796 | if ( !reverse ) | ||
797 | tags = tags.reverse(); | ||
798 | |||
799 | while ( ( tag = tags.pop() ) ) { | ||
800 | if ( this.check( tag ) ) | ||
801 | return enterModes[ tag ]; | ||
802 | } | ||
803 | |||
804 | return CKEDITOR.ENTER_BR; | ||
805 | }; | ||
806 | } )(), | ||
807 | |||
808 | /** | ||
809 | * Destroys the filter instance and removes it from the global {@link CKEDITOR.filter#instances} object. | ||
810 | * | ||
811 | * @since 4.4.5 | ||
812 | */ | ||
813 | destroy: function() { | ||
814 | delete CKEDITOR.filter.instances[ this.id ]; | ||
815 | // Deleting reference to filter instance should be enough, | ||
816 | // but since these are big objects it's safe to clean them up too. | ||
817 | delete this._; | ||
818 | delete this.allowedContent; | ||
819 | delete this.disallowedContent; | ||
820 | } | ||
821 | }; | ||
822 | |||
823 | function addAndOptimizeRules( that, newRules, featureName, standardizedRules, optimizedRules ) { | ||
824 | var groupName, rule, | ||
825 | rulesToOptimize = []; | ||
826 | |||
827 | for ( groupName in newRules ) { | ||
828 | rule = newRules[ groupName ]; | ||
829 | |||
830 | // { 'p h1': true } => { 'p h1': {} }. | ||
831 | if ( typeof rule == 'boolean' ) | ||
832 | rule = {}; | ||
833 | // { 'p h1': func } => { 'p h1': { match: func } }. | ||
834 | else if ( typeof rule == 'function' ) | ||
835 | rule = { match: rule }; | ||
836 | // Clone (shallow) rule, because we'll modify it later. | ||
837 | else | ||
838 | rule = copy( rule ); | ||
839 | |||
840 | // If this is not an unnamed rule ({ '$1' => { ... } }) | ||
841 | // move elements list to property. | ||
842 | if ( groupName.charAt( 0 ) != '$' ) | ||
843 | rule.elements = groupName; | ||
844 | |||
845 | if ( featureName ) | ||
846 | rule.featureName = featureName.toLowerCase(); | ||
847 | |||
848 | standardizeRule( rule ); | ||
849 | |||
850 | // Save rule and remember to optimize it. | ||
851 | standardizedRules.push( rule ); | ||
852 | rulesToOptimize.push( rule ); | ||
853 | } | ||
854 | |||
855 | optimizeRules( optimizedRules, rulesToOptimize ); | ||
856 | } | ||
857 | |||
858 | // Apply ACR to an element. | ||
859 | // @param rule | ||
860 | // @param element | ||
861 | // @param status Object containing status of element's filtering. | ||
862 | // @param {Boolean} skipRequired If true don't check if element has all required properties. | ||
863 | function applyAllowedRule( rule, element, status, skipRequired ) { | ||
864 | // This rule doesn't match this element - skip it. | ||
865 | if ( rule.match && !rule.match( element ) ) | ||
866 | return; | ||
867 | |||
868 | // If element doesn't have all required styles/attrs/classes | ||
869 | // this rule doesn't match it. | ||
870 | if ( !skipRequired && !hasAllRequired( rule, element ) ) | ||
871 | return; | ||
872 | |||
873 | // If this rule doesn't validate properties only mark element as valid. | ||
874 | if ( !rule.propertiesOnly ) | ||
875 | status.valid = true; | ||
876 | |||
877 | // Apply rule only when all attrs/styles/classes haven't been marked as valid. | ||
878 | if ( !status.allAttributes ) | ||
879 | status.allAttributes = applyAllowedRuleToHash( rule.attributes, element.attributes, status.validAttributes ); | ||
880 | |||
881 | if ( !status.allStyles ) | ||
882 | status.allStyles = applyAllowedRuleToHash( rule.styles, element.styles, status.validStyles ); | ||
883 | |||
884 | if ( !status.allClasses ) | ||
885 | status.allClasses = applyAllowedRuleToArray( rule.classes, element.classes, status.validClasses ); | ||
886 | } | ||
887 | |||
888 | // Apply itemsRule to items (only classes are kept in array). | ||
889 | // Push accepted items to validItems array. | ||
890 | // Return true when all items are valid. | ||
891 | function applyAllowedRuleToArray( itemsRule, items, validItems ) { | ||
892 | if ( !itemsRule ) | ||
893 | return false; | ||
894 | |||
895 | // True means that all elements of array are accepted (the asterix was used for classes). | ||
896 | if ( itemsRule === true ) | ||
897 | return true; | ||
898 | |||
899 | for ( var i = 0, l = items.length, item; i < l; ++i ) { | ||
900 | item = items[ i ]; | ||
901 | if ( !validItems[ item ] ) | ||
902 | validItems[ item ] = itemsRule( item ); | ||
903 | } | ||
904 | |||
905 | return false; | ||
906 | } | ||
907 | |||
908 | function applyAllowedRuleToHash( itemsRule, items, validItems ) { | ||
909 | if ( !itemsRule ) | ||
910 | return false; | ||
911 | |||
912 | if ( itemsRule === true ) | ||
913 | return true; | ||
914 | |||
915 | for ( var name in items ) { | ||
916 | if ( !validItems[ name ] ) | ||
917 | validItems[ name ] = itemsRule( name ); | ||
918 | } | ||
919 | |||
920 | return false; | ||
921 | } | ||
922 | |||
923 | // Apply DACR rule to an element. | ||
924 | function applyDisallowedRule( rule, element, status ) { | ||
925 | // This rule doesn't match this element - skip it. | ||
926 | if ( rule.match && !rule.match( element ) ) | ||
927 | return; | ||
928 | |||
929 | // No properties - it's an element only rule so it disallows entire element. | ||
930 | // Early return is handled in filterElement. | ||
931 | if ( rule.noProperties ) | ||
932 | return false; | ||
933 | |||
934 | // Apply rule to attributes, styles and classes. Switch hadInvalid* to true if method returned true. | ||
935 | status.hadInvalidAttribute = applyDisallowedRuleToHash( rule.attributes, element.attributes ) || status.hadInvalidAttribute; | ||
936 | status.hadInvalidStyle = applyDisallowedRuleToHash( rule.styles, element.styles ) || status.hadInvalidStyle; | ||
937 | status.hadInvalidClass = applyDisallowedRuleToArray( rule.classes, element.classes ) || status.hadInvalidClass; | ||
938 | } | ||
939 | |||
940 | // Apply DACR to items (only classes are kept in array). | ||
941 | // @returns {Boolean} True if at least one of items was invalid (disallowed). | ||
942 | function applyDisallowedRuleToArray( itemsRule, items ) { | ||
943 | if ( !itemsRule ) | ||
944 | return false; | ||
945 | |||
946 | var hadInvalid = false, | ||
947 | allDisallowed = itemsRule === true; | ||
948 | |||
949 | for ( var i = items.length; i--; ) { | ||
950 | if ( allDisallowed || itemsRule( items[ i ] ) ) { | ||
951 | items.splice( i, 1 ); | ||
952 | hadInvalid = true; | ||
953 | } | ||
954 | } | ||
955 | |||
956 | return hadInvalid; | ||
957 | } | ||
958 | |||
959 | // Apply DACR to items (styles and attributes). | ||
960 | // @returns {Boolean} True if at least one of items was invalid (disallowed). | ||
961 | function applyDisallowedRuleToHash( itemsRule, items ) { | ||
962 | if ( !itemsRule ) | ||
963 | return false; | ||
964 | |||
965 | var hadInvalid = false, | ||
966 | allDisallowed = itemsRule === true; | ||
967 | |||
968 | for ( var name in items ) { | ||
969 | if ( allDisallowed || itemsRule( name ) ) { | ||
970 | delete items[ name ]; | ||
971 | hadInvalid = true; | ||
972 | } | ||
973 | } | ||
974 | |||
975 | return hadInvalid; | ||
976 | } | ||
977 | |||
978 | function beforeAddingRule( that, newRules, overrideCustom ) { | ||
979 | if ( that.disabled ) | ||
980 | return false; | ||
981 | |||
982 | // Don't override custom user's configuration if not explicitly requested. | ||
983 | if ( that.customConfig && !overrideCustom ) | ||
984 | return false; | ||
985 | |||
986 | if ( !newRules ) | ||
987 | return false; | ||
988 | |||
989 | // Clear cache, because new rules could change results of checks. | ||
990 | that._.cachedChecks = {}; | ||
991 | |||
992 | return true; | ||
993 | } | ||
994 | |||
995 | // Convert CKEDITOR.style to filter's rule. | ||
996 | function convertStyleToRules( style ) { | ||
997 | var styleDef = style.getDefinition(), | ||
998 | rules = {}, | ||
999 | rule, | ||
1000 | attrs = styleDef.attributes; | ||
1001 | |||
1002 | rules[ styleDef.element ] = rule = { | ||
1003 | styles: styleDef.styles, | ||
1004 | requiredStyles: styleDef.styles && CKEDITOR.tools.objectKeys( styleDef.styles ) | ||
1005 | }; | ||
1006 | |||
1007 | if ( attrs ) { | ||
1008 | attrs = copy( attrs ); | ||
1009 | rule.classes = attrs[ 'class' ] ? attrs[ 'class' ].split( /\s+/ ) : null; | ||
1010 | rule.requiredClasses = rule.classes; | ||
1011 | delete attrs[ 'class' ]; | ||
1012 | rule.attributes = attrs; | ||
1013 | rule.requiredAttributes = attrs && CKEDITOR.tools.objectKeys( attrs ); | ||
1014 | } | ||
1015 | |||
1016 | return rules; | ||
1017 | } | ||
1018 | |||
1019 | // Convert all validator formats (string, array, object, boolean) to hash or boolean: | ||
1020 | // * true is returned for '*'/true validator, | ||
1021 | // * false is returned for empty validator (no validator at all (false/null) or e.g. empty array), | ||
1022 | // * object is returned in other cases. | ||
1023 | function convertValidatorToHash( validator, delimiter ) { | ||
1024 | if ( !validator ) | ||
1025 | return false; | ||
1026 | |||
1027 | if ( validator === true ) | ||
1028 | return validator; | ||
1029 | |||
1030 | if ( typeof validator == 'string' ) { | ||
1031 | validator = trim( validator ); | ||
1032 | if ( validator == '*' ) | ||
1033 | return true; | ||
1034 | else | ||
1035 | return CKEDITOR.tools.convertArrayToObject( validator.split( delimiter ) ); | ||
1036 | } | ||
1037 | else if ( CKEDITOR.tools.isArray( validator ) ) { | ||
1038 | if ( validator.length ) | ||
1039 | return CKEDITOR.tools.convertArrayToObject( validator ); | ||
1040 | else | ||
1041 | return false; | ||
1042 | } | ||
1043 | // If object. | ||
1044 | else { | ||
1045 | var obj = {}, | ||
1046 | len = 0; | ||
1047 | |||
1048 | for ( var i in validator ) { | ||
1049 | obj[ i ] = validator[ i ]; | ||
1050 | len++; | ||
1051 | } | ||
1052 | |||
1053 | return len ? obj : false; | ||
1054 | } | ||
1055 | } | ||
1056 | |||
1057 | function executeElementCallbacks( element, callbacks ) { | ||
1058 | for ( var i = 0, l = callbacks.length, retVal; i < l; ++i ) { | ||
1059 | if ( ( retVal = callbacks[ i ]( element ) ) ) | ||
1060 | return retVal; | ||
1061 | } | ||
1062 | } | ||
1063 | |||
1064 | // Extract required properties from "required" validator and "all" properties. | ||
1065 | // Remove exclamation marks from "all" properties. | ||
1066 | // | ||
1067 | // E.g.: | ||
1068 | // requiredClasses = { cl1: true } | ||
1069 | // (all) classes = { cl1: true, cl2: true, '!cl3': true } | ||
1070 | // | ||
1071 | // result: | ||
1072 | // returned = { cl1: true, cl3: true } | ||
1073 | // all = { cl1: true, cl2: true, cl3: true } | ||
1074 | // | ||
1075 | // This function returns false if nothing is required. | ||
1076 | function extractRequired( required, all ) { | ||
1077 | var unbang = [], | ||
1078 | empty = true, | ||
1079 | i; | ||
1080 | |||
1081 | if ( required ) | ||
1082 | empty = false; | ||
1083 | else | ||
1084 | required = {}; | ||
1085 | |||
1086 | for ( i in all ) { | ||
1087 | if ( i.charAt( 0 ) == '!' ) { | ||
1088 | i = i.slice( 1 ); | ||
1089 | unbang.push( i ); | ||
1090 | required[ i ] = true; | ||
1091 | empty = false; | ||
1092 | } | ||
1093 | } | ||
1094 | |||
1095 | while ( ( i = unbang.pop() ) ) { | ||
1096 | all[ i ] = all[ '!' + i ]; | ||
1097 | delete all[ '!' + i ]; | ||
1098 | } | ||
1099 | |||
1100 | return empty ? false : required; | ||
1101 | } | ||
1102 | |||
1103 | // Does the actual filtering by appling allowed content rules | ||
1104 | // to the element. | ||
1105 | // | ||
1106 | // @param {CKEDITOR.filter} that The context. | ||
1107 | // @param {CKEDITOR.htmlParser.element} element | ||
1108 | // @param {Object} opts The same as in processElement. | ||
1109 | function filterElement( that, element, opts ) { | ||
1110 | var name = element.name, | ||
1111 | privObj = that._, | ||
1112 | allowedRules = privObj.allowedRules.elements[ name ], | ||
1113 | genericAllowedRules = privObj.allowedRules.generic, | ||
1114 | disallowedRules = privObj.disallowedRules.elements[ name ], | ||
1115 | genericDisallowedRules = privObj.disallowedRules.generic, | ||
1116 | skipRequired = opts.skipRequired, | ||
1117 | status = { | ||
1118 | // Whether any of rules accepted element. | ||
1119 | // If not - it will be stripped. | ||
1120 | valid: false, | ||
1121 | // Objects containing accepted attributes, classes and styles. | ||
1122 | validAttributes: {}, | ||
1123 | validClasses: {}, | ||
1124 | validStyles: {}, | ||
1125 | // Whether all are valid. | ||
1126 | // If we know that all element's attrs/classes/styles are valid | ||
1127 | // we can skip their validation, to improve performance. | ||
1128 | allAttributes: false, | ||
1129 | allClasses: false, | ||
1130 | allStyles: false, | ||
1131 | // Whether element had (before applying DACRs) at least one invalid attribute/class/style. | ||
1132 | hadInvalidAttribute: false, | ||
1133 | hadInvalidClass: false, | ||
1134 | hadInvalidStyle: false | ||
1135 | }, | ||
1136 | i, l; | ||
1137 | |||
1138 | // Early return - if there are no rules for this element (specific or generic), remove it. | ||
1139 | if ( !allowedRules && !genericAllowedRules ) | ||
1140 | return null; | ||
1141 | |||
1142 | // Could not be done yet if there were no transformations and if this | ||
1143 | // is real (not mocked) object. | ||
1144 | populateProperties( element ); | ||
1145 | |||
1146 | // Note - this step modifies element's styles, classes and attributes. | ||
1147 | if ( disallowedRules ) { | ||
1148 | for ( i = 0, l = disallowedRules.length; i < l; ++i ) { | ||
1149 | // Apply rule and make an early return if false is returned what means | ||
1150 | // that element is completely disallowed. | ||
1151 | if ( applyDisallowedRule( disallowedRules[ i ], element, status ) === false ) | ||
1152 | return null; | ||
1153 | } | ||
1154 | } | ||
1155 | |||
1156 | // Note - this step modifies element's styles, classes and attributes. | ||
1157 | if ( genericDisallowedRules ) { | ||
1158 | for ( i = 0, l = genericDisallowedRules.length; i < l; ++i ) | ||
1159 | applyDisallowedRule( genericDisallowedRules[ i ], element, status ); | ||
1160 | } | ||
1161 | |||
1162 | if ( allowedRules ) { | ||
1163 | for ( i = 0, l = allowedRules.length; i < l; ++i ) | ||
1164 | applyAllowedRule( allowedRules[ i ], element, status, skipRequired ); | ||
1165 | } | ||
1166 | |||
1167 | if ( genericAllowedRules ) { | ||
1168 | for ( i = 0, l = genericAllowedRules.length; i < l; ++i ) | ||
1169 | applyAllowedRule( genericAllowedRules[ i ], element, status, skipRequired ); | ||
1170 | } | ||
1171 | |||
1172 | return status; | ||
1173 | } | ||
1174 | |||
1175 | // Check whether element has all properties (styles,classes,attrs) required by a rule. | ||
1176 | function hasAllRequired( rule, element ) { | ||
1177 | if ( rule.nothingRequired ) | ||
1178 | return true; | ||
1179 | |||
1180 | var i, req, reqs, existing; | ||
1181 | |||
1182 | if ( ( reqs = rule.requiredClasses ) ) { | ||
1183 | existing = element.classes; | ||
1184 | for ( i = 0; i < reqs.length; ++i ) { | ||
1185 | req = reqs[ i ]; | ||
1186 | if ( typeof req == 'string' ) { | ||
1187 | if ( CKEDITOR.tools.indexOf( existing, req ) == -1 ) | ||
1188 | return false; | ||
1189 | } | ||
1190 | // This means regexp. | ||
1191 | else { | ||
1192 | if ( !CKEDITOR.tools.checkIfAnyArrayItemMatches( existing, req ) ) | ||
1193 | return false; | ||
1194 | } | ||
1195 | } | ||
1196 | } | ||
1197 | |||
1198 | return hasAllRequiredInHash( element.styles, rule.requiredStyles ) && | ||
1199 | hasAllRequiredInHash( element.attributes, rule.requiredAttributes ); | ||
1200 | } | ||
1201 | |||
1202 | // Check whether all items in required (array) exist in existing (object). | ||
1203 | function hasAllRequiredInHash( existing, required ) { | ||
1204 | if ( !required ) | ||
1205 | return true; | ||
1206 | |||
1207 | for ( var i = 0, req; i < required.length; ++i ) { | ||
1208 | req = required[ i ]; | ||
1209 | if ( typeof req == 'string' ) { | ||
1210 | if ( !( req in existing ) ) | ||
1211 | return false; | ||
1212 | } | ||
1213 | // This means regexp. | ||
1214 | else { | ||
1215 | if ( !CKEDITOR.tools.checkIfAnyObjectPropertyMatches( existing, req ) ) | ||
1216 | return false; | ||
1217 | } | ||
1218 | } | ||
1219 | |||
1220 | return true; | ||
1221 | } | ||
1222 | |||
1223 | // Create pseudo element that will be passed through filter | ||
1224 | // to check if tested string is allowed. | ||
1225 | function mockElementFromString( str ) { | ||
1226 | var element = parseRulesString( str ).$1, | ||
1227 | styles = element.styles, | ||
1228 | classes = element.classes; | ||
1229 | |||
1230 | element.name = element.elements; | ||
1231 | element.classes = classes = ( classes ? classes.split( /\s*,\s*/ ) : [] ); | ||
1232 | element.styles = mockHash( styles ); | ||
1233 | element.attributes = mockHash( element.attributes ); | ||
1234 | element.children = []; | ||
1235 | |||
1236 | if ( classes.length ) | ||
1237 | element.attributes[ 'class' ] = classes.join( ' ' ); | ||
1238 | if ( styles ) | ||
1239 | element.attributes.style = CKEDITOR.tools.writeCssText( element.styles ); | ||
1240 | |||
1241 | return element; | ||
1242 | } | ||
1243 | |||
1244 | // Create pseudo element that will be passed through filter | ||
1245 | // to check if tested style is allowed. | ||
1246 | function mockElementFromStyle( style ) { | ||
1247 | var styleDef = style.getDefinition(), | ||
1248 | styles = styleDef.styles, | ||
1249 | attrs = styleDef.attributes || {}; | ||
1250 | |||
1251 | if ( styles ) { | ||
1252 | styles = copy( styles ); | ||
1253 | attrs.style = CKEDITOR.tools.writeCssText( styles, true ); | ||
1254 | } else { | ||
1255 | styles = {}; | ||
1256 | } | ||
1257 | |||
1258 | var el = { | ||
1259 | name: styleDef.element, | ||
1260 | attributes: attrs, | ||
1261 | classes: attrs[ 'class' ] ? attrs[ 'class' ].split( /\s+/ ) : [], | ||
1262 | styles: styles, | ||
1263 | children: [] | ||
1264 | }; | ||
1265 | |||
1266 | return el; | ||
1267 | } | ||
1268 | |||
1269 | // Mock hash based on string. | ||
1270 | // 'a,b,c' => { a: 'cke-test', b: 'cke-test', c: 'cke-test' } | ||
1271 | // Used to mock styles and attributes objects. | ||
1272 | function mockHash( str ) { | ||
1273 | // It may be a null or empty string. | ||
1274 | if ( !str ) | ||
1275 | return {}; | ||
1276 | |||
1277 | var keys = str.split( /\s*,\s*/ ).sort(), | ||
1278 | obj = {}; | ||
1279 | |||
1280 | while ( keys.length ) | ||
1281 | obj[ keys.shift() ] = TEST_VALUE; | ||
1282 | |||
1283 | return obj; | ||
1284 | } | ||
1285 | |||
1286 | // Extract properties names from the object | ||
1287 | // and replace those containing wildcards with regexps. | ||
1288 | // Note: there's a room for performance improvement. Array of mixed types | ||
1289 | // breaks JIT-compiler optiomization what may invalidate compilation of pretty a lot of code. | ||
1290 | // | ||
1291 | // @returns An array of strings and regexps. | ||
1292 | function optimizeRequiredProperties( requiredProperties ) { | ||
1293 | var arr = []; | ||
1294 | for ( var propertyName in requiredProperties ) { | ||
1295 | if ( propertyName.indexOf( '*' ) > -1 ) | ||
1296 | arr.push( new RegExp( '^' + propertyName.replace( /\*/g, '.*' ) + '$' ) ); | ||
1297 | else | ||
1298 | arr.push( propertyName ); | ||
1299 | } | ||
1300 | return arr; | ||
1301 | } | ||
1302 | |||
1303 | var validators = { styles: 1, attributes: 1, classes: 1 }, | ||
1304 | validatorsRequired = { | ||
1305 | styles: 'requiredStyles', | ||
1306 | attributes: 'requiredAttributes', | ||
1307 | classes: 'requiredClasses' | ||
1308 | }; | ||
1309 | |||
1310 | // Optimize a rule by replacing validators with functions | ||
1311 | // and rewriting requiredXXX validators to arrays. | ||
1312 | function optimizeRule( rule ) { | ||
1313 | var validatorName, | ||
1314 | requiredProperties, | ||
1315 | i; | ||
1316 | |||
1317 | for ( validatorName in validators ) | ||
1318 | rule[ validatorName ] = validatorFunction( rule[ validatorName ] ); | ||
1319 | |||
1320 | var nothingRequired = true; | ||
1321 | for ( i in validatorsRequired ) { | ||
1322 | validatorName = validatorsRequired[ i ]; | ||
1323 | requiredProperties = optimizeRequiredProperties( rule[ validatorName ] ); | ||
1324 | // Don't set anything if there are no required properties. This will allow to | ||
1325 | // save some memory by GCing all empty arrays (requiredProperties). | ||
1326 | if ( requiredProperties.length ) { | ||
1327 | rule[ validatorName ] = requiredProperties; | ||
1328 | nothingRequired = false; | ||
1329 | } | ||
1330 | } | ||
1331 | |||
1332 | rule.nothingRequired = nothingRequired; | ||
1333 | rule.noProperties = !( rule.attributes || rule.classes || rule.styles ); | ||
1334 | } | ||
1335 | |||
1336 | // Add optimized version of rule to optimizedRules object. | ||
1337 | function optimizeRules( optimizedRules, rules ) { | ||
1338 | var elementsRules = optimizedRules.elements, | ||
1339 | genericRules = optimizedRules.generic, | ||
1340 | i, l, rule, element, priority; | ||
1341 | |||
1342 | for ( i = 0, l = rules.length; i < l; ++i ) { | ||
1343 | // Shallow copy. Do not modify original rule. | ||
1344 | rule = copy( rules[ i ] ); | ||
1345 | priority = rule.classes === true || rule.styles === true || rule.attributes === true; | ||
1346 | optimizeRule( rule ); | ||
1347 | |||
1348 | // E.g. "*(xxx)[xxx]" - it's a generic rule that | ||
1349 | // validates properties only. | ||
1350 | // Or '$1': { match: function() {...} } | ||
1351 | if ( rule.elements === true || rule.elements === null ) { | ||
1352 | // Add priority rules at the beginning. | ||
1353 | genericRules[ priority ? 'unshift' : 'push' ]( rule ); | ||
1354 | } | ||
1355 | // If elements list was explicitly defined, | ||
1356 | // add this rule for every defined element. | ||
1357 | else { | ||
1358 | // We don't need elements validator for this kind of rule. | ||
1359 | var elements = rule.elements; | ||
1360 | delete rule.elements; | ||
1361 | |||
1362 | for ( element in elements ) { | ||
1363 | if ( !elementsRules[ element ] ) | ||
1364 | elementsRules[ element ] = [ rule ]; | ||
1365 | else | ||
1366 | elementsRules[ element ][ priority ? 'unshift' : 'push' ]( rule ); | ||
1367 | } | ||
1368 | } | ||
1369 | } | ||
1370 | } | ||
1371 | |||
1372 | // < elements >< styles, attributes and classes >< separator > | ||
1373 | var rulePattern = /^([a-z0-9\-*\s]+)((?:\s*\{[!\w\-,\s\*]+\}\s*|\s*\[[!\w\-,\s\*]+\]\s*|\s*\([!\w\-,\s\*]+\)\s*){0,3})(?:;\s*|$)/i, | ||
1374 | groupsPatterns = { | ||
1375 | styles: /{([^}]+)}/, | ||
1376 | attrs: /\[([^\]]+)\]/, | ||
1377 | classes: /\(([^\)]+)\)/ | ||
1378 | }; | ||
1379 | |||
1380 | function parseRulesString( input ) { | ||
1381 | var match, | ||
1382 | props, styles, attrs, classes, | ||
1383 | rules = {}, | ||
1384 | groupNum = 1; | ||
1385 | |||
1386 | input = trim( input ); | ||
1387 | |||
1388 | while ( ( match = input.match( rulePattern ) ) ) { | ||
1389 | if ( ( props = match[ 2 ] ) ) { | ||
1390 | styles = parseProperties( props, 'styles' ); | ||
1391 | attrs = parseProperties( props, 'attrs' ); | ||
1392 | classes = parseProperties( props, 'classes' ); | ||
1393 | } else { | ||
1394 | styles = attrs = classes = null; | ||
1395 | } | ||
1396 | |||
1397 | // Add as an unnamed rule, because there can be two rules | ||
1398 | // for one elements set defined in string format. | ||
1399 | rules[ '$' + groupNum++ ] = { | ||
1400 | elements: match[ 1 ], | ||
1401 | classes: classes, | ||
1402 | styles: styles, | ||
1403 | attributes: attrs | ||
1404 | }; | ||
1405 | |||
1406 | // Move to the next group. | ||
1407 | input = input.slice( match[ 0 ].length ); | ||
1408 | } | ||
1409 | |||
1410 | return rules; | ||
1411 | } | ||
1412 | |||
1413 | // Extract specified properties group (styles, attrs, classes) from | ||
1414 | // what stands after the elements list in string format of allowedContent. | ||
1415 | function parseProperties( properties, groupName ) { | ||
1416 | var group = properties.match( groupsPatterns[ groupName ] ); | ||
1417 | return group ? trim( group[ 1 ] ) : null; | ||
1418 | } | ||
1419 | |||
1420 | function populateProperties( element ) { | ||
1421 | // Backup styles and classes, because they may be removed by DACRs. | ||
1422 | // We'll need them in updateElement(). | ||
1423 | var styles = element.styleBackup = element.attributes.style, | ||
1424 | classes = element.classBackup = element.attributes[ 'class' ]; | ||
1425 | |||
1426 | // Parse classes and styles if that hasn't been done before. | ||
1427 | if ( !element.styles ) | ||
1428 | element.styles = CKEDITOR.tools.parseCssText( styles || '', 1 ); | ||
1429 | if ( !element.classes ) | ||
1430 | element.classes = classes ? classes.split( /\s+/ ) : []; | ||
1431 | } | ||
1432 | |||
1433 | // Filter element protected with a comment. | ||
1434 | // Returns true if protected content is ok, false otherwise. | ||
1435 | function processProtectedElement( that, comment, protectedRegexs, filterOpts ) { | ||
1436 | var source = decodeURIComponent( comment.value.replace( /^\{cke_protected\}/, '' ) ), | ||
1437 | protectedFrag, | ||
1438 | toBeRemoved = [], | ||
1439 | node, i, match; | ||
1440 | |||
1441 | // Protected element's and protected source's comments look exactly the same. | ||
1442 | // Check if what we have isn't a protected source instead of protected script/noscript. | ||
1443 | if ( protectedRegexs ) { | ||
1444 | for ( i = 0; i < protectedRegexs.length; ++i ) { | ||
1445 | if ( ( match = source.match( protectedRegexs[ i ] ) ) && | ||
1446 | match[ 0 ].length == source.length // Check whether this pattern matches entire source | ||
1447 | // to avoid '<script>alert("<? 1 ?>")</script>' matching | ||
1448 | // the PHP's protectedSource regexp. | ||
1449 | ) | ||
1450 | return true; | ||
1451 | } | ||
1452 | } | ||
1453 | |||
1454 | protectedFrag = CKEDITOR.htmlParser.fragment.fromHtml( source ); | ||
1455 | |||
1456 | if ( protectedFrag.children.length == 1 && ( node = protectedFrag.children[ 0 ] ).type == CKEDITOR.NODE_ELEMENT ) | ||
1457 | processElement( that, node, toBeRemoved, filterOpts ); | ||
1458 | |||
1459 | // If protected element has been marked to be removed, return 'false' - comment was rejected. | ||
1460 | return !toBeRemoved.length; | ||
1461 | } | ||
1462 | |||
1463 | var unprotectElementsNamesRegexp = /^cke:(object|embed|param)$/, | ||
1464 | protectElementsNamesRegexp = /^(object|embed|param)$/; | ||
1465 | |||
1466 | // The actual function which filters, transforms and does other funny things with an element. | ||
1467 | // | ||
1468 | // @param {CKEDITOR.filter} that Context. | ||
1469 | // @param {CKEDITOR.htmlParser.element} element The element to be processed. | ||
1470 | // @param {Array} toBeRemoved Array into which elements rejected by the filter will be pushed. | ||
1471 | // @param {Boolean} [opts.doFilter] Whether element should be filtered. | ||
1472 | // @param {Boolean} [opts.doTransform] Whether transformations should be applied. | ||
1473 | // @param {Boolean} [opts.doCallbacks] Whether to execute element callbacks. | ||
1474 | // @param {Boolean} [opts.toHtml] Set to true if filter used together with htmlDP#toHtml | ||
1475 | // @param {Boolean} [opts.skipRequired] Whether element's required properties shouldn't be verified. | ||
1476 | // @param {Boolean} [opts.skipFinalValidation] Whether to not perform final element validation (a,img). | ||
1477 | // @returns {Number} Possible flags: | ||
1478 | // * FILTER_ELEMENT_MODIFIED, | ||
1479 | // * FILTER_SKIP_TREE. | ||
1480 | function processElement( that, element, toBeRemoved, opts ) { | ||
1481 | var status, | ||
1482 | retVal = 0, | ||
1483 | callbacksRetVal; | ||
1484 | |||
1485 | // Unprotect elements names previously protected by htmlDataProcessor | ||
1486 | // (see protectElementNames and protectSelfClosingElements functions). | ||
1487 | // Note: body, title, etc. are not protected by htmlDataP (or are protected and then unprotected). | ||
1488 | if ( opts.toHtml ) | ||
1489 | element.name = element.name.replace( unprotectElementsNamesRegexp, '$1' ); | ||
1490 | |||
1491 | // Execute element callbacks and return if one of them returned any value. | ||
1492 | if ( opts.doCallbacks && that.elementCallbacks ) { | ||
1493 | // For now we only support here FILTER_SKIP_TREE, so we can early return if retVal is truly value. | ||
1494 | if ( ( callbacksRetVal = executeElementCallbacks( element, that.elementCallbacks ) ) ) | ||
1495 | return callbacksRetVal; | ||
1496 | } | ||
1497 | |||
1498 | // If transformations are set apply all groups. | ||
1499 | if ( opts.doTransform ) | ||
1500 | transformElement( that, element ); | ||
1501 | |||
1502 | if ( opts.doFilter ) { | ||
1503 | // Apply all filters. | ||
1504 | status = filterElement( that, element, opts ); | ||
1505 | |||
1506 | // Handle early return from filterElement. | ||
1507 | if ( !status ) { | ||
1508 | toBeRemoved.push( element ); | ||
1509 | return FILTER_ELEMENT_MODIFIED; | ||
1510 | } | ||
1511 | |||
1512 | // Finally, if after running all filter rules it still hasn't been allowed - remove it. | ||
1513 | if ( !status.valid ) { | ||
1514 | toBeRemoved.push( element ); | ||
1515 | return FILTER_ELEMENT_MODIFIED; | ||
1516 | } | ||
1517 | |||
1518 | // Update element's attributes based on status of filtering. | ||
1519 | if ( updateElement( element, status ) ) | ||
1520 | retVal = FILTER_ELEMENT_MODIFIED; | ||
1521 | |||
1522 | if ( !opts.skipFinalValidation && !validateElement( element ) ) { | ||
1523 | toBeRemoved.push( element ); | ||
1524 | return FILTER_ELEMENT_MODIFIED; | ||
1525 | } | ||
1526 | } | ||
1527 | |||
1528 | // Protect previously unprotected elements. | ||
1529 | if ( opts.toHtml ) | ||
1530 | element.name = element.name.replace( protectElementsNamesRegexp, 'cke:$1' ); | ||
1531 | |||
1532 | return retVal; | ||
1533 | } | ||
1534 | |||
1535 | // Returns a regexp object which can be used to test if a property | ||
1536 | // matches one of wildcard validators. | ||
1537 | function regexifyPropertiesWithWildcards( validators ) { | ||
1538 | var patterns = [], | ||
1539 | i; | ||
1540 | |||
1541 | for ( i in validators ) { | ||
1542 | if ( i.indexOf( '*' ) > -1 ) | ||
1543 | patterns.push( i.replace( /\*/g, '.*' ) ); | ||
1544 | } | ||
1545 | |||
1546 | if ( patterns.length ) | ||
1547 | return new RegExp( '^(?:' + patterns.join( '|' ) + ')$' ); | ||
1548 | else | ||
1549 | return null; | ||
1550 | } | ||
1551 | |||
1552 | // Standardize a rule by converting all validators to hashes. | ||
1553 | function standardizeRule( rule ) { | ||
1554 | rule.elements = convertValidatorToHash( rule.elements, /\s+/ ) || null; | ||
1555 | rule.propertiesOnly = rule.propertiesOnly || ( rule.elements === true ); | ||
1556 | |||
1557 | var delim = /\s*,\s*/, | ||
1558 | i; | ||
1559 | |||
1560 | for ( i in validators ) { | ||
1561 | rule[ i ] = convertValidatorToHash( rule[ i ], delim ) || null; | ||
1562 | rule[ validatorsRequired[ i ] ] = extractRequired( convertValidatorToHash( | ||
1563 | rule[ validatorsRequired[ i ] ], delim ), rule[ i ] ) || null; | ||
1564 | } | ||
1565 | |||
1566 | rule.match = rule.match || null; | ||
1567 | } | ||
1568 | |||
1569 | // Does the element transformation by applying registered | ||
1570 | // transformation rules. | ||
1571 | function transformElement( that, element ) { | ||
1572 | var transformations = that._.transformations[ element.name ], | ||
1573 | i; | ||
1574 | |||
1575 | if ( !transformations ) | ||
1576 | return; | ||
1577 | |||
1578 | populateProperties( element ); | ||
1579 | |||
1580 | for ( i = 0; i < transformations.length; ++i ) | ||
1581 | applyTransformationsGroup( that, element, transformations[ i ] ); | ||
1582 | |||
1583 | // Do not count on updateElement() which is called in processElement, because it: | ||
1584 | // * may not be called, | ||
1585 | // * may skip some properties when all are marked as valid. | ||
1586 | updateAttributes( element ); | ||
1587 | } | ||
1588 | |||
1589 | // Copy element's styles and classes back to attributes array. | ||
1590 | function updateAttributes( element ) { | ||
1591 | var attrs = element.attributes, | ||
1592 | styles; | ||
1593 | |||
1594 | // Will be recreated later if any of styles/classes exists. | ||
1595 | delete attrs.style; | ||
1596 | delete attrs[ 'class' ]; | ||
1597 | |||
1598 | if ( ( styles = CKEDITOR.tools.writeCssText( element.styles, true ) ) ) | ||
1599 | attrs.style = styles; | ||
1600 | |||
1601 | if ( element.classes.length ) | ||
1602 | attrs[ 'class' ] = element.classes.sort().join( ' ' ); | ||
1603 | } | ||
1604 | |||
1605 | // Update element object based on status of filtering. | ||
1606 | // @returns Whether element was modified. | ||
1607 | function updateElement( element, status ) { | ||
1608 | var validAttrs = status.validAttributes, | ||
1609 | validStyles = status.validStyles, | ||
1610 | validClasses = status.validClasses, | ||
1611 | attrs = element.attributes, | ||
1612 | styles = element.styles, | ||
1613 | classes = element.classes, | ||
1614 | origClasses = element.classBackup, | ||
1615 | origStyles = element.styleBackup, | ||
1616 | name, origName, i, | ||
1617 | stylesArr = [], | ||
1618 | classesArr = [], | ||
1619 | internalAttr = /^data-cke-/, | ||
1620 | isModified = false; | ||
1621 | |||
1622 | // Will be recreated later if any of styles/classes were passed. | ||
1623 | delete attrs.style; | ||
1624 | delete attrs[ 'class' ]; | ||
1625 | // Clean up. | ||
1626 | delete element.classBackup; | ||
1627 | delete element.styleBackup; | ||
1628 | |||
1629 | if ( !status.allAttributes ) { | ||
1630 | for ( name in attrs ) { | ||
1631 | // If not valid and not internal attribute delete it. | ||
1632 | if ( !validAttrs[ name ] ) { | ||
1633 | // Allow all internal attibutes... | ||
1634 | if ( internalAttr.test( name ) ) { | ||
1635 | // ... unless this is a saved attribute and the original one isn't allowed. | ||
1636 | if ( name != ( origName = name.replace( /^data-cke-saved-/, '' ) ) && | ||
1637 | !validAttrs[ origName ] | ||
1638 | ) { | ||
1639 | delete attrs[ name ]; | ||
1640 | isModified = true; | ||
1641 | } | ||
1642 | } else { | ||
1643 | delete attrs[ name ]; | ||
1644 | isModified = true; | ||
1645 | } | ||
1646 | } | ||
1647 | |||
1648 | } | ||
1649 | } | ||
1650 | |||
1651 | if ( !status.allStyles || status.hadInvalidStyle ) { | ||
1652 | for ( name in styles ) { | ||
1653 | // We check status.allStyles because when there was a '*' ACR and some | ||
1654 | // DACR we have now both properties true - status.allStyles and status.hadInvalidStyle. | ||
1655 | // However unlike in the case when we only have '*' ACR, in which we can just copy original | ||
1656 | // styles, in this case we must copy only those styles which were not removed by DACRs. | ||
1657 | if ( status.allStyles || validStyles[ name ] ) | ||
1658 | stylesArr.push( name + ':' + styles[ name ] ); | ||
1659 | else | ||
1660 | isModified = true; | ||
1661 | } | ||
1662 | if ( stylesArr.length ) | ||
1663 | attrs.style = stylesArr.sort().join( '; ' ); | ||
1664 | } | ||
1665 | else if ( origStyles ) { | ||
1666 | attrs.style = origStyles; | ||
1667 | } | ||
1668 | |||
1669 | if ( !status.allClasses || status.hadInvalidClass ) { | ||
1670 | for ( i = 0; i < classes.length; ++i ) { | ||
1671 | // See comment for styles. | ||
1672 | if ( status.allClasses || validClasses[ classes[ i ] ] ) | ||
1673 | classesArr.push( classes[ i ] ); | ||
1674 | } | ||
1675 | if ( classesArr.length ) | ||
1676 | attrs[ 'class' ] = classesArr.sort().join( ' ' ); | ||
1677 | |||
1678 | if ( origClasses && classesArr.length < origClasses.split( /\s+/ ).length ) | ||
1679 | isModified = true; | ||
1680 | } | ||
1681 | else if ( origClasses ) { | ||
1682 | attrs[ 'class' ] = origClasses; | ||
1683 | } | ||
1684 | |||
1685 | return isModified; | ||
1686 | } | ||
1687 | |||
1688 | function validateElement( element ) { | ||
1689 | switch ( element.name ) { | ||
1690 | case 'a': | ||
1691 | // Code borrowed from htmlDataProcessor, so ACF does the same clean up. | ||
1692 | if ( !( element.children.length || element.attributes.name || element.attributes.id ) ) | ||
1693 | return false; | ||
1694 | break; | ||
1695 | case 'img': | ||
1696 | if ( !element.attributes.src ) | ||
1697 | return false; | ||
1698 | break; | ||
1699 | } | ||
1700 | |||
1701 | return true; | ||
1702 | } | ||
1703 | |||
1704 | function validatorFunction( validator ) { | ||
1705 | if ( !validator ) | ||
1706 | return false; | ||
1707 | if ( validator === true ) | ||
1708 | return true; | ||
1709 | |||
1710 | // Note: We don't need to remove properties with wildcards from the validator object. | ||
1711 | // E.g. data-* is actually an edge case of /^data-.*$/, so when it's accepted | ||
1712 | // by `value in validator` it's ok. | ||
1713 | var regexp = regexifyPropertiesWithWildcards( validator ); | ||
1714 | |||
1715 | return function( value ) { | ||
1716 | return value in validator || ( regexp && value.match( regexp ) ); | ||
1717 | }; | ||
1718 | } | ||
1719 | |||
1720 | // | ||
1721 | // REMOVE ELEMENT --------------------------------------------------------- | ||
1722 | // | ||
1723 | |||
1724 | // Check whether all children will be valid in new context. | ||
1725 | // Note: it doesn't verify if text node is valid, because | ||
1726 | // new parent should accept them. | ||
1727 | function checkChildren( children, newParentName ) { | ||
1728 | var allowed = DTD[ newParentName ]; | ||
1729 | |||
1730 | for ( var i = 0, l = children.length, child; i < l; ++i ) { | ||
1731 | child = children[ i ]; | ||
1732 | if ( child.type == CKEDITOR.NODE_ELEMENT && !allowed[ child.name ] ) | ||
1733 | return false; | ||
1734 | } | ||
1735 | |||
1736 | return true; | ||
1737 | } | ||
1738 | |||
1739 | function createBr() { | ||
1740 | return new CKEDITOR.htmlParser.element( 'br' ); | ||
1741 | } | ||
1742 | |||
1743 | // Whether this is an inline element or text. | ||
1744 | function inlineNode( node ) { | ||
1745 | return node.type == CKEDITOR.NODE_TEXT || | ||
1746 | node.type == CKEDITOR.NODE_ELEMENT && DTD.$inline[ node.name ]; | ||
1747 | } | ||
1748 | |||
1749 | function isBrOrBlock( node ) { | ||
1750 | return node.type == CKEDITOR.NODE_ELEMENT && | ||
1751 | ( node.name == 'br' || DTD.$block[ node.name ] ); | ||
1752 | } | ||
1753 | |||
1754 | // Try to remove element in the best possible way. | ||
1755 | // | ||
1756 | // @param {Array} toBeChecked After executing this function | ||
1757 | // this array will contain elements that should be checked | ||
1758 | // because they were marked as potentially: | ||
1759 | // * in wrong context (e.g. li in body), | ||
1760 | // * empty elements from $removeEmpty, | ||
1761 | // * incorrect img/a/other element validated by validateElement(). | ||
1762 | function removeElement( element, enterTag, toBeChecked ) { | ||
1763 | var name = element.name; | ||
1764 | |||
1765 | if ( DTD.$empty[ name ] || !element.children.length ) { | ||
1766 | // Special case - hr in br mode should be replaced with br, not removed. | ||
1767 | if ( name == 'hr' && enterTag == 'br' ) | ||
1768 | element.replaceWith( createBr() ); | ||
1769 | else { | ||
1770 | // Parent might become an empty inline specified in $removeEmpty or empty a[href]. | ||
1771 | if ( element.parent ) | ||
1772 | toBeChecked.push( { check: 'it', el: element.parent } ); | ||
1773 | |||
1774 | element.remove(); | ||
1775 | } | ||
1776 | } else if ( DTD.$block[ name ] || name == 'tr' ) { | ||
1777 | if ( enterTag == 'br' ) | ||
1778 | stripBlockBr( element, toBeChecked ); | ||
1779 | else | ||
1780 | stripBlock( element, enterTag, toBeChecked ); | ||
1781 | } | ||
1782 | // Special case - elements that may contain CDATA should be removed completely. | ||
1783 | else if ( name in { style: 1, script: 1 } ) | ||
1784 | element.remove(); | ||
1785 | // The rest of inline elements. May also be the last resort | ||
1786 | // for some special elements. | ||
1787 | else { | ||
1788 | // Parent might become an empty inline specified in $removeEmpty or empty a[href]. | ||
1789 | if ( element.parent ) | ||
1790 | toBeChecked.push( { check: 'it', el: element.parent } ); | ||
1791 | element.replaceWithChildren(); | ||
1792 | } | ||
1793 | } | ||
1794 | |||
1795 | // Strip element block, but leave its content. | ||
1796 | // Works in 'div' and 'p' enter modes. | ||
1797 | function stripBlock( element, enterTag, toBeChecked ) { | ||
1798 | var children = element.children; | ||
1799 | |||
1800 | // First, check if element's children may be wrapped with <p/div>. | ||
1801 | // Ignore that <p/div> may not be allowed in element.parent. | ||
1802 | // This will be fixed when removing parent or by toBeChecked rule. | ||
1803 | if ( checkChildren( children, enterTag ) ) { | ||
1804 | element.name = enterTag; | ||
1805 | element.attributes = {}; | ||
1806 | // Check if this p/div was put in correct context. | ||
1807 | // If not - strip parent. | ||
1808 | toBeChecked.push( { check: 'parent-down', el: element } ); | ||
1809 | return; | ||
1810 | } | ||
1811 | |||
1812 | var parent = element.parent, | ||
1813 | shouldAutoP = parent.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT || parent.name == 'body', | ||
1814 | i, child, p, parentDtd; | ||
1815 | |||
1816 | for ( i = children.length; i > 0; ) { | ||
1817 | child = children[ --i ]; | ||
1818 | |||
1819 | // If parent requires auto paragraphing and child is inline node, | ||
1820 | // insert this child into newly created paragraph. | ||
1821 | if ( shouldAutoP && inlineNode( child ) ) { | ||
1822 | if ( !p ) { | ||
1823 | p = new CKEDITOR.htmlParser.element( enterTag ); | ||
1824 | p.insertAfter( element ); | ||
1825 | |||
1826 | // Check if this p/div was put in correct context. | ||
1827 | // If not - strip parent. | ||
1828 | toBeChecked.push( { check: 'parent-down', el: p } ); | ||
1829 | } | ||
1830 | p.add( child, 0 ); | ||
1831 | } | ||
1832 | // Child which doesn't need to be auto paragraphed. | ||
1833 | else { | ||
1834 | p = null; | ||
1835 | parentDtd = DTD[ parent.name ] || DTD.span; | ||
1836 | |||
1837 | child.insertAfter( element ); | ||
1838 | // If inserted into invalid context, mark it and check | ||
1839 | // after removing all elements. | ||
1840 | if ( parent.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT && | ||
1841 | child.type == CKEDITOR.NODE_ELEMENT && | ||
1842 | !parentDtd[ child.name ] | ||
1843 | ) | ||
1844 | toBeChecked.push( { check: 'el-up', el: child } ); | ||
1845 | } | ||
1846 | } | ||
1847 | |||
1848 | // All children have been moved to element's parent, so remove it. | ||
1849 | element.remove(); | ||
1850 | } | ||
1851 | |||
1852 | // Prepend/append block with <br> if isn't | ||
1853 | // already prepended/appended with <br> or block and | ||
1854 | // isn't first/last child of its parent. | ||
1855 | // Then replace element with its children. | ||
1856 | // <p>a</p><p>b</p> => <p>a</p><br>b => a<br>b | ||
1857 | function stripBlockBr( element ) { | ||
1858 | var br; | ||
1859 | |||
1860 | if ( element.previous && !isBrOrBlock( element.previous ) ) { | ||
1861 | br = createBr(); | ||
1862 | br.insertBefore( element ); | ||
1863 | } | ||
1864 | |||
1865 | if ( element.next && !isBrOrBlock( element.next ) ) { | ||
1866 | br = createBr(); | ||
1867 | br.insertAfter( element ); | ||
1868 | } | ||
1869 | |||
1870 | element.replaceWithChildren(); | ||
1871 | } | ||
1872 | |||
1873 | // | ||
1874 | // TRANSFORMATIONS -------------------------------------------------------- | ||
1875 | // | ||
1876 | |||
1877 | // Apply given transformations group to the element. | ||
1878 | function applyTransformationsGroup( filter, element, group ) { | ||
1879 | var i, rule; | ||
1880 | |||
1881 | for ( i = 0; i < group.length; ++i ) { | ||
1882 | rule = group[ i ]; | ||
1883 | |||
1884 | // Test with #check or #left only if it's set. | ||
1885 | // Do not apply transformations because that creates infinite loop. | ||
1886 | if ( ( !rule.check || filter.check( rule.check, false ) ) && | ||
1887 | ( !rule.left || rule.left( element ) ) ) { | ||
1888 | rule.right( element, transformationsTools ); | ||
1889 | return; // Only first matching rule in a group is executed. | ||
1890 | } | ||
1891 | } | ||
1892 | } | ||
1893 | |||
1894 | // Check whether element matches CKEDITOR.style. | ||
1895 | // The element can be a "superset" of style, | ||
1896 | // e.g. it may have more classes, but need to have | ||
1897 | // at least those defined in style. | ||
1898 | function elementMatchesStyle( element, style ) { | ||
1899 | var def = style.getDefinition(), | ||
1900 | defAttrs = def.attributes, | ||
1901 | defStyles = def.styles, | ||
1902 | attrName, styleName, | ||
1903 | classes, classPattern, cl; | ||
1904 | |||
1905 | if ( element.name != def.element ) | ||
1906 | return false; | ||
1907 | |||
1908 | for ( attrName in defAttrs ) { | ||
1909 | if ( attrName == 'class' ) { | ||
1910 | classes = defAttrs[ attrName ].split( /\s+/ ); | ||
1911 | classPattern = element.classes.join( '|' ); | ||
1912 | while ( ( cl = classes.pop() ) ) { | ||
1913 | if ( classPattern.indexOf( cl ) == -1 ) | ||
1914 | return false; | ||
1915 | } | ||
1916 | } else { | ||
1917 | if ( element.attributes[ attrName ] != defAttrs[ attrName ] ) | ||
1918 | return false; | ||
1919 | } | ||
1920 | } | ||
1921 | |||
1922 | for ( styleName in defStyles ) { | ||
1923 | if ( element.styles[ styleName ] != defStyles[ styleName ] ) | ||
1924 | return false; | ||
1925 | } | ||
1926 | |||
1927 | return true; | ||
1928 | } | ||
1929 | |||
1930 | // Return transformation group for content form. | ||
1931 | // One content form makes one transformation rule in one group. | ||
1932 | function getContentFormTransformationGroup( form, preferredForm ) { | ||
1933 | var element, left; | ||
1934 | |||
1935 | if ( typeof form == 'string' ) | ||
1936 | element = form; | ||
1937 | else if ( form instanceof CKEDITOR.style ) | ||
1938 | left = form; | ||
1939 | else { | ||
1940 | element = form[ 0 ]; | ||
1941 | left = form[ 1 ]; | ||
1942 | } | ||
1943 | |||
1944 | return [ { | ||
1945 | element: element, | ||
1946 | left: left, | ||
1947 | right: function( el, tools ) { | ||
1948 | tools.transform( el, preferredForm ); | ||
1949 | } | ||
1950 | } ]; | ||
1951 | } | ||
1952 | |||
1953 | // Obtain element's name from transformation rule. | ||
1954 | // It will be defined by #element, or #check or #left (styleDef.element). | ||
1955 | function getElementNameForTransformation( rule, check ) { | ||
1956 | if ( rule.element ) | ||
1957 | return rule.element; | ||
1958 | if ( check ) | ||
1959 | return check.match( /^([a-z0-9]+)/i )[ 0 ]; | ||
1960 | return rule.left.getDefinition().element; | ||
1961 | } | ||
1962 | |||
1963 | function getMatchStyleFn( style ) { | ||
1964 | return function( el ) { | ||
1965 | return elementMatchesStyle( el, style ); | ||
1966 | }; | ||
1967 | } | ||
1968 | |||
1969 | function getTransformationFn( toolName ) { | ||
1970 | return function( el, tools ) { | ||
1971 | tools[ toolName ]( el ); | ||
1972 | }; | ||
1973 | } | ||
1974 | |||
1975 | function optimizeTransformationsGroup( rules ) { | ||
1976 | var groupName, i, rule, | ||
1977 | check, left, right, | ||
1978 | optimizedRules = []; | ||
1979 | |||
1980 | for ( i = 0; i < rules.length; ++i ) { | ||
1981 | rule = rules[ i ]; | ||
1982 | |||
1983 | if ( typeof rule == 'string' ) { | ||
1984 | rule = rule.split( /\s*:\s*/ ); | ||
1985 | check = rule[ 0 ]; | ||
1986 | left = null; | ||
1987 | right = rule[ 1 ]; | ||
1988 | } else { | ||
1989 | check = rule.check; | ||
1990 | left = rule.left; | ||
1991 | right = rule.right; | ||
1992 | } | ||
1993 | |||
1994 | // Extract element name. | ||
1995 | if ( !groupName ) | ||
1996 | groupName = getElementNameForTransformation( rule, check ); | ||
1997 | |||
1998 | if ( left instanceof CKEDITOR.style ) | ||
1999 | left = getMatchStyleFn( left ); | ||
2000 | |||
2001 | optimizedRules.push( { | ||
2002 | // It doesn't make sense to test against name rule (e.g. 'table'), so don't save it. | ||
2003 | check: check == groupName ? null : check, | ||
2004 | |||
2005 | left: left, | ||
2006 | |||
2007 | // Handle shorthand format. E.g.: 'table[width]:sizeToAttribute'. | ||
2008 | right: typeof right == 'string' ? getTransformationFn( right ) : right | ||
2009 | } ); | ||
2010 | } | ||
2011 | |||
2012 | return { | ||
2013 | name: groupName, | ||
2014 | rules: optimizedRules | ||
2015 | }; | ||
2016 | } | ||
2017 | |||
2018 | /** | ||
2019 | * Singleton containing tools useful for transformation rules. | ||
2020 | * | ||
2021 | * @class CKEDITOR.filter.transformationsTools | ||
2022 | * @singleton | ||
2023 | */ | ||
2024 | var transformationsTools = CKEDITOR.filter.transformationsTools = { | ||
2025 | /** | ||
2026 | * Converts `width` and `height` attributes to styles. | ||
2027 | * | ||
2028 | * @param {CKEDITOR.htmlParser.element} element | ||
2029 | */ | ||
2030 | sizeToStyle: function( element ) { | ||
2031 | this.lengthToStyle( element, 'width' ); | ||
2032 | this.lengthToStyle( element, 'height' ); | ||
2033 | }, | ||
2034 | |||
2035 | /** | ||
2036 | * Converts `width` and `height` styles to attributes. | ||
2037 | * | ||
2038 | * @param {CKEDITOR.htmlParser.element} element | ||
2039 | */ | ||
2040 | sizeToAttribute: function( element ) { | ||
2041 | this.lengthToAttribute( element, 'width' ); | ||
2042 | this.lengthToAttribute( element, 'height' ); | ||
2043 | }, | ||
2044 | |||
2045 | /** | ||
2046 | * Converts length in the `attrName` attribute to a valid CSS length (like `width` or `height`). | ||
2047 | * | ||
2048 | * @param {CKEDITOR.htmlParser.element} element | ||
2049 | * @param {String} attrName Name of the attribute that will be converted. | ||
2050 | * @param {String} [styleName=attrName] Name of the style into which the attribute will be converted. | ||
2051 | */ | ||
2052 | lengthToStyle: function( element, attrName, styleName ) { | ||
2053 | styleName = styleName || attrName; | ||
2054 | |||
2055 | if ( !( styleName in element.styles ) ) { | ||
2056 | var value = element.attributes[ attrName ]; | ||
2057 | |||
2058 | if ( value ) { | ||
2059 | if ( ( /^\d+$/ ).test( value ) ) | ||
2060 | value += 'px'; | ||
2061 | |||
2062 | element.styles[ styleName ] = value; | ||
2063 | } | ||
2064 | } | ||
2065 | |||
2066 | delete element.attributes[ attrName ]; | ||
2067 | }, | ||
2068 | |||
2069 | /** | ||
2070 | * Converts length in the `styleName` style to a valid length attribute (like `width` or `height`). | ||
2071 | * | ||
2072 | * @param {CKEDITOR.htmlParser.element} element | ||
2073 | * @param {String} styleName Name of the style that will be converted. | ||
2074 | * @param {String} [attrName=styleName] Name of the attribute into which the style will be converted. | ||
2075 | */ | ||
2076 | lengthToAttribute: function( element, styleName, attrName ) { | ||
2077 | attrName = attrName || styleName; | ||
2078 | |||
2079 | if ( !( attrName in element.attributes ) ) { | ||
2080 | var value = element.styles[ styleName ], | ||
2081 | match = value && value.match( /^(\d+)(?:\.\d*)?px$/ ); | ||
2082 | |||
2083 | if ( match ) | ||
2084 | element.attributes[ attrName ] = match[ 1 ]; | ||
2085 | // Pass the TEST_VALUE used by filter#check when mocking element. | ||
2086 | else if ( value == TEST_VALUE ) | ||
2087 | element.attributes[ attrName ] = TEST_VALUE; | ||
2088 | } | ||
2089 | |||
2090 | delete element.styles[ styleName ]; | ||
2091 | }, | ||
2092 | |||
2093 | /** | ||
2094 | * Converts the `align` attribute to the `float` style if not set. Attribute | ||
2095 | * is always removed. | ||
2096 | * | ||
2097 | * @param {CKEDITOR.htmlParser.element} element | ||
2098 | */ | ||
2099 | alignmentToStyle: function( element ) { | ||
2100 | if ( !( 'float' in element.styles ) ) { | ||
2101 | var value = element.attributes.align; | ||
2102 | |||
2103 | if ( value == 'left' || value == 'right' ) | ||
2104 | element.styles[ 'float' ] = value; // Uh... GCC doesn't like the 'float' prop name. | ||
2105 | } | ||
2106 | |||
2107 | delete element.attributes.align; | ||
2108 | }, | ||
2109 | |||
2110 | /** | ||
2111 | * Converts the `float` style to the `align` attribute if not set. | ||
2112 | * Style is always removed. | ||
2113 | * | ||
2114 | * @param {CKEDITOR.htmlParser.element} element | ||
2115 | */ | ||
2116 | alignmentToAttribute: function( element ) { | ||
2117 | if ( !( 'align' in element.attributes ) ) { | ||
2118 | var value = element.styles[ 'float' ]; | ||
2119 | |||
2120 | if ( value == 'left' || value == 'right' ) | ||
2121 | element.attributes.align = value; | ||
2122 | } | ||
2123 | |||
2124 | delete element.styles[ 'float' ]; // Uh... GCC doesn't like the 'float' prop name. | ||
2125 | }, | ||
2126 | |||
2127 | /** | ||
2128 | * Checks whether an element matches a given {@link CKEDITOR.style}. | ||
2129 | * The element can be a "superset" of a style, e.g. it may have | ||
2130 | * more classes, but needs to have at least those defined in the style. | ||
2131 | * | ||
2132 | * @param {CKEDITOR.htmlParser.element} element | ||
2133 | * @param {CKEDITOR.style} style | ||
2134 | */ | ||
2135 | matchesStyle: elementMatchesStyle, | ||
2136 | |||
2137 | /** | ||
2138 | * Transforms element to given form. | ||
2139 | * | ||
2140 | * Form may be a: | ||
2141 | * | ||
2142 | * * {@link CKEDITOR.style}, | ||
2143 | * * string – the new name of an element. | ||
2144 | * | ||
2145 | * @param {CKEDITOR.htmlParser.element} el | ||
2146 | * @param {CKEDITOR.style/String} form | ||
2147 | */ | ||
2148 | transform: function( el, form ) { | ||
2149 | if ( typeof form == 'string' ) | ||
2150 | el.name = form; | ||
2151 | // Form is an instance of CKEDITOR.style. | ||
2152 | else { | ||
2153 | var def = form.getDefinition(), | ||
2154 | defStyles = def.styles, | ||
2155 | defAttrs = def.attributes, | ||
2156 | attrName, styleName, | ||
2157 | existingClassesPattern, defClasses, cl; | ||
2158 | |||
2159 | el.name = def.element; | ||
2160 | |||
2161 | for ( attrName in defAttrs ) { | ||
2162 | if ( attrName == 'class' ) { | ||
2163 | existingClassesPattern = el.classes.join( '|' ); | ||
2164 | defClasses = defAttrs[ attrName ].split( /\s+/ ); | ||
2165 | |||
2166 | while ( ( cl = defClasses.pop() ) ) { | ||
2167 | if ( existingClassesPattern.indexOf( cl ) == -1 ) | ||
2168 | el.classes.push( cl ); | ||
2169 | } | ||
2170 | } else { | ||
2171 | el.attributes[ attrName ] = defAttrs[ attrName ]; | ||
2172 | } | ||
2173 | |||
2174 | } | ||
2175 | |||
2176 | for ( styleName in defStyles ) { | ||
2177 | el.styles[ styleName ] = defStyles[ styleName ]; | ||
2178 | } | ||
2179 | } | ||
2180 | } | ||
2181 | }; | ||
2182 | |||
2183 | } )(); | ||
2184 | |||
2185 | /** | ||
2186 | * Allowed content rules. This setting is used when | ||
2187 | * instantiating {@link CKEDITOR.editor#filter}. | ||
2188 | * | ||
2189 | * The following values are accepted: | ||
2190 | * | ||
2191 | * * {@link CKEDITOR.filter.allowedContentRules} – defined rules will be added | ||
2192 | * to the {@link CKEDITOR.editor#filter}. | ||
2193 | * * `true` – will disable the filter (data will not be filtered, | ||
2194 | * all features will be activated). | ||
2195 | * * default – the filter will be configured by loaded features | ||
2196 | * (toolbar items, commands, etc.). | ||
2197 | * | ||
2198 | * In all cases filter configuration may be extended by | ||
2199 | * {@link CKEDITOR.config#extraAllowedContent}. This option may be especially | ||
2200 | * useful when you want to use the default `allowedContent` value | ||
2201 | * along with some additional rules. | ||
2202 | * | ||
2203 | * CKEDITOR.replace( 'textarea_id', { | ||
2204 | * allowedContent: 'p b i; a[!href]', | ||
2205 | * on: { | ||
2206 | * instanceReady: function( evt ) { | ||
2207 | * var editor = evt.editor; | ||
2208 | * | ||
2209 | * editor.filter.check( 'h1' ); // -> false | ||
2210 | * editor.setData( '<h1><i>Foo</i></h1><p class="left"><span>Bar</span> <a href="http://foo.bar">foo</a></p>' ); | ||
2211 | * // Editor contents will be: | ||
2212 | * '<p><i>Foo</i></p><p>Bar <a href="http://foo.bar">foo</a></p>' | ||
2213 | * } | ||
2214 | * } | ||
2215 | * } ); | ||
2216 | * | ||
2217 | * It is also possible to disallow some already allowed content. It is especially | ||
2218 | * useful when you want to "trim down" the content allowed by default by | ||
2219 | * editor features. To do that, use the {@link #disallowedContent} option. | ||
2220 | * | ||
2221 | * Read more in the [documentation](#!/guide/dev_acf) | ||
2222 | * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html). | ||
2223 | * | ||
2224 | * @since 4.1 | ||
2225 | * @cfg {CKEDITOR.filter.allowedContentRules/Boolean} [allowedContent=null] | ||
2226 | * @member CKEDITOR.config | ||
2227 | */ | ||
2228 | |||
2229 | /** | ||
2230 | * This option makes it possible to set additional allowed | ||
2231 | * content rules for {@link CKEDITOR.editor#filter}. | ||
2232 | * | ||
2233 | * It is especially useful in combination with the default | ||
2234 | * {@link CKEDITOR.config#allowedContent} value: | ||
2235 | * | ||
2236 | * CKEDITOR.replace( 'textarea_id', { | ||
2237 | * plugins: 'wysiwygarea,toolbar,format', | ||
2238 | * extraAllowedContent: 'b i', | ||
2239 | * on: { | ||
2240 | * instanceReady: function( evt ) { | ||
2241 | * var editor = evt.editor; | ||
2242 | * | ||
2243 | * editor.filter.check( 'h1' ); // -> true (thanks to Format combo) | ||
2244 | * editor.filter.check( 'b' ); // -> true (thanks to extraAllowedContent) | ||
2245 | * editor.setData( '<h1><i>Foo</i></h1><p class="left"><b>Bar</b> <a href="http://foo.bar">foo</a></p>' ); | ||
2246 | * // Editor contents will be: | ||
2247 | * '<h1><i>Foo</i></h1><p><b>Bar</b> foo</p>' | ||
2248 | * } | ||
2249 | * } | ||
2250 | * } ); | ||
2251 | * | ||
2252 | * Read more in the [documentation](#!/guide/dev_acf-section-automatic-mode-and-allow-additional-tags%2Fproperties) | ||
2253 | * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html). | ||
2254 | * See also {@link CKEDITOR.config#allowedContent} for more details. | ||
2255 | * | ||
2256 | * @since 4.1 | ||
2257 | * @cfg {Object/String} extraAllowedContent | ||
2258 | * @member CKEDITOR.config | ||
2259 | */ | ||
2260 | |||
2261 | /** | ||
2262 | * Disallowed content rules. They have precedence over {@link #allowedContent allowed content rules}. | ||
2263 | * Read more in the [Disallowed Content guide](#!/guide/dev_disallowed_content). | ||
2264 | * | ||
2265 | * Read more in the [documentation](#!/guide/dev_acf-section-automatic-mode-but-disallow-certain-tags%2Fproperties) | ||
2266 | * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html). | ||
2267 | * See also {@link CKEDITOR.config#allowedContent} and {@link CKEDITOR.config#extraAllowedContent}. | ||
2268 | * | ||
2269 | * @since 4.4 | ||
2270 | * @cfg {CKEDITOR.filter.disallowedContentRules} disallowedContent | ||
2271 | * @member CKEDITOR.config | ||
2272 | */ | ||
2273 | |||
2274 | /** | ||
2275 | * This event is fired when {@link CKEDITOR.filter} has stripped some | ||
2276 | * content from the data that was loaded (e.g. by {@link CKEDITOR.editor#method-setData} | ||
2277 | * method or in the source mode) or inserted (e.g. when pasting or using the | ||
2278 | * {@link CKEDITOR.editor#method-insertHtml} method). | ||
2279 | * | ||
2280 | * This event is useful when testing whether the {@link CKEDITOR.config#allowedContent} | ||
2281 | * setting is sufficient and correct for a system that is migrating to CKEditor 4.1 | ||
2282 | * (where the [Advanced Content Filter](#!/guide/dev_advanced_content_filter) was introduced). | ||
2283 | * | ||
2284 | * @since 4.1 | ||
2285 | * @event dataFiltered | ||
2286 | * @member CKEDITOR.editor | ||
2287 | * @param {CKEDITOR.editor} editor This editor instance. | ||
2288 | */ | ||
2289 | |||
2290 | /** | ||
2291 | * Virtual class which is the [Allowed Content Rules](#!/guide/dev_allowed_content_rules) formats type. | ||
2292 | * | ||
2293 | * Possible formats are: | ||
2294 | * | ||
2295 | * * the [string format](#!/guide/dev_allowed_content_rules-section-2), | ||
2296 | * * the [object format](#!/guide/dev_allowed_content_rules-section-3), | ||
2297 | * * a {@link CKEDITOR.style} instance – used mainly for integrating plugins with Advanced Content Filter, | ||
2298 | * * an array of the above formats. | ||
2299 | * | ||
2300 | * @since 4.1 | ||
2301 | * @class CKEDITOR.filter.allowedContentRules | ||
2302 | * @abstract | ||
2303 | */ | ||
2304 | |||
2305 | /** | ||
2306 | * Virtual class representing the {@link CKEDITOR.filter#disallow} argument and a type of | ||
2307 | * the {@link CKEDITOR.config#disallowedContent} option. | ||
2308 | * | ||
2309 | * This is a simplified version of the {@link CKEDITOR.filter.allowedContentRules} type. | ||
2310 | * Only the string format and object format are accepted. Required properties | ||
2311 | * are not allowed in this format. | ||
2312 | * | ||
2313 | * Read more in the [Disallowed Content guide](#!/guide/dev_disallowed_content). | ||
2314 | * | ||
2315 | * @since 4.4 | ||
2316 | * @class CKEDITOR.filter.disallowedContentRules | ||
2317 | * @abstract | ||
2318 | */ | ||
2319 | |||
2320 | /** | ||
2321 | * Virtual class representing {@link CKEDITOR.filter#check} argument. | ||
2322 | * | ||
2323 | * This is a simplified version of the {@link CKEDITOR.filter.allowedContentRules} type. | ||
2324 | * It may contain only one element and its styles, classes, and attributes. Only the | ||
2325 | * string format and a {@link CKEDITOR.style} instances are accepted. Required properties | ||
2326 | * are not allowed in this format. | ||
2327 | * | ||
2328 | * Example: | ||
2329 | * | ||
2330 | * 'img[src,alt](foo)' // Correct rule. | ||
2331 | * 'ol, ul(!foo)' // Incorrect rule. Multiple elements and required | ||
2332 | * // properties are not supported. | ||
2333 | * | ||
2334 | * @since 4.1 | ||
2335 | * @class CKEDITOR.filter.contentRule | ||
2336 | * @abstract | ||
2337 | */ | ||
2338 | |||
2339 | /** | ||
2340 | * Interface that may be automatically implemented by any | ||
2341 | * instance of any class which has at least the `name` property and | ||
2342 | * can be meant as an editor feature. | ||
2343 | * | ||
2344 | * For example: | ||
2345 | * | ||
2346 | * * "Bold" command, button, and keystroke – it does not mean exactly | ||
2347 | * `<strong>` or `<b>` but just the ability to create bold text. | ||
2348 | * * "Format" drop-down list – it also does not imply any HTML tag. | ||
2349 | * * "Link" command, button, and keystroke. | ||
2350 | * * "Image" command, button, and dialog window. | ||
2351 | * | ||
2352 | * Thus most often a feature is an instance of one of the following classes: | ||
2353 | * | ||
2354 | * * {@link CKEDITOR.command} | ||
2355 | * * {@link CKEDITOR.ui.button} | ||
2356 | * * {@link CKEDITOR.ui.richCombo} | ||
2357 | * | ||
2358 | * None of them have a `name` property explicitly defined, but | ||
2359 | * it is set by {@link CKEDITOR.editor#addCommand} and {@link CKEDITOR.ui#add}. | ||
2360 | * | ||
2361 | * During editor initialization all features that the editor should activate | ||
2362 | * should be passed to {@link CKEDITOR.editor#addFeature} (shorthand for {@link CKEDITOR.filter#addFeature}). | ||
2363 | * | ||
2364 | * This method checks if a feature can be activated (see {@link #requiredContent}) and if yes, | ||
2365 | * then it registers allowed content rules required by this feature (see {@link #allowedContent}) along | ||
2366 | * with two kinds of transformations: {@link #contentForms} and {@link #contentTransformations}. | ||
2367 | * | ||
2368 | * By default all buttons that are included in [toolbar layout configuration](#!/guide/dev_toolbar) | ||
2369 | * are checked and registered with {@link CKEDITOR.editor#addFeature}, all styles available in the | ||
2370 | * 'Format' and 'Styles' drop-down lists are checked and registered too and so on. | ||
2371 | * | ||
2372 | * @since 4.1 | ||
2373 | * @class CKEDITOR.feature | ||
2374 | * @abstract | ||
2375 | */ | ||
2376 | |||
2377 | /** | ||
2378 | * HTML code that can be generated by this feature. | ||
2379 | * | ||
2380 | * For example a basic image feature (image button displaying the image dialog window) | ||
2381 | * may allow `'img[!src,alt,width,height]'`. | ||
2382 | * | ||
2383 | * During the feature activation this value is passed to {@link CKEDITOR.filter#allow}. | ||
2384 | * | ||
2385 | * @property {CKEDITOR.filter.allowedContentRules} [allowedContent=null] | ||
2386 | */ | ||
2387 | |||
2388 | /** | ||
2389 | * Minimal HTML code that this feature must be allowed to | ||
2390 | * generate in order to work. | ||
2391 | * | ||
2392 | * For example a basic image feature (image button displaying the image dialog window) | ||
2393 | * needs `'img[src,alt]'` in order to be activated. | ||
2394 | * | ||
2395 | * During the feature validation this value is passed to {@link CKEDITOR.filter#check}. | ||
2396 | * | ||
2397 | * If this value is not provided, a feature will be always activated. | ||
2398 | * | ||
2399 | * @property {CKEDITOR.filter.contentRule} [requiredContent=null] | ||
2400 | */ | ||
2401 | |||
2402 | /** | ||
2403 | * The name of the feature. | ||
2404 | * | ||
2405 | * It is used for example to identify which {@link CKEDITOR.filter#allowedContent} | ||
2406 | * rule was added for which feature. | ||
2407 | * | ||
2408 | * @property {String} name | ||
2409 | */ | ||
2410 | |||
2411 | /** | ||
2412 | * Feature content forms to be registered in the {@link CKEDITOR.editor#filter} | ||
2413 | * during the feature activation. | ||
2414 | * | ||
2415 | * See {@link CKEDITOR.filter#addContentForms} for more details. | ||
2416 | * | ||
2417 | * @property [contentForms=null] | ||
2418 | */ | ||
2419 | |||
2420 | /** | ||
2421 | * Transformations (usually for content generated by this feature, but not necessarily) | ||
2422 | * that will be registered in the {@link CKEDITOR.editor#filter} during the feature activation. | ||
2423 | * | ||
2424 | * See {@link CKEDITOR.filter#addTransformations} for more details. | ||
2425 | * | ||
2426 | * @property [contentTransformations=null] | ||
2427 | */ | ||
2428 | |||
2429 | /** | ||
2430 | * Returns a feature that this feature needs to register. | ||
2431 | * | ||
2432 | * In some cases, during activation, one feature may need to register | ||
2433 | * another feature. For example a {@link CKEDITOR.ui.button} often registers | ||
2434 | * a related command. See {@link CKEDITOR.ui.button#toFeature}. | ||
2435 | * | ||
2436 | * This method is executed when a feature is passed to the {@link CKEDITOR.editor#addFeature}. | ||
2437 | * | ||
2438 | * @method toFeature | ||
2439 | * @returns {CKEDITOR.feature} | ||
2440 | */ | ||
diff --git a/sources/core/focusmanager.js b/sources/core/focusmanager.js new file mode 100644 index 0000000..ee1bc39 --- /dev/null +++ b/sources/core/focusmanager.js | |||
@@ -0,0 +1,273 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.focusManager} class, which is used | ||
8 | * to handle the focus in editor instances. | ||
9 | */ | ||
10 | |||
11 | ( function() { | ||
12 | /** | ||
13 | * Manages the focus activity in an editor instance. This class is to be | ||
14 | * used mainly by UI element coders when adding interface elements that need | ||
15 | * to set the focus state of the editor. | ||
16 | * | ||
17 | * var focusManager = new CKEDITOR.focusManager( editor ); | ||
18 | * focusManager.focus(); | ||
19 | * | ||
20 | * @class | ||
21 | * @constructor Creates a focusManager class instance. | ||
22 | * @param {CKEDITOR.editor} editor The editor instance. | ||
23 | */ | ||
24 | CKEDITOR.focusManager = function( editor ) { | ||
25 | if ( editor.focusManager ) | ||
26 | return editor.focusManager; | ||
27 | |||
28 | /** | ||
29 | * Indicates that the editor instance has focus. | ||
30 | * | ||
31 | * alert( CKEDITOR.instances.editor1.focusManager.hasFocus ); // e.g. true | ||
32 | */ | ||
33 | this.hasFocus = false; | ||
34 | |||
35 | /** | ||
36 | * Indicates the currently focused DOM element that makes the editor activated. | ||
37 | * | ||
38 | * @property {CKEDITOR.dom.domObject} | ||
39 | */ | ||
40 | this.currentActive = null; | ||
41 | |||
42 | /** | ||
43 | * Object used to store private stuff. | ||
44 | * | ||
45 | * @private | ||
46 | */ | ||
47 | this._ = { | ||
48 | editor: editor | ||
49 | }; | ||
50 | |||
51 | return this; | ||
52 | }; | ||
53 | |||
54 | var SLOT_NAME = 'focusmanager', | ||
55 | SLOT_NAME_LISTENERS = 'focusmanager_handlers'; | ||
56 | |||
57 | /** | ||
58 | * Object used to store private stuff. | ||
59 | * | ||
60 | * @private | ||
61 | * @class | ||
62 | * @singleton | ||
63 | */ | ||
64 | CKEDITOR.focusManager._ = { | ||
65 | /** | ||
66 | * The delay (in milliseconds) to deactivate the editor when a UI DOM element has lost focus. | ||
67 | * | ||
68 | * @private | ||
69 | * @property {Number} [blurDelay=200] | ||
70 | * @member CKEDITOR.focusManager._ | ||
71 | */ | ||
72 | blurDelay: 200 | ||
73 | }; | ||
74 | |||
75 | CKEDITOR.focusManager.prototype = { | ||
76 | |||
77 | /** | ||
78 | * Indicates that this editor instance is activated (due to a DOM focus change). | ||
79 | * The `activated` state is a symbolic indicator of an active user | ||
80 | * interaction session. | ||
81 | * | ||
82 | * **Note:** This method will not introduce UI focus | ||
83 | * impact on DOM, it is here to record the editor UI focus state internally. | ||
84 | * If you want to make the cursor blink inside the editable, use | ||
85 | * {@link CKEDITOR.editor#method-focus} instead. | ||
86 | * | ||
87 | * var editor = CKEDITOR.instances.editor1; | ||
88 | * editor.focusManage.focus( editor.editable() ); | ||
89 | * | ||
90 | * @param {CKEDITOR.dom.element} [currentActive] The new value of the {@link #currentActive} property. | ||
91 | * @member CKEDITOR.focusManager | ||
92 | */ | ||
93 | focus: function( currentActive ) { | ||
94 | if ( this._.timer ) | ||
95 | clearTimeout( this._.timer ); | ||
96 | |||
97 | if ( currentActive ) | ||
98 | this.currentActive = currentActive; | ||
99 | |||
100 | if ( !( this.hasFocus || this._.locked ) ) { | ||
101 | // If another editor has the current focus, we first "blur" it. In | ||
102 | // this way the events happen in a more logical sequence, like: | ||
103 | // "focus 1" > "blur 1" > "focus 2" | ||
104 | // ... instead of: | ||
105 | // "focus 1" > "focus 2" > "blur 1" | ||
106 | var current = CKEDITOR.currentInstance; | ||
107 | current && current.focusManager.blur( 1 ); | ||
108 | |||
109 | this.hasFocus = true; | ||
110 | |||
111 | var ct = this._.editor.container; | ||
112 | ct && ct.addClass( 'cke_focus' ); | ||
113 | this._.editor.fire( 'focus' ); | ||
114 | } | ||
115 | }, | ||
116 | |||
117 | /** | ||
118 | * Prevents from changing the focus manager state until the next {@link #unlock} is called. | ||
119 | * | ||
120 | * @member CKEDITOR.focusManager | ||
121 | */ | ||
122 | lock: function() { | ||
123 | this._.locked = 1; | ||
124 | }, | ||
125 | |||
126 | /** | ||
127 | * Restores the automatic focus management if {@link #lock} is called. | ||
128 | * | ||
129 | * @member CKEDITOR.focusManager | ||
130 | */ | ||
131 | unlock: function() { | ||
132 | delete this._.locked; | ||
133 | }, | ||
134 | |||
135 | /** | ||
136 | * Used to indicate that the editor instance has been deactivated by the specified | ||
137 | * element which has just lost focus. | ||
138 | * | ||
139 | * **Note:** This function acts asynchronously with a delay of 100ms to | ||
140 | * avoid temporary deactivation. Use the `noDelay` parameter instead | ||
141 | * to deactivate immediately. | ||
142 | * | ||
143 | * var editor = CKEDITOR.instances.editor1; | ||
144 | * editor.focusManager.blur(); | ||
145 | * | ||
146 | * @param {Boolean} [noDelay=false] Immediately deactivate the editor instance synchronously. | ||
147 | * @member CKEDITOR.focusManager | ||
148 | */ | ||
149 | blur: function( noDelay ) { | ||
150 | if ( this._.locked ) | ||
151 | return; | ||
152 | |||
153 | function doBlur() { | ||
154 | if ( this.hasFocus ) { | ||
155 | this.hasFocus = false; | ||
156 | |||
157 | var ct = this._.editor.container; | ||
158 | ct && ct.removeClass( 'cke_focus' ); | ||
159 | this._.editor.fire( 'blur' ); | ||
160 | } | ||
161 | } | ||
162 | |||
163 | if ( this._.timer ) | ||
164 | clearTimeout( this._.timer ); | ||
165 | |||
166 | var delay = CKEDITOR.focusManager._.blurDelay; | ||
167 | if ( noDelay || !delay ) | ||
168 | doBlur.call( this ); | ||
169 | else { | ||
170 | this._.timer = CKEDITOR.tools.setTimeout( function() { | ||
171 | delete this._.timer; | ||
172 | doBlur.call( this ); | ||
173 | }, delay, this ); | ||
174 | } | ||
175 | }, | ||
176 | |||
177 | /** | ||
178 | * Registers a UI DOM element to the focus manager, which will make the focus manager "hasFocus" | ||
179 | * once the input focus is relieved on the element. | ||
180 | * This method is designed to be used by plugins to expand the jurisdiction of the editor focus. | ||
181 | * | ||
182 | * @param {CKEDITOR.dom.element} element The container (topmost) element of one UI part. | ||
183 | * @param {Boolean} isCapture If specified, {@link CKEDITOR.event#useCapture} will be used when listening to the focus event. | ||
184 | * @member CKEDITOR.focusManager | ||
185 | */ | ||
186 | add: function( element, isCapture ) { | ||
187 | var fm = element.getCustomData( SLOT_NAME ); | ||
188 | if ( !fm || fm != this ) { | ||
189 | // If this element is already taken by another instance, dismiss it first. | ||
190 | fm && fm.remove( element ); | ||
191 | |||
192 | var focusEvent = 'focus', | ||
193 | blurEvent = 'blur'; | ||
194 | |||
195 | // Bypass the element's internal DOM focus change. | ||
196 | if ( isCapture ) { | ||
197 | |||
198 | // Use "focusin/focusout" events instead of capture phase in IEs, | ||
199 | // which fires synchronously. | ||
200 | if ( CKEDITOR.env.ie ) { | ||
201 | focusEvent = 'focusin'; | ||
202 | blurEvent = 'focusout'; | ||
203 | } else { | ||
204 | CKEDITOR.event.useCapture = 1; | ||
205 | } | ||
206 | } | ||
207 | |||
208 | var listeners = { | ||
209 | blur: function() { | ||
210 | if ( element.equals( this.currentActive ) ) | ||
211 | this.blur(); | ||
212 | }, | ||
213 | focus: function() { | ||
214 | this.focus( element ); | ||
215 | } | ||
216 | }; | ||
217 | |||
218 | element.on( focusEvent, listeners.focus, this ); | ||
219 | element.on( blurEvent, listeners.blur, this ); | ||
220 | |||
221 | if ( isCapture ) | ||
222 | CKEDITOR.event.useCapture = 0; | ||
223 | |||
224 | element.setCustomData( SLOT_NAME, this ); | ||
225 | element.setCustomData( SLOT_NAME_LISTENERS, listeners ); | ||
226 | } | ||
227 | }, | ||
228 | |||
229 | /** | ||
230 | * Dismisses an element from the focus manager delegations added by {@link #add}. | ||
231 | * | ||
232 | * @param {CKEDITOR.dom.element} element The element to be removed from the focus manager. | ||
233 | * @member CKEDITOR.focusManager | ||
234 | */ | ||
235 | remove: function( element ) { | ||
236 | element.removeCustomData( SLOT_NAME ); | ||
237 | var listeners = element.removeCustomData( SLOT_NAME_LISTENERS ); | ||
238 | element.removeListener( 'blur', listeners.blur ); | ||
239 | element.removeListener( 'focus', listeners.focus ); | ||
240 | } | ||
241 | |||
242 | }; | ||
243 | |||
244 | } )(); | ||
245 | |||
246 | /** | ||
247 | * Fired when the editor instance receives the input focus. | ||
248 | * | ||
249 | * editor.on( 'focus', function( e ) { | ||
250 | * alert( 'The editor named ' + e.editor.name + ' is now focused' ); | ||
251 | * } ); | ||
252 | * | ||
253 | * @event focus | ||
254 | * @member CKEDITOR.editor | ||
255 | * @param {CKEDITOR.editor} editor The editor instance. | ||
256 | */ | ||
257 | |||
258 | /** | ||
259 | * Fired when the editor instance loses the input focus. | ||
260 | * | ||
261 | * **Note:** This event will **NOT** be triggered when focus is moved internally, e.g. from | ||
262 | * an editable to another part of the editor UI like a dialog window. | ||
263 | * If you are interested only in the focus state of the editable, listen to the `focus` | ||
264 | * and `blur` events of the {@link CKEDITOR.editable} instead. | ||
265 | * | ||
266 | * editor.on( 'blur', function( e ) { | ||
267 | * alert( 'The editor named ' + e.editor.name + ' lost the focus' ); | ||
268 | * } ); | ||
269 | * | ||
270 | * @event blur | ||
271 | * @member CKEDITOR.editor | ||
272 | * @param {CKEDITOR.editor} editor The editor instance. | ||
273 | */ | ||
diff --git a/sources/core/htmldataprocessor.js b/sources/core/htmldataprocessor.js new file mode 100644 index 0000000..d079e4d --- /dev/null +++ b/sources/core/htmldataprocessor.js | |||
@@ -0,0 +1,1036 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | ( function() { | ||
7 | /** | ||
8 | * Represents an HTML data processor, which is responsible for translating and | ||
9 | * transforming the editor data on input and output. | ||
10 | * | ||
11 | * @class | ||
12 | * @extends CKEDITOR.dataProcessor | ||
13 | * @constructor Creates an htmlDataProcessor class instance. | ||
14 | * @param {CKEDITOR.editor} editor | ||
15 | */ | ||
16 | CKEDITOR.htmlDataProcessor = function( editor ) { | ||
17 | var dataFilter, htmlFilter, | ||
18 | that = this; | ||
19 | |||
20 | this.editor = editor; | ||
21 | |||
22 | /** | ||
23 | * Data filter used when processing input by {@link #toHtml}. | ||
24 | * | ||
25 | * @property {CKEDITOR.htmlParser.filter} | ||
26 | */ | ||
27 | this.dataFilter = dataFilter = new CKEDITOR.htmlParser.filter(); | ||
28 | |||
29 | /** | ||
30 | * HTML filter used when processing output by {@link #toDataFormat}. | ||
31 | * | ||
32 | * @property {CKEDITOR.htmlParser.filter} | ||
33 | */ | ||
34 | this.htmlFilter = htmlFilter = new CKEDITOR.htmlParser.filter(); | ||
35 | |||
36 | /** | ||
37 | * The HTML writer used by this data processor to format the output. | ||
38 | * | ||
39 | * @property {CKEDITOR.htmlParser.basicWriter} | ||
40 | */ | ||
41 | this.writer = new CKEDITOR.htmlParser.basicWriter(); | ||
42 | |||
43 | dataFilter.addRules( defaultDataFilterRulesEditableOnly ); | ||
44 | dataFilter.addRules( defaultDataFilterRulesForAll, { applyToAll: true } ); | ||
45 | dataFilter.addRules( createBogusAndFillerRules( editor, 'data' ), { applyToAll: true } ); | ||
46 | htmlFilter.addRules( defaultHtmlFilterRulesEditableOnly ); | ||
47 | htmlFilter.addRules( defaultHtmlFilterRulesForAll, { applyToAll: true } ); | ||
48 | htmlFilter.addRules( createBogusAndFillerRules( editor, 'html' ), { applyToAll: true } ); | ||
49 | |||
50 | editor.on( 'toHtml', function( evt ) { | ||
51 | var evtData = evt.data, | ||
52 | data = evtData.dataValue, | ||
53 | fixBodyTag; | ||
54 | |||
55 | // The source data is already HTML, but we need to clean | ||
56 | // it up and apply the filter. | ||
57 | data = protectSource( data, editor ); | ||
58 | |||
59 | // Protect content of textareas. (#9995) | ||
60 | // Do this before protecting attributes to avoid breaking: | ||
61 | // <textarea><img src="..." /></textarea> | ||
62 | data = protectElements( data, protectTextareaRegex ); | ||
63 | |||
64 | // Before anything, we must protect the URL attributes as the | ||
65 | // browser may changing them when setting the innerHTML later in | ||
66 | // the code. | ||
67 | data = protectAttributes( data ); | ||
68 | |||
69 | // Protect elements than can't be set inside a DIV. E.g. IE removes | ||
70 | // style tags from innerHTML. (#3710) | ||
71 | data = protectElements( data, protectElementsRegex ); | ||
72 | |||
73 | // Certain elements has problem to go through DOM operation, protect | ||
74 | // them by prefixing 'cke' namespace. (#3591) | ||
75 | data = protectElementsNames( data ); | ||
76 | |||
77 | // All none-IE browsers ignore self-closed custom elements, | ||
78 | // protecting them into open-close. (#3591) | ||
79 | data = protectSelfClosingElements( data ); | ||
80 | |||
81 | // Compensate one leading line break after <pre> open as browsers | ||
82 | // eat it up. (#5789) | ||
83 | data = protectPreFormatted( data ); | ||
84 | |||
85 | // There are attributes which may execute JavaScript code inside fixBin. | ||
86 | // Encode them greedily. They will be unprotected right after getting HTML from fixBin. (#10) | ||
87 | data = protectInsecureAttributes( data ); | ||
88 | |||
89 | var fixBin = evtData.context || editor.editable().getName(), | ||
90 | isPre; | ||
91 | |||
92 | // Old IEs loose formats when load html into <pre>. | ||
93 | if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && fixBin == 'pre' ) { | ||
94 | fixBin = 'div'; | ||
95 | data = '<pre>' + data + '</pre>'; | ||
96 | isPre = 1; | ||
97 | } | ||
98 | |||
99 | // Call the browser to help us fixing a possibly invalid HTML | ||
100 | // structure. | ||
101 | var el = editor.document.createElement( fixBin ); | ||
102 | // Add fake character to workaround IE comments bug. (#3801) | ||
103 | el.setHtml( 'a' + data ); | ||
104 | data = el.getHtml().substr( 1 ); | ||
105 | |||
106 | // Restore shortly protected attribute names. | ||
107 | data = data.replace( new RegExp( 'data-cke-' + CKEDITOR.rnd + '-', 'ig' ), '' ); | ||
108 | |||
109 | isPre && ( data = data.replace( /^<pre>|<\/pre>$/gi, '' ) ); | ||
110 | |||
111 | // Unprotect "some" of the protected elements at this point. | ||
112 | data = unprotectElementNames( data ); | ||
113 | |||
114 | data = unprotectElements( data ); | ||
115 | |||
116 | // Restore the comments that have been protected, in this way they | ||
117 | // can be properly filtered. | ||
118 | data = unprotectRealComments( data ); | ||
119 | |||
120 | if ( evtData.fixForBody === false ) { | ||
121 | fixBodyTag = false; | ||
122 | } else { | ||
123 | fixBodyTag = getFixBodyTag( evtData.enterMode, editor.config.autoParagraph ); | ||
124 | } | ||
125 | |||
126 | // Now use our parser to make further fixes to the structure, as | ||
127 | // well as apply the filter. | ||
128 | data = CKEDITOR.htmlParser.fragment.fromHtml( data, evtData.context, fixBodyTag ); | ||
129 | |||
130 | // The empty root element needs to be fixed by adding 'p' or 'div' into it. | ||
131 | // This avoids the need to create that element on the first focus (#12630). | ||
132 | if ( fixBodyTag ) { | ||
133 | fixEmptyRoot( data, fixBodyTag ); | ||
134 | } | ||
135 | |||
136 | evtData.dataValue = data; | ||
137 | }, null, null, 5 ); | ||
138 | |||
139 | // Filter incoming "data". | ||
140 | // Add element filter before htmlDataProcessor.dataFilter when purifying input data to correct html. | ||
141 | editor.on( 'toHtml', function( evt ) { | ||
142 | if ( evt.data.filter.applyTo( evt.data.dataValue, true, evt.data.dontFilter, evt.data.enterMode ) ) | ||
143 | editor.fire( 'dataFiltered' ); | ||
144 | }, null, null, 6 ); | ||
145 | |||
146 | editor.on( 'toHtml', function( evt ) { | ||
147 | evt.data.dataValue.filterChildren( that.dataFilter, true ); | ||
148 | }, null, null, 10 ); | ||
149 | |||
150 | editor.on( 'toHtml', function( evt ) { | ||
151 | var evtData = evt.data, | ||
152 | data = evtData.dataValue, | ||
153 | writer = new CKEDITOR.htmlParser.basicWriter(); | ||
154 | |||
155 | data.writeChildrenHtml( writer ); | ||
156 | data = writer.getHtml( true ); | ||
157 | |||
158 | // Protect the real comments again. | ||
159 | evtData.dataValue = protectRealComments( data ); | ||
160 | }, null, null, 15 ); | ||
161 | |||
162 | |||
163 | editor.on( 'toDataFormat', function( evt ) { | ||
164 | var data = evt.data.dataValue; | ||
165 | |||
166 | // #10854 - we need to strip leading blockless <br> which FF adds | ||
167 | // automatically when editable contains only non-editable content. | ||
168 | // We do that for every browser (so it's a constant behavior) and | ||
169 | // not in BR mode, in which chance of valid leading blockless <br> is higher. | ||
170 | if ( evt.data.enterMode != CKEDITOR.ENTER_BR ) | ||
171 | data = data.replace( /^<br *\/?>/i, '' ); | ||
172 | |||
173 | evt.data.dataValue = CKEDITOR.htmlParser.fragment.fromHtml( | ||
174 | data, evt.data.context, getFixBodyTag( evt.data.enterMode, editor.config.autoParagraph ) ); | ||
175 | }, null, null, 5 ); | ||
176 | |||
177 | editor.on( 'toDataFormat', function( evt ) { | ||
178 | evt.data.dataValue.filterChildren( that.htmlFilter, true ); | ||
179 | }, null, null, 10 ); | ||
180 | |||
181 | // Transform outcoming "data". | ||
182 | // Add element filter after htmlDataProcessor.htmlFilter when preparing output data HTML. | ||
183 | editor.on( 'toDataFormat', function( evt ) { | ||
184 | evt.data.filter.applyTo( evt.data.dataValue, false, true ); | ||
185 | }, null, null, 11 ); | ||
186 | |||
187 | editor.on( 'toDataFormat', function( evt ) { | ||
188 | var data = evt.data.dataValue, | ||
189 | writer = that.writer; | ||
190 | |||
191 | writer.reset(); | ||
192 | data.writeChildrenHtml( writer ); | ||
193 | data = writer.getHtml( true ); | ||
194 | |||
195 | // Restore those non-HTML protected source. (#4475,#4880) | ||
196 | data = unprotectRealComments( data ); | ||
197 | data = unprotectSource( data, editor ); | ||
198 | |||
199 | evt.data.dataValue = data; | ||
200 | }, null, null, 15 ); | ||
201 | }; | ||
202 | |||
203 | CKEDITOR.htmlDataProcessor.prototype = { | ||
204 | /** | ||
205 | * Processes the (potentially malformed) input HTML to a purified form which | ||
206 | * is suitable for using in the WYSIWYG editable. | ||
207 | * | ||
208 | * This method fires the {@link CKEDITOR.editor#toHtml} event which makes it possible | ||
209 | * to hook into the process at various stages. | ||
210 | * | ||
211 | * **Note:** Since CKEditor 4.3 the signature of this method changed and all options | ||
212 | * are now grouped in one `options` object. Previously `context`, `fixForBody` and `dontFilter` | ||
213 | * were passed separately. | ||
214 | * | ||
215 | * @param {String} data The raw data. | ||
216 | * @param {Object} [options] The options object. | ||
217 | * @param {String} [options.context] The tag name of a context element within which | ||
218 | * the input is to be processed, defaults to the editable element. | ||
219 | * If `null` is passed, then data will be parsed without context (as children of {@link CKEDITOR.htmlParser.fragment}). | ||
220 | * See {@link CKEDITOR.htmlParser.fragment#fromHtml} for more details. | ||
221 | * @param {Boolean} [options.fixForBody=true] Whether to trigger the auto paragraph for non-block content. | ||
222 | * @param {CKEDITOR.filter} [options.filter] When specified, instead of using the {@link CKEDITOR.editor#filter main filter}, | ||
223 | * the passed instance will be used to filter the content. | ||
224 | * @param {Boolean} [options.dontFilter] Do not filter data with {@link CKEDITOR.filter} (note: transformations | ||
225 | * will still be applied). | ||
226 | * @param {Number} [options.enterMode] When specified, it will be used instead of the {@link CKEDITOR.editor#enterMode main enterMode}. | ||
227 | * @param {Boolean} [options.protectedWhitespaces] Indicates that content was wrapped with `<span>` elements to preserve | ||
228 | * leading and trailing whitespaces. Option used by the {@link CKEDITOR.editor#method-insertHtml} method. | ||
229 | * @returns {String} | ||
230 | */ | ||
231 | toHtml: function( data, options, fixForBody, dontFilter ) { | ||
232 | var editor = this.editor, | ||
233 | context, filter, enterMode, protectedWhitespaces; | ||
234 | |||
235 | // Typeof null == 'object', so check truthiness of options too. | ||
236 | if ( options && typeof options == 'object' ) { | ||
237 | context = options.context; | ||
238 | fixForBody = options.fixForBody; | ||
239 | dontFilter = options.dontFilter; | ||
240 | filter = options.filter; | ||
241 | enterMode = options.enterMode; | ||
242 | protectedWhitespaces = options.protectedWhitespaces; | ||
243 | } | ||
244 | // Backward compatibility. Since CKEDITOR 4.3 every option was a separate argument. | ||
245 | else { | ||
246 | context = options; | ||
247 | } | ||
248 | |||
249 | // Fall back to the editable as context if not specified. | ||
250 | if ( !context && context !== null ) | ||
251 | context = editor.editable().getName(); | ||
252 | |||
253 | return editor.fire( 'toHtml', { | ||
254 | dataValue: data, | ||
255 | context: context, | ||
256 | fixForBody: fixForBody, | ||
257 | dontFilter: dontFilter, | ||
258 | filter: filter || editor.filter, | ||
259 | enterMode: enterMode || editor.enterMode, | ||
260 | protectedWhitespaces: protectedWhitespaces | ||
261 | } ).dataValue; | ||
262 | }, | ||
263 | |||
264 | /** | ||
265 | * See {@link CKEDITOR.dataProcessor#toDataFormat}. | ||
266 | * | ||
267 | * This method fires the {@link CKEDITOR.editor#toDataFormat} event which makes it possible | ||
268 | * to hook into the process at various stages. | ||
269 | * | ||
270 | * @param {String} html | ||
271 | * @param {Object} [options] The options object. | ||
272 | * @param {String} [options.context] The tag name of the context element within which | ||
273 | * the input is to be processed, defaults to the editable element. | ||
274 | * @param {CKEDITOR.filter} [options.filter] When specified, instead of using the {@link CKEDITOR.editor#filter main filter}, | ||
275 | * the passed instance will be used to apply content transformations to the content. | ||
276 | * @param {Number} [options.enterMode] When specified, it will be used instead of the {@link CKEDITOR.editor#enterMode main enterMode}. | ||
277 | * @returns {String} | ||
278 | */ | ||
279 | toDataFormat: function( html, options ) { | ||
280 | var context, filter, enterMode; | ||
281 | |||
282 | // Do not shorten this to `options && options.xxx`, because | ||
283 | // falsy `options` will be passed instead of undefined. | ||
284 | if ( options ) { | ||
285 | context = options.context; | ||
286 | filter = options.filter; | ||
287 | enterMode = options.enterMode; | ||
288 | } | ||
289 | |||
290 | // Fall back to the editable as context if not specified. | ||
291 | if ( !context && context !== null ) | ||
292 | context = this.editor.editable().getName(); | ||
293 | |||
294 | return this.editor.fire( 'toDataFormat', { | ||
295 | dataValue: html, | ||
296 | filter: filter || this.editor.filter, | ||
297 | context: context, | ||
298 | enterMode: enterMode || this.editor.enterMode | ||
299 | } ).dataValue; | ||
300 | } | ||
301 | }; | ||
302 | |||
303 | // Produce a set of filtering rules that handles bogus and filler node at the | ||
304 | // end of block/pseudo block, in the following consequence: | ||
305 | // 1. elements:<block> - this filter removes any bogus node, then check | ||
306 | // if it's an empty block that requires a filler. | ||
307 | // 2. elements:<br> - After cleaned with bogus, this filter checks the real | ||
308 | // line-break BR to compensate a filler after it. | ||
309 | // | ||
310 | // Terms definitions: | ||
311 | // filler: An element that's either <BR> or &NBSP; at the end of block that established line height. | ||
312 | // bogus: Whenever a filler is proceeded with inline content, it becomes a bogus which is subjected to be removed. | ||
313 | // | ||
314 | // Various forms of the filler: | ||
315 | // In output HTML: Filler should be consistently &NBSP; <BR> at the end of block is always considered as bogus. | ||
316 | // In Wysiwyg HTML: Browser dependent - see env.needsBrFiller. Either BR for when needsBrFiller is true, or &NBSP; otherwise. | ||
317 | // <BR> is NEVER considered as bogus when needsBrFiller is true. | ||
318 | function createBogusAndFillerRules( editor, type ) { | ||
319 | function createFiller( isOutput ) { | ||
320 | return isOutput || CKEDITOR.env.needsNbspFiller ? | ||
321 | new CKEDITOR.htmlParser.text( '\xa0' ) : | ||
322 | new CKEDITOR.htmlParser.element( 'br', { 'data-cke-bogus': 1 } ); | ||
323 | } | ||
324 | |||
325 | // This text block filter, remove any bogus and create the filler on demand. | ||
326 | function blockFilter( isOutput, fillEmptyBlock ) { | ||
327 | |||
328 | return function( block ) { | ||
329 | // DO NOT apply the filler if it's a fragment node. | ||
330 | if ( block.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) | ||
331 | return; | ||
332 | |||
333 | cleanBogus( block ); | ||
334 | |||
335 | // Add fillers to input (always) and to output (if fillEmptyBlock is ok with that). | ||
336 | var shouldFillBlock = !isOutput || | ||
337 | ( typeof fillEmptyBlock == 'function' ? fillEmptyBlock( block ) : fillEmptyBlock ) !== false; | ||
338 | |||
339 | if ( shouldFillBlock && isEmptyBlockNeedFiller( block ) ) { | ||
340 | block.add( createFiller( isOutput ) ); | ||
341 | } | ||
342 | }; | ||
343 | } | ||
344 | |||
345 | // Append a filler right after the last line-break BR, found at the end of block. | ||
346 | function brFilter( isOutput ) { | ||
347 | return function( br ) { | ||
348 | // DO NOT apply the filer if parent's a fragment node. | ||
349 | if ( br.parent.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) | ||
350 | return; | ||
351 | |||
352 | var attrs = br.attributes; | ||
353 | // Dismiss BRs that are either bogus or eol marker. | ||
354 | if ( 'data-cke-bogus' in attrs || 'data-cke-eol' in attrs ) { | ||
355 | delete attrs [ 'data-cke-bogus' ]; | ||
356 | return; | ||
357 | } | ||
358 | |||
359 | // Judge the tail line-break BR, and to insert bogus after it. | ||
360 | var next = getNext( br ), previous = getPrevious( br ); | ||
361 | |||
362 | if ( !next && isBlockBoundary( br.parent ) ) | ||
363 | append( br.parent, createFiller( isOutput ) ); | ||
364 | else if ( isBlockBoundary( next ) && previous && !isBlockBoundary( previous ) ) | ||
365 | createFiller( isOutput ).insertBefore( next ); | ||
366 | }; | ||
367 | } | ||
368 | |||
369 | // Determinate whether this node is potentially a bogus node. | ||
370 | function maybeBogus( node, atBlockEnd ) { | ||
371 | |||
372 | // BR that's not from IE<11 DOM, except for a EOL marker. | ||
373 | if ( !( isOutput && !CKEDITOR.env.needsBrFiller ) && | ||
374 | node.type == CKEDITOR.NODE_ELEMENT && node.name == 'br' && | ||
375 | !node.attributes[ 'data-cke-eol' ] ) { | ||
376 | return true; | ||
377 | } | ||
378 | |||
379 | var match; | ||
380 | |||
381 | // NBSP, possibly. | ||
382 | if ( node.type == CKEDITOR.NODE_TEXT && ( match = node.value.match( tailNbspRegex ) ) ) { | ||
383 | // We need to separate tail NBSP out of a text node, for later removal. | ||
384 | if ( match.index ) { | ||
385 | ( new CKEDITOR.htmlParser.text( node.value.substring( 0, match.index ) ) ).insertBefore( node ); | ||
386 | node.value = match[ 0 ]; | ||
387 | } | ||
388 | |||
389 | // From IE<11 DOM, at the end of a text block, or before block boundary. | ||
390 | if ( !CKEDITOR.env.needsBrFiller && isOutput && ( !atBlockEnd || node.parent.name in textBlockTags ) ) | ||
391 | return true; | ||
392 | |||
393 | // From the output. | ||
394 | if ( !isOutput ) { | ||
395 | var previous = node.previous; | ||
396 | |||
397 | // Following a line-break at the end of block. | ||
398 | if ( previous && previous.name == 'br' ) | ||
399 | return true; | ||
400 | |||
401 | // Or a single NBSP between two blocks. | ||
402 | if ( !previous || isBlockBoundary( previous ) ) | ||
403 | return true; | ||
404 | } | ||
405 | } | ||
406 | |||
407 | return false; | ||
408 | } | ||
409 | |||
410 | // Removes all bogus inside of this block, and to convert fillers into the proper form. | ||
411 | function cleanBogus( block ) { | ||
412 | var bogus = []; | ||
413 | var last = getLast( block ), node, previous; | ||
414 | |||
415 | if ( last ) { | ||
416 | // Check for bogus at the end of this block. | ||
417 | // e.g. <p>foo<br /></p> | ||
418 | maybeBogus( last, 1 ) && bogus.push( last ); | ||
419 | |||
420 | while ( last ) { | ||
421 | // Check for bogus at the end of any pseudo block contained. | ||
422 | if ( isBlockBoundary( last ) && ( node = getPrevious( last ) ) && maybeBogus( node ) ) { | ||
423 | // Bogus must have inline proceeding, instead single BR between two blocks, | ||
424 | // is considered as filler, e.g. <hr /><br /><hr /> | ||
425 | if ( ( previous = getPrevious( node ) ) && !isBlockBoundary( previous ) ) | ||
426 | bogus.push( node ); | ||
427 | // Convert the filler into appropriate form. | ||
428 | else { | ||
429 | createFiller( isOutput ).insertAfter( node ); | ||
430 | node.remove(); | ||
431 | } | ||
432 | } | ||
433 | |||
434 | last = last.previous; | ||
435 | } | ||
436 | } | ||
437 | |||
438 | // Now remove all bogus collected from above. | ||
439 | for ( var i = 0 ; i < bogus.length ; i++ ) | ||
440 | bogus[ i ].remove(); | ||
441 | } | ||
442 | |||
443 | // Judge whether it's an empty block that requires a filler node. | ||
444 | function isEmptyBlockNeedFiller( block ) { | ||
445 | |||
446 | // DO NOT fill empty editable in IE<11. | ||
447 | if ( !isOutput && !CKEDITOR.env.needsBrFiller && block.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) | ||
448 | return false; | ||
449 | |||
450 | // 1. For IE version >=8, empty blocks are displayed correctly themself in wysiwiyg; | ||
451 | // 2. For the rest, at least table cell and list item need no filler space. (#6248) | ||
452 | if ( !isOutput && !CKEDITOR.env.needsBrFiller && | ||
453 | ( document.documentMode > 7 || | ||
454 | block.name in CKEDITOR.dtd.tr || | ||
455 | block.name in CKEDITOR.dtd.$listItem ) ) { | ||
456 | return false; | ||
457 | } | ||
458 | |||
459 | var last = getLast( block ); | ||
460 | return !last || block.name == 'form' && last.name == 'input' ; | ||
461 | } | ||
462 | |||
463 | var rules = { elements: {} }, | ||
464 | isOutput = type == 'html', | ||
465 | textBlockTags = CKEDITOR.tools.extend( {}, blockLikeTags ); | ||
466 | |||
467 | // Build the list of text blocks. | ||
468 | for ( var i in textBlockTags ) { | ||
469 | if ( !( '#' in dtd[ i ] ) ) | ||
470 | delete textBlockTags[ i ]; | ||
471 | } | ||
472 | |||
473 | for ( i in textBlockTags ) | ||
474 | rules.elements[ i ] = blockFilter( isOutput, editor.config.fillEmptyBlocks ); | ||
475 | |||
476 | // Editable element has to be checked separately. | ||
477 | rules.root = blockFilter( isOutput, false ); | ||
478 | rules.elements.br = brFilter( isOutput ); | ||
479 | return rules; | ||
480 | } | ||
481 | |||
482 | function getFixBodyTag( enterMode, autoParagraph ) { | ||
483 | return ( enterMode != CKEDITOR.ENTER_BR && autoParagraph !== false ) ? enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' : false; | ||
484 | } | ||
485 | |||
486 | // Regex to scan for at the end of blocks, which are actually placeholders. | ||
487 | // Safari transforms the to \xa0. (#4172) | ||
488 | var tailNbspRegex = /(?: |\xa0)$/; | ||
489 | |||
490 | var protectedSourceMarker = '{cke_protected}'; | ||
491 | |||
492 | function getLast( node ) { | ||
493 | var last = node.children[ node.children.length - 1 ]; | ||
494 | while ( last && isEmpty( last ) ) | ||
495 | last = last.previous; | ||
496 | return last; | ||
497 | } | ||
498 | |||
499 | function getNext( node ) { | ||
500 | var next = node.next; | ||
501 | while ( next && isEmpty( next ) ) | ||
502 | next = next.next; | ||
503 | return next; | ||
504 | } | ||
505 | |||
506 | function getPrevious( node ) { | ||
507 | var previous = node.previous; | ||
508 | while ( previous && isEmpty( previous ) ) | ||
509 | previous = previous.previous; | ||
510 | return previous; | ||
511 | } | ||
512 | |||
513 | // Judge whether the node is an ghost node to be ignored, when traversing. | ||
514 | function isEmpty( node ) { | ||
515 | return node.type == CKEDITOR.NODE_TEXT && | ||
516 | !CKEDITOR.tools.trim( node.value ) || | ||
517 | node.type == CKEDITOR.NODE_ELEMENT && | ||
518 | node.attributes[ 'data-cke-bookmark' ]; | ||
519 | } | ||
520 | |||
521 | // Judge whether the node is a block-like element. | ||
522 | function isBlockBoundary( node ) { | ||
523 | return node && | ||
524 | ( node.type == CKEDITOR.NODE_ELEMENT && node.name in blockLikeTags || | ||
525 | node.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ); | ||
526 | } | ||
527 | |||
528 | function append( parent, node ) { | ||
529 | var last = parent.children[ parent.children.length - 1 ]; | ||
530 | parent.children.push( node ); | ||
531 | node.parent = parent; | ||
532 | if ( last ) { | ||
533 | last.next = node; | ||
534 | node.previous = last; | ||
535 | } | ||
536 | } | ||
537 | |||
538 | function getNodeIndex( node ) { | ||
539 | return node.parent ? node.getIndex() : -1; | ||
540 | } | ||
541 | |||
542 | var dtd = CKEDITOR.dtd, | ||
543 | // Define orders of table elements. | ||
544 | tableOrder = [ 'caption', 'colgroup', 'col', 'thead', 'tfoot', 'tbody' ], | ||
545 | // List of all block elements. | ||
546 | blockLikeTags = CKEDITOR.tools.extend( {}, dtd.$blockLimit, dtd.$block ); | ||
547 | |||
548 | // | ||
549 | // DATA filter rules ------------------------------------------------------ | ||
550 | // | ||
551 | |||
552 | var defaultDataFilterRulesEditableOnly = { | ||
553 | elements: { | ||
554 | input: protectReadOnly, | ||
555 | textarea: protectReadOnly | ||
556 | } | ||
557 | }; | ||
558 | |||
559 | // These rules will also be applied to non-editable content. | ||
560 | var defaultDataFilterRulesForAll = { | ||
561 | attributeNames: [ | ||
562 | // Event attributes (onXYZ) must not be directly set. They can become | ||
563 | // active in the editing area (IE|WebKit). | ||
564 | [ ( /^on/ ), 'data-cke-pa-on' ], | ||
565 | |||
566 | // Don't let some old expando enter editor. Concerns only IE8, | ||
567 | // but for consistency remove on all browsers. | ||
568 | [ ( /^data-cke-expando$/ ), '' ] | ||
569 | ] | ||
570 | }; | ||
571 | |||
572 | // Disable form elements editing mode provided by some browsers. (#5746) | ||
573 | function protectReadOnly( element ) { | ||
574 | var attrs = element.attributes; | ||
575 | |||
576 | // We should flag that the element was locked by our code so | ||
577 | // it'll be editable by the editor functions (#6046). | ||
578 | if ( attrs.contenteditable != 'false' ) | ||
579 | attrs[ 'data-cke-editable' ] = attrs.contenteditable ? 'true' : 1; | ||
580 | |||
581 | attrs.contenteditable = 'false'; | ||
582 | } | ||
583 | |||
584 | // | ||
585 | // HTML filter rules ------------------------------------------------------ | ||
586 | // | ||
587 | |||
588 | var defaultHtmlFilterRulesEditableOnly = { | ||
589 | elements: { | ||
590 | embed: function( element ) { | ||
591 | var parent = element.parent; | ||
592 | |||
593 | // If the <embed> is child of a <object>, copy the width | ||
594 | // and height attributes from it. | ||
595 | if ( parent && parent.name == 'object' ) { | ||
596 | var parentWidth = parent.attributes.width, | ||
597 | parentHeight = parent.attributes.height; | ||
598 | if ( parentWidth ) | ||
599 | element.attributes.width = parentWidth; | ||
600 | if ( parentHeight ) | ||
601 | element.attributes.height = parentHeight; | ||
602 | } | ||
603 | }, | ||
604 | |||
605 | // Remove empty link but not empty anchor. (#3829, #13516) | ||
606 | a: function( element ) { | ||
607 | var attrs = element.attributes; | ||
608 | |||
609 | if ( !( element.children.length || attrs.name || attrs.id || element.attributes[ 'data-cke-saved-name' ] ) ) | ||
610 | return false; | ||
611 | } | ||
612 | } | ||
613 | }; | ||
614 | |||
615 | // These rules will also be applied to non-editable content. | ||
616 | var defaultHtmlFilterRulesForAll = { | ||
617 | elementNames: [ | ||
618 | // Remove the "cke:" namespace prefix. | ||
619 | [ ( /^cke:/ ), '' ], | ||
620 | |||
621 | // Ignore <?xml:namespace> tags. | ||
622 | [ ( /^\?xml:namespace$/ ), '' ] | ||
623 | ], | ||
624 | |||
625 | attributeNames: [ | ||
626 | // Attributes saved for changes and protected attributes. | ||
627 | [ ( /^data-cke-(saved|pa)-/ ), '' ], | ||
628 | |||
629 | // All "data-cke-" attributes are to be ignored. | ||
630 | [ ( /^data-cke-.*/ ), '' ], | ||
631 | |||
632 | [ 'hidefocus', '' ] | ||
633 | ], | ||
634 | |||
635 | elements: { | ||
636 | $: function( element ) { | ||
637 | var attribs = element.attributes; | ||
638 | |||
639 | if ( attribs ) { | ||
640 | // Elements marked as temporary are to be ignored. | ||
641 | if ( attribs[ 'data-cke-temp' ] ) | ||
642 | return false; | ||
643 | |||
644 | // Remove duplicated attributes - #3789. | ||
645 | var attributeNames = [ 'name', 'href', 'src' ], | ||
646 | savedAttributeName; | ||
647 | for ( var i = 0; i < attributeNames.length; i++ ) { | ||
648 | savedAttributeName = 'data-cke-saved-' + attributeNames[ i ]; | ||
649 | savedAttributeName in attribs && ( delete attribs[ attributeNames[ i ] ] ); | ||
650 | } | ||
651 | } | ||
652 | |||
653 | return element; | ||
654 | }, | ||
655 | |||
656 | // The contents of table should be in correct order (#4809). | ||
657 | table: function( element ) { | ||
658 | // Clone the array as it would become empty during the sort call. | ||
659 | var children = element.children.slice( 0 ); | ||
660 | |||
661 | children.sort( function( node1, node2 ) { | ||
662 | var index1, index2; | ||
663 | |||
664 | // Compare in the predefined order. | ||
665 | if ( node1.type == CKEDITOR.NODE_ELEMENT && node2.type == node1.type ) { | ||
666 | index1 = CKEDITOR.tools.indexOf( tableOrder, node1.name ); | ||
667 | index2 = CKEDITOR.tools.indexOf( tableOrder, node2.name ); | ||
668 | } | ||
669 | |||
670 | // Make sure the sort is stable, if no order can be established above. | ||
671 | if ( !( index1 > -1 && index2 > -1 && index1 != index2 ) ) { | ||
672 | index1 = getNodeIndex( node1 ); | ||
673 | index2 = getNodeIndex( node2 ); | ||
674 | } | ||
675 | |||
676 | return index1 > index2 ? 1 : -1; | ||
677 | } ); | ||
678 | }, | ||
679 | |||
680 | // Restore param elements into self-closing. | ||
681 | param: function( param ) { | ||
682 | param.children = []; | ||
683 | param.isEmpty = true; | ||
684 | return param; | ||
685 | }, | ||
686 | |||
687 | // Remove dummy span in webkit. | ||
688 | span: function( element ) { | ||
689 | if ( element.attributes[ 'class' ] == 'Apple-style-span' ) | ||
690 | delete element.name; | ||
691 | }, | ||
692 | |||
693 | html: function( element ) { | ||
694 | delete element.attributes.contenteditable; | ||
695 | delete element.attributes[ 'class' ]; | ||
696 | }, | ||
697 | |||
698 | body: function( element ) { | ||
699 | delete element.attributes.spellcheck; | ||
700 | delete element.attributes.contenteditable; | ||
701 | }, | ||
702 | |||
703 | style: function( element ) { | ||
704 | var child = element.children[ 0 ]; | ||
705 | if ( child && child.value ) | ||
706 | child.value = CKEDITOR.tools.trim( child.value ); | ||
707 | |||
708 | if ( !element.attributes.type ) | ||
709 | element.attributes.type = 'text/css'; | ||
710 | }, | ||
711 | |||
712 | title: function( element ) { | ||
713 | var titleText = element.children[ 0 ]; | ||
714 | |||
715 | // Append text-node to title tag if not present (i.e. non-IEs) (#9882). | ||
716 | !titleText && append( element, titleText = new CKEDITOR.htmlParser.text() ); | ||
717 | |||
718 | // Transfer data-saved title to title tag. | ||
719 | titleText.value = element.attributes[ 'data-cke-title' ] || ''; | ||
720 | }, | ||
721 | |||
722 | input: unprotectReadyOnly, | ||
723 | textarea: unprotectReadyOnly | ||
724 | }, | ||
725 | |||
726 | attributes: { | ||
727 | 'class': function( value ) { | ||
728 | // Remove all class names starting with "cke_". | ||
729 | return CKEDITOR.tools.ltrim( value.replace( /(?:^|\s+)cke_[^\s]*/g, '' ) ) || false; | ||
730 | } | ||
731 | } | ||
732 | }; | ||
733 | |||
734 | if ( CKEDITOR.env.ie ) { | ||
735 | // IE outputs style attribute in capital letters. We should convert | ||
736 | // them back to lower case, while not hurting the values (#5930) | ||
737 | defaultHtmlFilterRulesForAll.attributes.style = function( value ) { | ||
738 | return value.replace( /(^|;)([^\:]+)/g, function( match ) { | ||
739 | return match.toLowerCase(); | ||
740 | } ); | ||
741 | }; | ||
742 | } | ||
743 | |||
744 | // Disable form elements editing mode provided by some browsers. (#5746) | ||
745 | function unprotectReadyOnly( element ) { | ||
746 | var attrs = element.attributes; | ||
747 | switch ( attrs[ 'data-cke-editable' ] ) { | ||
748 | case 'true': | ||
749 | attrs.contenteditable = 'true'; | ||
750 | break; | ||
751 | case '1': | ||
752 | delete attrs.contenteditable; | ||
753 | break; | ||
754 | } | ||
755 | } | ||
756 | |||
757 | // | ||
758 | // Preprocessor filters --------------------------------------------------- | ||
759 | // | ||
760 | |||
761 | var protectElementRegex = /<(a|area|img|input|source)\b([^>]*)>/gi, | ||
762 | // Be greedy while looking for protected attributes. This will let us avoid an unfortunate | ||
763 | // situation when "nested attributes", which may appear valid, are also protected. | ||
764 | // I.e. if we consider the following HTML: | ||
765 | // | ||
766 | // <img data-x="<a href="X"" /> | ||
767 | // | ||
768 | // then the "non-greedy match" returns: | ||
769 | // | ||
770 | // 'href' => '"X"' // It's wrong! Href is not an attribute of <img>. | ||
771 | // | ||
772 | // while greedy match returns: | ||
773 | // | ||
774 | // 'data-x' => '<a href="X"' | ||
775 | // | ||
776 | // which, can be easily filtered out (#11508). | ||
777 | protectAttributeRegex = /([\w-:]+)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+))/gi, | ||
778 | protectAttributeNameRegex = /^(href|src|name)$/i; | ||
779 | |||
780 | // Note: we use lazy star '*?' to prevent eating everything up to the last occurrence of </style> or </textarea>. | ||
781 | var protectElementsRegex = /(?:<style(?=[ >])[^>]*>[\s\S]*?<\/style>)|(?:<(:?link|meta|base)[^>]*>)/gi, | ||
782 | protectTextareaRegex = /(<textarea(?=[ >])[^>]*>)([\s\S]*?)(?:<\/textarea>)/gi, | ||
783 | encodedElementsRegex = /<cke:encoded>([^<]*)<\/cke:encoded>/gi; | ||
784 | |||
785 | var protectElementNamesRegex = /(<\/?)((?:object|embed|param|html|body|head|title)[^>]*>)/gi, | ||
786 | unprotectElementNamesRegex = /(<\/?)cke:((?:html|body|head|title)[^>]*>)/gi; | ||
787 | |||
788 | var protectSelfClosingRegex = /<cke:(param|embed)([^>]*?)\/?>(?!\s*<\/cke:\1)/gi; | ||
789 | |||
790 | function protectAttributes( html ) { | ||
791 | return html.replace( protectElementRegex, function( element, tag, attributes ) { | ||
792 | return '<' + tag + attributes.replace( protectAttributeRegex, function( fullAttr, attrName ) { | ||
793 | // Avoid corrupting the inline event attributes (#7243). | ||
794 | // We should not rewrite the existed protected attributes, e.g. clipboard content from editor. (#5218) | ||
795 | if ( protectAttributeNameRegex.test( attrName ) && attributes.indexOf( 'data-cke-saved-' + attrName ) == -1 ) | ||
796 | return ' data-cke-saved-' + fullAttr + ' data-cke-' + CKEDITOR.rnd + '-' + fullAttr; | ||
797 | |||
798 | return fullAttr; | ||
799 | } ) + '>'; | ||
800 | } ); | ||
801 | } | ||
802 | |||
803 | function protectElements( html, regex ) { | ||
804 | return html.replace( regex, function( match, tag, content ) { | ||
805 | // Encode < and > in textarea because this won't be done by a browser, since | ||
806 | // textarea will be protected during passing data through fix bin. | ||
807 | if ( match.indexOf( '<textarea' ) === 0 ) | ||
808 | match = tag + unprotectRealComments( content ).replace( /</g, '<' ).replace( />/g, '>' ) + '</textarea>'; | ||
809 | |||
810 | return '<cke:encoded>' + encodeURIComponent( match ) + '</cke:encoded>'; | ||
811 | } ); | ||
812 | } | ||
813 | |||
814 | function unprotectElements( html ) { | ||
815 | return html.replace( encodedElementsRegex, function( match, encoded ) { | ||
816 | return decodeURIComponent( encoded ); | ||
817 | } ); | ||
818 | } | ||
819 | |||
820 | function protectElementsNames( html ) { | ||
821 | return html.replace( protectElementNamesRegex, '$1cke:$2' ); | ||
822 | } | ||
823 | |||
824 | function unprotectElementNames( html ) { | ||
825 | return html.replace( unprotectElementNamesRegex, '$1$2' ); | ||
826 | } | ||
827 | |||
828 | function protectSelfClosingElements( html ) { | ||
829 | return html.replace( protectSelfClosingRegex, '<cke:$1$2></cke:$1>' ); | ||
830 | } | ||
831 | |||
832 | function protectPreFormatted( html ) { | ||
833 | return html.replace( /(<pre\b[^>]*>)(\r\n|\n)/g, '$1$2$2' ); | ||
834 | } | ||
835 | |||
836 | function protectRealComments( html ) { | ||
837 | return html.replace( /<!--(?!{cke_protected})[\s\S]+?-->/g, function( match ) { | ||
838 | return '<!--' + protectedSourceMarker + | ||
839 | '{C}' + | ||
840 | encodeURIComponent( match ).replace( /--/g, '%2D%2D' ) + | ||
841 | '-->'; | ||
842 | } ); | ||
843 | } | ||
844 | |||
845 | // Replace all "on\w{3,}" strings which are not: | ||
846 | // * opening tags - e.g. `<onfoo`, | ||
847 | // * closing tags - e.g. </onfoo> (tested in "false positive 1"), | ||
848 | // * part of other attribute - e.g. `data-onfoo` or `fonfoo`. | ||
849 | function protectInsecureAttributes( html ) { | ||
850 | return html.replace( /([^a-z0-9<\-])(on\w{3,})(?!>)/gi, '$1data-cke-' + CKEDITOR.rnd + '-$2' ); | ||
851 | } | ||
852 | |||
853 | function unprotectRealComments( html ) { | ||
854 | return html.replace( /<!--\{cke_protected\}\{C\}([\s\S]+?)-->/g, function( match, data ) { | ||
855 | return decodeURIComponent( data ); | ||
856 | } ); | ||
857 | } | ||
858 | |||
859 | function unprotectSource( html, editor ) { | ||
860 | var store = editor._.dataStore; | ||
861 | |||
862 | return html.replace( /<!--\{cke_protected\}([\s\S]+?)-->/g, function( match, data ) { | ||
863 | return decodeURIComponent( data ); | ||
864 | } ).replace( /\{cke_protected_(\d+)\}/g, function( match, id ) { | ||
865 | return store && store[ id ] || ''; | ||
866 | } ); | ||
867 | } | ||
868 | |||
869 | function protectSource( data, editor ) { | ||
870 | var protectedHtml = [], | ||
871 | protectRegexes = editor.config.protectedSource, | ||
872 | store = editor._.dataStore || ( editor._.dataStore = { id: 1 } ), | ||
873 | tempRegex = /<\!--\{cke_temp(comment)?\}(\d*?)-->/g; | ||
874 | |||
875 | var regexes = [ | ||
876 | // Script tags will also be forced to be protected, otherwise | ||
877 | // IE will execute them. | ||
878 | ( /<script[\s\S]*?(<\/script>|$)/gi ), | ||
879 | |||
880 | // <noscript> tags (get lost in IE and messed up in FF). | ||
881 | /<noscript[\s\S]*?<\/noscript>/gi, | ||
882 | |||
883 | // Avoid meta tags being stripped (#8117). | ||
884 | /<meta[\s\S]*?\/?>/gi | ||
885 | ].concat( protectRegexes ); | ||
886 | |||
887 | // First of any other protection, we must protect all comments | ||
888 | // to avoid loosing them (of course, IE related). | ||
889 | // Note that we use a different tag for comments, as we need to | ||
890 | // transform them when applying filters. | ||
891 | data = data.replace( ( /<!--[\s\S]*?-->/g ), function( match ) { | ||
892 | return '<!--{cke_tempcomment}' + ( protectedHtml.push( match ) - 1 ) + '-->'; | ||
893 | } ); | ||
894 | |||
895 | for ( var i = 0; i < regexes.length; i++ ) { | ||
896 | data = data.replace( regexes[ i ], function( match ) { | ||
897 | match = match.replace( tempRegex, // There could be protected source inside another one. (#3869). | ||
898 | function( $, isComment, id ) { | ||
899 | return protectedHtml[ id ]; | ||
900 | } ); | ||
901 | |||
902 | // Avoid protecting over protected, e.g. /\{.*?\}/ | ||
903 | return ( /cke_temp(comment)?/ ).test( match ) ? match : '<!--{cke_temp}' + ( protectedHtml.push( match ) - 1 ) + '-->'; | ||
904 | } ); | ||
905 | } | ||
906 | data = data.replace( tempRegex, function( $, isComment, id ) { | ||
907 | return '<!--' + protectedSourceMarker + | ||
908 | ( isComment ? '{C}' : '' ) + | ||
909 | encodeURIComponent( protectedHtml[ id ] ).replace( /--/g, '%2D%2D' ) + | ||
910 | '-->'; | ||
911 | } ); | ||
912 | |||
913 | // Different protection pattern is used for those that | ||
914 | // live in attributes to avoid from being HTML encoded. | ||
915 | // Why so serious? See #9205, #8216, #7805, #11754, #11846. | ||
916 | data = data.replace( /<\w+(?:\s+(?:(?:[^\s=>]+\s*=\s*(?:[^'"\s>]+|'[^']*'|"[^"]*"))|[^\s=\/>]+))+\s*\/?>/g, function( match ) { | ||
917 | return match.replace( /<!--\{cke_protected\}([^>]*)-->/g, function( match, data ) { | ||
918 | store[ store.id ] = decodeURIComponent( data ); | ||
919 | return '{cke_protected_' + ( store.id++ ) + '}'; | ||
920 | } ); | ||
921 | } ); | ||
922 | |||
923 | // This RegExp searches for innerText in all the title/iframe/textarea elements. | ||
924 | // This is because browser doesn't allow HTML in these elements, that's why we can't | ||
925 | // nest comments in there. (#11223) | ||
926 | data = data.replace( /<(title|iframe|textarea)([^>]*)>([\s\S]*?)<\/\1>/g, function( match, tagName, tagAttributes, innerText ) { | ||
927 | return '<' + tagName + tagAttributes + '>' + unprotectSource( unprotectRealComments( innerText ), editor ) + '</' + tagName + '>'; | ||
928 | } ); | ||
929 | |||
930 | return data; | ||
931 | } | ||
932 | |||
933 | // Creates a block if the root element is empty. | ||
934 | function fixEmptyRoot( root, fixBodyTag ) { | ||
935 | if ( !root.children.length && CKEDITOR.dtd[ root.name ][ fixBodyTag ] ) { | ||
936 | var fixBodyElement = new CKEDITOR.htmlParser.element( fixBodyTag ); | ||
937 | root.add( fixBodyElement ); | ||
938 | } | ||
939 | } | ||
940 | } )(); | ||
941 | |||
942 | /** | ||
943 | * Whether a filler text (non-breaking space entity — ` `) will be | ||
944 | * inserted into empty block elements in HTML output. | ||
945 | * This is used to render block elements properly with `line-height`. | ||
946 | * When a function is specified instead, it will be passed a {@link CKEDITOR.htmlParser.element} | ||
947 | * to decide whether adding the filler text by expecting a Boolean return value. | ||
948 | * | ||
949 | * config.fillEmptyBlocks = false; // Prevent filler nodes in all empty blocks. | ||
950 | * | ||
951 | * // Prevent filler node only in float cleaners. | ||
952 | * config.fillEmptyBlocks = function( element ) { | ||
953 | * if ( element.attributes[ 'class' ].indexOf( 'clear-both' ) != -1 ) | ||
954 | * return false; | ||
955 | * }; | ||
956 | * | ||
957 | * @since 3.5 | ||
958 | * @cfg {Boolean/Function} [fillEmptyBlocks=true] | ||
959 | * @member CKEDITOR.config | ||
960 | */ | ||
961 | |||
962 | /** | ||
963 | * This event is fired by the {@link CKEDITOR.htmlDataProcessor} when input HTML | ||
964 | * is to be purified by the {@link CKEDITOR.htmlDataProcessor#toHtml} method. | ||
965 | * | ||
966 | * By adding listeners with different priorities it is possible | ||
967 | * to process input HTML on different stages: | ||
968 | * | ||
969 | * * 1-4: Data is available in the original string format. | ||
970 | * * 5: Data is initially filtered with regexp patterns and parsed to | ||
971 | * {@link CKEDITOR.htmlParser.fragment} {@link CKEDITOR.htmlParser.element}. | ||
972 | * * 5-9: Data is available in the parsed format, but {@link CKEDITOR.htmlDataProcessor#dataFilter} | ||
973 | * is not applied yet. | ||
974 | * * 6: Data is filtered with the {CKEDITOR.filter content filter}. | ||
975 | * * 10: Data is processed with {@link CKEDITOR.htmlDataProcessor#dataFilter}. | ||
976 | * * 10-14: Data is available in the parsed format and {@link CKEDITOR.htmlDataProcessor#dataFilter} | ||
977 | * has already been applied. | ||
978 | * * 15: Data is written back to an HTML string. | ||
979 | * * 15-*: Data is available in an HTML string. | ||
980 | * | ||
981 | * For example to be able to process parsed, but not yet filtered data add listener this way: | ||
982 | * | ||
983 | * editor.on( 'toHtml', function( evt) { | ||
984 | * evt.data.dataValue; // -> CKEDITOR.htmlParser.fragment instance | ||
985 | * }, null, null, 7 ); | ||
986 | * | ||
987 | * @since 4.1 | ||
988 | * @event toHtml | ||
989 | * @member CKEDITOR.editor | ||
990 | * @param {CKEDITOR.editor} editor This editor instance. | ||
991 | * @param data | ||
992 | * @param {String/CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} data.dataValue Input data to be purified. | ||
993 | * @param {String} data.context See {@link CKEDITOR.htmlDataProcessor#toHtml} The `context` argument. | ||
994 | * @param {Boolean} data.fixForBody See {@link CKEDITOR.htmlDataProcessor#toHtml} The `fixForBody` argument. | ||
995 | * @param {Boolean} data.dontFilter See {@link CKEDITOR.htmlDataProcessor#toHtml} The `dontFilter` argument. | ||
996 | * @param {Boolean} data.filter See {@link CKEDITOR.htmlDataProcessor#toHtml} The `filter` argument. | ||
997 | * @param {Boolean} data.enterMode See {@link CKEDITOR.htmlDataProcessor#toHtml} The `enterMode` argument. | ||
998 | * @param {Boolean} [data.protectedWhitespaces] See {@link CKEDITOR.htmlDataProcessor#toHtml} The `protectedWhitespaces` argument. | ||
999 | */ | ||
1000 | |||
1001 | /** | ||
1002 | * This event is fired when {@link CKEDITOR.htmlDataProcessor} is converting | ||
1003 | * internal HTML to output data HTML. | ||
1004 | * | ||
1005 | * By adding listeners with different priorities it is possible | ||
1006 | * to process input HTML on different stages: | ||
1007 | * | ||
1008 | * * 1-4: Data is available in the original string format. | ||
1009 | * * 5: Data is initially filtered with regexp patterns and parsed to | ||
1010 | * {@link CKEDITOR.htmlParser.fragment} {@link CKEDITOR.htmlParser.element}. | ||
1011 | * * 5-9: Data is available in the parsed format, but {@link CKEDITOR.htmlDataProcessor#htmlFilter} | ||
1012 | * is not applied yet. | ||
1013 | * * 10: Data is filtered with {@link CKEDITOR.htmlDataProcessor#htmlFilter}. | ||
1014 | * * 11: Data is filtered with the {CKEDITOR.filter content filter} (on output the content filter makes | ||
1015 | * only transformations, without filtering). | ||
1016 | * * 10-14: Data is available in the parsed format and {@link CKEDITOR.htmlDataProcessor#htmlFilter} | ||
1017 | * has already been applied. | ||
1018 | * * 15: Data is written back to an HTML string. | ||
1019 | * * 15-*: Data is available in an HTML string. | ||
1020 | * | ||
1021 | * For example to be able to process parsed and already processed data add listener this way: | ||
1022 | * | ||
1023 | * editor.on( 'toDataFormat', function( evt) { | ||
1024 | * evt.data.dataValue; // -> CKEDITOR.htmlParser.fragment instance | ||
1025 | * }, null, null, 12 ); | ||
1026 | * | ||
1027 | * @since 4.1 | ||
1028 | * @event toDataFormat | ||
1029 | * @member CKEDITOR.editor | ||
1030 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1031 | * @param data | ||
1032 | * @param {String/CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} data.dataValue Output data to be prepared. | ||
1033 | * @param {String} data.context See {@link CKEDITOR.htmlDataProcessor#toDataFormat} The `context` argument. | ||
1034 | * @param {Boolean} data.filter See {@link CKEDITOR.htmlDataProcessor#toDataFormat} The `filter` argument. | ||
1035 | * @param {Boolean} data.enterMode See {@link CKEDITOR.htmlDataProcessor#toDataFormat} The `enterMode` argument. | ||
1036 | */ | ||
diff --git a/sources/core/htmlparser.js b/sources/core/htmlparser.js new file mode 100644 index 0000000..dffde95 --- /dev/null +++ b/sources/core/htmlparser.js | |||
@@ -0,0 +1,205 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * Provides an "event like" system to parse strings of HTML data. | ||
8 | * | ||
9 | * var parser = new CKEDITOR.htmlParser(); | ||
10 | * parser.onTagOpen = function( tagName, attributes, selfClosing ) { | ||
11 | * alert( tagName ); | ||
12 | * }; | ||
13 | * parser.parse( '<p>Some <b>text</b>.</p>' ); // Alerts 'p', 'b'. | ||
14 | * | ||
15 | * @class | ||
16 | * @constructor Creates a htmlParser class instance. | ||
17 | */ | ||
18 | CKEDITOR.htmlParser = function() { | ||
19 | this._ = { | ||
20 | htmlPartsRegex: /<(?:(?:\/([^>]+)>)|(?:!--([\S|\s]*?)-->)|(?:([^\/\s>]+)((?:\s+[\w\-:.]+(?:\s*=\s*?(?:(?:"[^"]*")|(?:'[^']*')|[^\s"'\/>]+))?)*)[\S\s]*?(\/?)>))/g | ||
21 | }; | ||
22 | }; | ||
23 | |||
24 | ( function() { | ||
25 | var attribsRegex = /([\w\-:.]+)(?:(?:\s*=\s*(?:(?:"([^"]*)")|(?:'([^']*)')|([^\s>]+)))|(?=\s|$))/g, | ||
26 | emptyAttribs = { checked: 1, compact: 1, declare: 1, defer: 1, disabled: 1, ismap: 1, multiple: 1, nohref: 1, noresize: 1, noshade: 1, nowrap: 1, readonly: 1, selected: 1 }; | ||
27 | |||
28 | CKEDITOR.htmlParser.prototype = { | ||
29 | /** | ||
30 | * Function to be fired when a tag opener is found. This function | ||
31 | * should be overriden when using this class. | ||
32 | * | ||
33 | * var parser = new CKEDITOR.htmlParser(); | ||
34 | * parser.onTagOpen = function( tagName, attributes, selfClosing ) { | ||
35 | * alert( tagName ); // e.g. 'b' | ||
36 | * } ); | ||
37 | * parser.parse( '<!-- Example --><b>Hello</b>' ); | ||
38 | * | ||
39 | * @param {String} tagName The tag name. The name is guarantted to be lowercased. | ||
40 | * @param {Object} attributes An object containing all tag attributes. Each | ||
41 | * property in this object represent and attribute name and its value is the attribute value. | ||
42 | * @param {Boolean} selfClosing `true` if the tag closes itself, false if the tag doesn't. | ||
43 | */ | ||
44 | onTagOpen: function() {}, | ||
45 | |||
46 | /** | ||
47 | * Function to be fired when a tag closer is found. This function | ||
48 | * should be overriden when using this class. | ||
49 | * | ||
50 | * var parser = new CKEDITOR.htmlParser(); | ||
51 | * parser.onTagClose = function( tagName ) { | ||
52 | * alert( tagName ); // 'b' | ||
53 | * } ); | ||
54 | * parser.parse( '<!-- Example --><b>Hello</b>' ); | ||
55 | * | ||
56 | * @param {String} tagName The tag name. The name is guarantted to be lowercased. | ||
57 | */ | ||
58 | onTagClose: function() {}, | ||
59 | |||
60 | /** | ||
61 | * Function to be fired when text is found. This function | ||
62 | * should be overriden when using this class. | ||
63 | * | ||
64 | * var parser = new CKEDITOR.htmlParser(); | ||
65 | * parser.onText = function( text ) { | ||
66 | * alert( text ); // 'Hello' | ||
67 | * } ); | ||
68 | * parser.parse( '<!-- Example --><b>Hello</b>' ); | ||
69 | * | ||
70 | * @param {String} text The text found. | ||
71 | */ | ||
72 | onText: function() {}, | ||
73 | |||
74 | /** | ||
75 | * Function to be fired when CDATA section is found. This function | ||
76 | * should be overriden when using this class. | ||
77 | * | ||
78 | * var parser = new CKEDITOR.htmlParser(); | ||
79 | * parser.onCDATA = function( cdata ) { | ||
80 | * alert( cdata ); // 'var hello;' | ||
81 | * } ); | ||
82 | * parser.parse( '<script>var hello;</script>' ); | ||
83 | * | ||
84 | * @param {String} cdata The CDATA been found. | ||
85 | */ | ||
86 | onCDATA: function() {}, | ||
87 | |||
88 | /** | ||
89 | * Function to be fired when a commend is found. This function | ||
90 | * should be overriden when using this class. | ||
91 | * | ||
92 | * var parser = new CKEDITOR.htmlParser(); | ||
93 | * parser.onComment = function( comment ) { | ||
94 | * alert( comment ); // ' Example ' | ||
95 | * } ); | ||
96 | * parser.parse( '<!-- Example --><b>Hello</b>' ); | ||
97 | * | ||
98 | * @param {String} comment The comment text. | ||
99 | */ | ||
100 | onComment: function() {}, | ||
101 | |||
102 | /** | ||
103 | * Parses text, looking for HTML tokens, like tag openers or closers, | ||
104 | * or comments. This function fires the onTagOpen, onTagClose, onText | ||
105 | * and onComment function during its execution. | ||
106 | * | ||
107 | * var parser = new CKEDITOR.htmlParser(); | ||
108 | * // The onTagOpen, onTagClose, onText and onComment should be overriden | ||
109 | * // at this point. | ||
110 | * parser.parse( '<!-- Example --><b>Hello</b>' ); | ||
111 | * | ||
112 | * @param {String} html The HTML to be parsed. | ||
113 | */ | ||
114 | parse: function( html ) { | ||
115 | var parts, tagName, | ||
116 | nextIndex = 0, | ||
117 | cdata; // The collected data inside a CDATA section. | ||
118 | |||
119 | while ( ( parts = this._.htmlPartsRegex.exec( html ) ) ) { | ||
120 | var tagIndex = parts.index; | ||
121 | if ( tagIndex > nextIndex ) { | ||
122 | var text = html.substring( nextIndex, tagIndex ); | ||
123 | |||
124 | if ( cdata ) | ||
125 | cdata.push( text ); | ||
126 | else | ||
127 | this.onText( text ); | ||
128 | } | ||
129 | |||
130 | nextIndex = this._.htmlPartsRegex.lastIndex; | ||
131 | |||
132 | // "parts" is an array with the following items: | ||
133 | // 0 : The entire match for opening/closing tags and comments. | ||
134 | // : Group filled with the tag name for closing tags. | ||
135 | // 2 : Group filled with the comment text. | ||
136 | // 3 : Group filled with the tag name for opening tags. | ||
137 | // 4 : Group filled with the attributes part of opening tags. | ||
138 | |||
139 | // Closing tag | ||
140 | if ( ( tagName = parts[ 1 ] ) ) { | ||
141 | tagName = tagName.toLowerCase(); | ||
142 | |||
143 | if ( cdata && CKEDITOR.dtd.$cdata[ tagName ] ) { | ||
144 | // Send the CDATA data. | ||
145 | this.onCDATA( cdata.join( '' ) ); | ||
146 | cdata = null; | ||
147 | } | ||
148 | |||
149 | if ( !cdata ) { | ||
150 | this.onTagClose( tagName ); | ||
151 | continue; | ||
152 | } | ||
153 | } | ||
154 | |||
155 | // If CDATA is enabled, just save the raw match. | ||
156 | if ( cdata ) { | ||
157 | cdata.push( parts[ 0 ] ); | ||
158 | continue; | ||
159 | } | ||
160 | |||
161 | // Opening tag | ||
162 | if ( ( tagName = parts[ 3 ] ) ) { | ||
163 | tagName = tagName.toLowerCase(); | ||
164 | |||
165 | // There are some tag names that can break things, so let's | ||
166 | // simply ignore them when parsing. (#5224) | ||
167 | if ( /="/.test( tagName ) ) | ||
168 | continue; | ||
169 | |||
170 | var attribs = {}, | ||
171 | attribMatch, | ||
172 | attribsPart = parts[ 4 ], | ||
173 | selfClosing = !!parts[ 5 ]; | ||
174 | |||
175 | if ( attribsPart ) { | ||
176 | while ( ( attribMatch = attribsRegex.exec( attribsPart ) ) ) { | ||
177 | var attName = attribMatch[ 1 ].toLowerCase(), | ||
178 | attValue = attribMatch[ 2 ] || attribMatch[ 3 ] || attribMatch[ 4 ] || ''; | ||
179 | |||
180 | if ( !attValue && emptyAttribs[ attName ] ) | ||
181 | attribs[ attName ] = attName; | ||
182 | else | ||
183 | attribs[ attName ] = CKEDITOR.tools.htmlDecodeAttr( attValue ); | ||
184 | } | ||
185 | } | ||
186 | |||
187 | this.onTagOpen( tagName, attribs, selfClosing ); | ||
188 | |||
189 | // Open CDATA mode when finding the appropriate tags. | ||
190 | if ( !cdata && CKEDITOR.dtd.$cdata[ tagName ] ) | ||
191 | cdata = []; | ||
192 | |||
193 | continue; | ||
194 | } | ||
195 | |||
196 | // Comment | ||
197 | if ( ( tagName = parts[ 2 ] ) ) | ||
198 | this.onComment( tagName ); | ||
199 | } | ||
200 | |||
201 | if ( html.length > nextIndex ) | ||
202 | this.onText( html.substring( nextIndex, html.length ) ); | ||
203 | } | ||
204 | }; | ||
205 | } )(); | ||
diff --git a/sources/core/htmlparser/basicwriter.js b/sources/core/htmlparser/basicwriter.js new file mode 100644 index 0000000..62a97ef --- /dev/null +++ b/sources/core/htmlparser/basicwriter.js | |||
@@ -0,0 +1,152 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * TODO | ||
8 | * | ||
9 | * @class | ||
10 | * @todo | ||
11 | */ | ||
12 | CKEDITOR.htmlParser.basicWriter = CKEDITOR.tools.createClass( { | ||
13 | /** | ||
14 | * Creates a basicWriter class instance. | ||
15 | * | ||
16 | * @constructor | ||
17 | */ | ||
18 | $: function() { | ||
19 | this._ = { | ||
20 | output: [] | ||
21 | }; | ||
22 | }, | ||
23 | |||
24 | proto: { | ||
25 | /** | ||
26 | * Writes the tag opening part for a opener tag. | ||
27 | * | ||
28 | * // Writes '<p'. | ||
29 | * writer.openTag( 'p', { class : 'MyClass', id : 'MyId' } ); | ||
30 | * | ||
31 | * @param {String} tagName The element name for this tag. | ||
32 | * @param {Object} attributes The attributes defined for this tag. The | ||
33 | * attributes could be used to inspect the tag. | ||
34 | */ | ||
35 | openTag: function( tagName ) { | ||
36 | this._.output.push( '<', tagName ); | ||
37 | }, | ||
38 | |||
39 | /** | ||
40 | * Writes the tag closing part for a opener tag. | ||
41 | * | ||
42 | * // Writes '>'. | ||
43 | * writer.openTagClose( 'p', false ); | ||
44 | * | ||
45 | * // Writes ' />'. | ||
46 | * writer.openTagClose( 'br', true ); | ||
47 | * | ||
48 | * @param {String} tagName The element name for this tag. | ||
49 | * @param {Boolean} isSelfClose Indicates that this is a self-closing tag, | ||
50 | * like `<br>` or `<img>`. | ||
51 | */ | ||
52 | openTagClose: function( tagName, isSelfClose ) { | ||
53 | if ( isSelfClose ) | ||
54 | this._.output.push( ' />' ); | ||
55 | else | ||
56 | this._.output.push( '>' ); | ||
57 | }, | ||
58 | |||
59 | /** | ||
60 | * Writes an attribute. This function should be called after opening the | ||
61 | * tag with {@link #openTagClose}. | ||
62 | * | ||
63 | * // Writes ' class="MyClass"'. | ||
64 | * writer.attribute( 'class', 'MyClass' ); | ||
65 | * | ||
66 | * @param {String} attName The attribute name. | ||
67 | * @param {String} attValue The attribute value. | ||
68 | */ | ||
69 | attribute: function( attName, attValue ) { | ||
70 | // Browsers don't always escape special character in attribute values. (#4683, #4719). | ||
71 | if ( typeof attValue == 'string' ) | ||
72 | attValue = CKEDITOR.tools.htmlEncodeAttr( attValue ); | ||
73 | |||
74 | this._.output.push( ' ', attName, '="', attValue, '"' ); | ||
75 | }, | ||
76 | |||
77 | /** | ||
78 | * Writes a closer tag. | ||
79 | * | ||
80 | * // Writes '</p>'. | ||
81 | * writer.closeTag( 'p' ); | ||
82 | * | ||
83 | * @param {String} tagName The element name for this tag. | ||
84 | */ | ||
85 | closeTag: function( tagName ) { | ||
86 | this._.output.push( '</', tagName, '>' ); | ||
87 | }, | ||
88 | |||
89 | /** | ||
90 | * Writes text. | ||
91 | * | ||
92 | * // Writes 'Hello Word'. | ||
93 | * writer.text( 'Hello Word' ); | ||
94 | * | ||
95 | * @param {String} text The text value. | ||
96 | */ | ||
97 | text: function( text ) { | ||
98 | this._.output.push( text ); | ||
99 | }, | ||
100 | |||
101 | /** | ||
102 | * Writes a comment. | ||
103 | * | ||
104 | * // Writes '<!-- My comment -->'. | ||
105 | * writer.comment( ' My comment ' ); | ||
106 | * | ||
107 | * @param {String} comment The comment text. | ||
108 | */ | ||
109 | comment: function( comment ) { | ||
110 | this._.output.push( '<!--', comment, '-->' ); | ||
111 | }, | ||
112 | |||
113 | /** | ||
114 | * Writes any kind of data to the ouput. | ||
115 | * | ||
116 | * writer.write( 'This is an <b>example</b>.' ); | ||
117 | * | ||
118 | * @param {String} data | ||
119 | */ | ||
120 | write: function( data ) { | ||
121 | this._.output.push( data ); | ||
122 | }, | ||
123 | |||
124 | /** | ||
125 | * Empties the current output buffer. | ||
126 | * | ||
127 | * writer.reset(); | ||
128 | */ | ||
129 | reset: function() { | ||
130 | this._.output = []; | ||
131 | this._.indent = false; | ||
132 | }, | ||
133 | |||
134 | /** | ||
135 | * Empties the current output buffer. | ||
136 | * | ||
137 | * var html = writer.getHtml(); | ||
138 | * | ||
139 | * @param {Boolean} reset Indicates that the {@link #reset} method is to | ||
140 | * be automatically called after retrieving the HTML. | ||
141 | * @returns {String} The HTML written to the writer so far. | ||
142 | */ | ||
143 | getHtml: function( reset ) { | ||
144 | var html = this._.output.join( '' ); | ||
145 | |||
146 | if ( reset ) | ||
147 | this.reset(); | ||
148 | |||
149 | return html; | ||
150 | } | ||
151 | } | ||
152 | } ); | ||
diff --git a/sources/core/htmlparser/cdata.js b/sources/core/htmlparser/cdata.js new file mode 100644 index 0000000..4ece2b7 --- /dev/null +++ b/sources/core/htmlparser/cdata.js | |||
@@ -0,0 +1,48 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 | /** | ||
11 | * A lightweight representation of HTML CDATA. | ||
12 | * | ||
13 | * @class | ||
14 | * @extends CKEDITOR.htmlParser.node | ||
15 | * @constructor Creates a cdata class instance. | ||
16 | * @param {String} value The CDATA section value. | ||
17 | */ | ||
18 | CKEDITOR.htmlParser.cdata = function( value ) { | ||
19 | /** | ||
20 | * The CDATA value. | ||
21 | * | ||
22 | * @property {String} | ||
23 | */ | ||
24 | this.value = value; | ||
25 | }; | ||
26 | |||
27 | CKEDITOR.htmlParser.cdata.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), { | ||
28 | /** | ||
29 | * CDATA has the same type as {@link CKEDITOR.htmlParser.text} This is | ||
30 | * a constant value set to {@link CKEDITOR#NODE_TEXT}. | ||
31 | * | ||
32 | * @readonly | ||
33 | * @property {Number} [=CKEDITOR.NODE_TEXT] | ||
34 | */ | ||
35 | type: CKEDITOR.NODE_TEXT, | ||
36 | |||
37 | filter: function() {}, | ||
38 | |||
39 | /** | ||
40 | * Writes the CDATA with no special manipulations. | ||
41 | * | ||
42 | * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. | ||
43 | */ | ||
44 | writeHtml: function( writer ) { | ||
45 | writer.write( this.value ); | ||
46 | } | ||
47 | } ); | ||
48 | } )(); | ||
diff --git a/sources/core/htmlparser/comment.js b/sources/core/htmlparser/comment.js new file mode 100644 index 0000000..171c62e --- /dev/null +++ b/sources/core/htmlparser/comment.js | |||
@@ -0,0 +1,80 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | 'use strict'; | ||
7 | |||
8 | /** | ||
9 | * A lightweight representation of an HTML comment. | ||
10 | * | ||
11 | * @class | ||
12 | * @extends CKEDITOR.htmlParser.node | ||
13 | * @constructor Creates a comment class instance. | ||
14 | * @param {String} value The comment text value. | ||
15 | */ | ||
16 | CKEDITOR.htmlParser.comment = function( value ) { | ||
17 | /** | ||
18 | * The comment text. | ||
19 | * | ||
20 | * @property {String} | ||
21 | */ | ||
22 | this.value = value; | ||
23 | |||
24 | /** @private */ | ||
25 | this._ = { | ||
26 | isBlockLike: false | ||
27 | }; | ||
28 | }; | ||
29 | |||
30 | CKEDITOR.htmlParser.comment.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), { | ||
31 | /** | ||
32 | * The node type. This is a constant value set to {@link CKEDITOR#NODE_COMMENT}. | ||
33 | * | ||
34 | * @readonly | ||
35 | * @property {Number} [=CKEDITOR.NODE_COMMENT] | ||
36 | */ | ||
37 | type: CKEDITOR.NODE_COMMENT, | ||
38 | |||
39 | /** | ||
40 | * Filter this comment with given filter. | ||
41 | * | ||
42 | * @since 4.1 | ||
43 | * @param {CKEDITOR.htmlParser.filter} filter | ||
44 | * @returns {Boolean} Method returns `false` when this comment has | ||
45 | * been removed or replaced with other node. This is an information for | ||
46 | * {@link CKEDITOR.htmlParser.element#filterChildren} that it has | ||
47 | * to repeat filter on current position in parent's children array. | ||
48 | */ | ||
49 | filter: function( filter, context ) { | ||
50 | var comment = this.value; | ||
51 | |||
52 | if ( !( comment = filter.onComment( context, comment, this ) ) ) { | ||
53 | this.remove(); | ||
54 | return false; | ||
55 | } | ||
56 | |||
57 | if ( typeof comment != 'string' ) { | ||
58 | this.replaceWith( comment ); | ||
59 | return false; | ||
60 | } | ||
61 | |||
62 | this.value = comment; | ||
63 | |||
64 | return true; | ||
65 | }, | ||
66 | |||
67 | /** | ||
68 | * Writes the HTML representation of this comment to a CKEDITOR.htmlWriter. | ||
69 | * | ||
70 | * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. | ||
71 | * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node. | ||
72 | * **Note:** it's unsafe to filter offline (not appended) node. | ||
73 | */ | ||
74 | writeHtml: function( writer, filter ) { | ||
75 | if ( filter ) | ||
76 | this.filter( filter ); | ||
77 | |||
78 | writer.comment( this.value ); | ||
79 | } | ||
80 | } ); | ||
diff --git a/sources/core/htmlparser/element.js b/sources/core/htmlparser/element.js new file mode 100644 index 0000000..3654322 --- /dev/null +++ b/sources/core/htmlparser/element.js | |||
@@ -0,0 +1,536 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | 'use strict'; | ||
7 | |||
8 | /** | ||
9 | * A lightweight representation of an HTML element. | ||
10 | * | ||
11 | * @class | ||
12 | * @extends CKEDITOR.htmlParser.node | ||
13 | * @constructor Creates an element class instance. | ||
14 | * @param {String} name The element name. | ||
15 | * @param {Object} attributes An object storing all attributes defined for | ||
16 | * this element. | ||
17 | */ | ||
18 | CKEDITOR.htmlParser.element = function( name, attributes ) { | ||
19 | /** | ||
20 | * The element name. | ||
21 | * | ||
22 | * @property {String} | ||
23 | */ | ||
24 | this.name = name; | ||
25 | |||
26 | /** | ||
27 | * Stores the attributes defined for this element. | ||
28 | * | ||
29 | * @property {Object} | ||
30 | */ | ||
31 | this.attributes = attributes || {}; | ||
32 | |||
33 | /** | ||
34 | * The nodes that are direct children of this element. | ||
35 | */ | ||
36 | this.children = []; | ||
37 | |||
38 | // Reveal the real semantic of our internal custom tag name (#6639), | ||
39 | // when resolving whether it's block like. | ||
40 | var realName = name || '', | ||
41 | prefixed = realName.match( /^cke:(.*)/ ); | ||
42 | prefixed && ( realName = prefixed[ 1 ] ); | ||
43 | |||
44 | var isBlockLike = !!( CKEDITOR.dtd.$nonBodyContent[ realName ] || CKEDITOR.dtd.$block[ realName ] || | ||
45 | CKEDITOR.dtd.$listItem[ realName ] || CKEDITOR.dtd.$tableContent[ realName ] || | ||
46 | CKEDITOR.dtd.$nonEditable[ realName ] || realName == 'br' ); | ||
47 | |||
48 | this.isEmpty = !!CKEDITOR.dtd.$empty[ name ]; | ||
49 | this.isUnknown = !CKEDITOR.dtd[ name ]; | ||
50 | |||
51 | /** @private */ | ||
52 | this._ = { | ||
53 | isBlockLike: isBlockLike, | ||
54 | hasInlineStarted: this.isEmpty || !isBlockLike | ||
55 | }; | ||
56 | }; | ||
57 | |||
58 | /** | ||
59 | * Object presentation of CSS style declaration text. | ||
60 | * | ||
61 | * @class | ||
62 | * @constructor Creates a `cssStyle` class instance. | ||
63 | * @param {CKEDITOR.htmlParser.element/String} elementOrStyleText | ||
64 | * An HTML parser element or the inline style text. | ||
65 | */ | ||
66 | CKEDITOR.htmlParser.cssStyle = function() { | ||
67 | var styleText, | ||
68 | arg = arguments[ 0 ], | ||
69 | rules = {}; | ||
70 | |||
71 | styleText = arg instanceof CKEDITOR.htmlParser.element ? arg.attributes.style : arg; | ||
72 | |||
73 | // html-encoded quote might be introduced by 'font-family' | ||
74 | // from MS-Word which confused the following regexp. e.g. | ||
75 | //'font-family: "Lucida, Console"' | ||
76 | // TODO reuse CSS methods from tools. | ||
77 | ( styleText || '' ).replace( /"/g, '"' ).replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value ) { | ||
78 | name == 'font-family' && ( value = value.replace( /["']/g, '' ) ); | ||
79 | rules[ name.toLowerCase() ] = value; | ||
80 | } ); | ||
81 | |||
82 | return { | ||
83 | |||
84 | rules: rules, | ||
85 | |||
86 | /** | ||
87 | * Applies the styles to the specified element or object. | ||
88 | * | ||
89 | * @param {CKEDITOR.htmlParser.element/CKEDITOR.dom.element/Object} obj | ||
90 | */ | ||
91 | populate: function( obj ) { | ||
92 | var style = this.toString(); | ||
93 | if ( style ) | ||
94 | obj instanceof CKEDITOR.dom.element ? obj.setAttribute( 'style', style ) : obj instanceof CKEDITOR.htmlParser.element ? obj.attributes.style = style : obj.style = style; | ||
95 | |||
96 | }, | ||
97 | |||
98 | /** | ||
99 | * Serializes CSS style declaration to a string. | ||
100 | * | ||
101 | * @returns {String} | ||
102 | */ | ||
103 | toString: function() { | ||
104 | var output = []; | ||
105 | for ( var i in rules ) | ||
106 | rules[ i ] && output.push( i, ':', rules[ i ], ';' ); | ||
107 | return output.join( '' ); | ||
108 | } | ||
109 | }; | ||
110 | }; | ||
111 | |||
112 | /** @class CKEDITOR.htmlParser.element */ | ||
113 | ( function() { | ||
114 | // Used to sort attribute entries in an array, where the first element of | ||
115 | // each object is the attribute name. | ||
116 | var sortAttribs = function( a, b ) { | ||
117 | a = a[ 0 ]; | ||
118 | b = b[ 0 ]; | ||
119 | return a < b ? -1 : a > b ? 1 : 0; | ||
120 | }, | ||
121 | fragProto = CKEDITOR.htmlParser.fragment.prototype; | ||
122 | |||
123 | CKEDITOR.htmlParser.element.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), { | ||
124 | /** | ||
125 | * The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}. | ||
126 | * | ||
127 | * @readonly | ||
128 | * @property {Number} [=CKEDITOR.NODE_ELEMENT] | ||
129 | */ | ||
130 | type: CKEDITOR.NODE_ELEMENT, | ||
131 | |||
132 | /** | ||
133 | * Adds a node to the element children list. | ||
134 | * | ||
135 | * @method | ||
136 | * @param {CKEDITOR.htmlParser.node} node The node to be added. | ||
137 | * @param {Number} [index] From where the insertion happens. | ||
138 | */ | ||
139 | add: fragProto.add, | ||
140 | |||
141 | /** | ||
142 | * Clones this element. | ||
143 | * | ||
144 | * @returns {CKEDITOR.htmlParser.element} The element clone. | ||
145 | */ | ||
146 | clone: function() { | ||
147 | return new CKEDITOR.htmlParser.element( this.name, this.attributes ); | ||
148 | }, | ||
149 | |||
150 | /** | ||
151 | * Filters this element and its children with the given filter. | ||
152 | * | ||
153 | * @since 4.1 | ||
154 | * @param {CKEDITOR.htmlParser.filter} filter | ||
155 | * @returns {Boolean} The method returns `false` when this element has | ||
156 | * been removed or replaced with another. This information means that | ||
157 | * {@link #filterChildren} has to repeat the filter on the current | ||
158 | * position in parent's children array. | ||
159 | */ | ||
160 | filter: function( filter, context ) { | ||
161 | var element = this, | ||
162 | originalName, name; | ||
163 | |||
164 | context = element.getFilterContext( context ); | ||
165 | |||
166 | // Do not process elements with data-cke-processor attribute set to off. | ||
167 | if ( context.off ) | ||
168 | return true; | ||
169 | |||
170 | // Filtering if it's the root node. | ||
171 | if ( !element.parent ) | ||
172 | filter.onRoot( context, element ); | ||
173 | |||
174 | while ( true ) { | ||
175 | originalName = element.name; | ||
176 | |||
177 | if ( !( name = filter.onElementName( context, originalName ) ) ) { | ||
178 | this.remove(); | ||
179 | return false; | ||
180 | } | ||
181 | |||
182 | element.name = name; | ||
183 | |||
184 | if ( !( element = filter.onElement( context, element ) ) ) { | ||
185 | this.remove(); | ||
186 | return false; | ||
187 | } | ||
188 | |||
189 | // New element has been returned - replace current one | ||
190 | // and process it (stop processing this and return false, what | ||
191 | // means that element has been removed). | ||
192 | if ( element !== this ) { | ||
193 | this.replaceWith( element ); | ||
194 | return false; | ||
195 | } | ||
196 | |||
197 | // If name has been changed - continue loop, so in next iteration | ||
198 | // filters for new name will be applied to this element. | ||
199 | // If name hasn't been changed - stop. | ||
200 | if ( element.name == originalName ) | ||
201 | break; | ||
202 | |||
203 | // If element has been replaced with something of a | ||
204 | // different type, then make the replacement filter itself. | ||
205 | if ( element.type != CKEDITOR.NODE_ELEMENT ) { | ||
206 | this.replaceWith( element ); | ||
207 | return false; | ||
208 | } | ||
209 | |||
210 | // This indicate that the element has been dropped by | ||
211 | // filter but not the children. | ||
212 | if ( !element.name ) { | ||
213 | this.replaceWithChildren(); | ||
214 | return false; | ||
215 | } | ||
216 | } | ||
217 | |||
218 | var attributes = element.attributes, | ||
219 | a, value, newAttrName; | ||
220 | |||
221 | for ( a in attributes ) { | ||
222 | newAttrName = a; | ||
223 | value = attributes[ a ]; | ||
224 | |||
225 | // Loop until name isn't modified. | ||
226 | // A little bit senseless, but IE would do that anyway | ||
227 | // because it iterates with for-in loop even over properties | ||
228 | // created during its run. | ||
229 | while ( true ) { | ||
230 | if ( !( newAttrName = filter.onAttributeName( context, a ) ) ) { | ||
231 | delete attributes[ a ]; | ||
232 | break; | ||
233 | } else if ( newAttrName != a ) { | ||
234 | delete attributes[ a ]; | ||
235 | a = newAttrName; | ||
236 | continue; | ||
237 | } else { | ||
238 | break; | ||
239 | } | ||
240 | } | ||
241 | |||
242 | if ( newAttrName ) { | ||
243 | if ( ( value = filter.onAttribute( context, element, newAttrName, value ) ) === false ) | ||
244 | delete attributes[ newAttrName ]; | ||
245 | else | ||
246 | attributes[ newAttrName ] = value; | ||
247 | } | ||
248 | } | ||
249 | |||
250 | if ( !element.isEmpty ) | ||
251 | this.filterChildren( filter, false, context ); | ||
252 | |||
253 | return true; | ||
254 | }, | ||
255 | |||
256 | /** | ||
257 | * Filters this element's children with the given filter. | ||
258 | * | ||
259 | * Element's children may only be filtered once by one | ||
260 | * instance of the filter. | ||
261 | * | ||
262 | * @method filterChildren | ||
263 | * @param {CKEDITOR.htmlParser.filter} filter | ||
264 | */ | ||
265 | filterChildren: fragProto.filterChildren, | ||
266 | |||
267 | /** | ||
268 | * Writes the element HTML to the CKEDITOR.htmlWriter. | ||
269 | * | ||
270 | * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which HTML will be written. | ||
271 | * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node. | ||
272 | * **Note:** It is unsafe to filter an offline (not appended) node. | ||
273 | */ | ||
274 | writeHtml: function( writer, filter ) { | ||
275 | if ( filter ) | ||
276 | this.filter( filter ); | ||
277 | |||
278 | var name = this.name, | ||
279 | attribsArray = [], | ||
280 | attributes = this.attributes, | ||
281 | attrName, | ||
282 | attr, i, l; | ||
283 | |||
284 | // Open element tag. | ||
285 | writer.openTag( name, attributes ); | ||
286 | |||
287 | // Copy all attributes to an array. | ||
288 | for ( attrName in attributes ) | ||
289 | attribsArray.push( [ attrName, attributes[ attrName ] ] ); | ||
290 | |||
291 | // Sort the attributes by name. | ||
292 | if ( writer.sortAttributes ) | ||
293 | attribsArray.sort( sortAttribs ); | ||
294 | |||
295 | // Send the attributes. | ||
296 | for ( i = 0, l = attribsArray.length; i < l; i++ ) { | ||
297 | attr = attribsArray[ i ]; | ||
298 | writer.attribute( attr[ 0 ], attr[ 1 ] ); | ||
299 | } | ||
300 | |||
301 | // Close the tag. | ||
302 | writer.openTagClose( name, this.isEmpty ); | ||
303 | |||
304 | this.writeChildrenHtml( writer ); | ||
305 | |||
306 | // Close the element. | ||
307 | if ( !this.isEmpty ) | ||
308 | writer.closeTag( name ); | ||
309 | }, | ||
310 | |||
311 | /** | ||
312 | * Sends children of this element to the writer. | ||
313 | * | ||
314 | * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which HTML will be written. | ||
315 | * @param {CKEDITOR.htmlParser.filter} [filter] | ||
316 | */ | ||
317 | writeChildrenHtml: fragProto.writeChildrenHtml, | ||
318 | |||
319 | /** | ||
320 | * Replaces this element with its children. | ||
321 | * | ||
322 | * @since 4.1 | ||
323 | */ | ||
324 | replaceWithChildren: function() { | ||
325 | var children = this.children; | ||
326 | |||
327 | for ( var i = children.length; i; ) | ||
328 | children[ --i ].insertAfter( this ); | ||
329 | |||
330 | this.remove(); | ||
331 | }, | ||
332 | |||
333 | /** | ||
334 | * Executes a callback on each node (of the given type) in this element. | ||
335 | * | ||
336 | * // Create a <p> element with foo<b>bar</b>bom as its content. | ||
337 | * var elP = CKEDITOR.htmlParser.fragment.fromHtml( 'foo<b>bar</b>bom', 'p' ); | ||
338 | * elP.forEach( function( node ) { | ||
339 | * console.log( node ); | ||
340 | * } ); | ||
341 | * // Will log: | ||
342 | * // 1. document fragment, | ||
343 | * // 2. <p> element, | ||
344 | * // 3. "foo" text node, | ||
345 | * // 4. <b> element, | ||
346 | * // 5. "bar" text node, | ||
347 | * // 6. "bom" text node. | ||
348 | * | ||
349 | * @since 4.1 | ||
350 | * @param {Function} callback Function to be executed on every node. | ||
351 | * **Since 4.3**: If `callback` returned `false`, the descendants of the current node will be ignored. | ||
352 | * @param {CKEDITOR.htmlParser.node} callback.node Node passed as an argument. | ||
353 | * @param {Number} [type] Whether the specified `callback` will be executed only on nodes of this type. | ||
354 | * @param {Boolean} [skipRoot] Do not execute `callback` on this element. | ||
355 | */ | ||
356 | forEach: fragProto.forEach, | ||
357 | |||
358 | /** | ||
359 | * Gets this element's first child. If `condition` is given, this method returns | ||
360 | * the first child which satisfies that condition. | ||
361 | * | ||
362 | * @since 4.3 | ||
363 | * @param {String/Object/Function} condition Name of a child, a hash of names, or a validator function. | ||
364 | * @returns {CKEDITOR.htmlParser.node} | ||
365 | */ | ||
366 | getFirst: function( condition ) { | ||
367 | if ( !condition ) | ||
368 | return this.children.length ? this.children[ 0 ] : null; | ||
369 | |||
370 | if ( typeof condition != 'function' ) | ||
371 | condition = nameCondition( condition ); | ||
372 | |||
373 | for ( var i = 0, l = this.children.length; i < l; ++i ) { | ||
374 | if ( condition( this.children[ i ] ) ) | ||
375 | return this.children[ i ]; | ||
376 | } | ||
377 | return null; | ||
378 | }, | ||
379 | |||
380 | /** | ||
381 | * Gets this element's inner HTML. | ||
382 | * | ||
383 | * @since 4.3 | ||
384 | * @returns {String} | ||
385 | */ | ||
386 | getHtml: function() { | ||
387 | var writer = new CKEDITOR.htmlParser.basicWriter(); | ||
388 | this.writeChildrenHtml( writer ); | ||
389 | return writer.getHtml(); | ||
390 | }, | ||
391 | |||
392 | /** | ||
393 | * Sets this element's inner HTML. | ||
394 | * | ||
395 | * @since 4.3 | ||
396 | * @param {String} html | ||
397 | */ | ||
398 | setHtml: function( html ) { | ||
399 | var children = this.children = CKEDITOR.htmlParser.fragment.fromHtml( html ).children; | ||
400 | |||
401 | for ( var i = 0, l = children.length; i < l; ++i ) | ||
402 | children[ i ].parent = this; | ||
403 | }, | ||
404 | |||
405 | /** | ||
406 | * Gets this element's outer HTML. | ||
407 | * | ||
408 | * @since 4.3 | ||
409 | * @returns {String} | ||
410 | */ | ||
411 | getOuterHtml: function() { | ||
412 | var writer = new CKEDITOR.htmlParser.basicWriter(); | ||
413 | this.writeHtml( writer ); | ||
414 | return writer.getHtml(); | ||
415 | }, | ||
416 | |||
417 | /** | ||
418 | * Splits this element at the given index. | ||
419 | * | ||
420 | * @since 4.3 | ||
421 | * @param {Number} index Index at which the element will be split — `0` means the beginning, | ||
422 | * `1` after first child node, etc. | ||
423 | * @returns {CKEDITOR.htmlParser.element} The new element following this one. | ||
424 | */ | ||
425 | split: function( index ) { | ||
426 | var cloneChildren = this.children.splice( index, this.children.length - index ), | ||
427 | clone = this.clone(); | ||
428 | |||
429 | for ( var i = 0; i < cloneChildren.length; ++i ) | ||
430 | cloneChildren[ i ].parent = clone; | ||
431 | |||
432 | clone.children = cloneChildren; | ||
433 | |||
434 | if ( cloneChildren[ 0 ] ) | ||
435 | cloneChildren[ 0 ].previous = null; | ||
436 | |||
437 | if ( index > 0 ) | ||
438 | this.children[ index - 1 ].next = null; | ||
439 | |||
440 | this.parent.add( clone, this.getIndex() + 1 ); | ||
441 | |||
442 | return clone; | ||
443 | }, | ||
444 | |||
445 | /** | ||
446 | * Adds a class name to the list of classes. | ||
447 | * | ||
448 | * @since 4.4 | ||
449 | * @param {String} className The class name to be added. | ||
450 | */ | ||
451 | addClass: function( className ) { | ||
452 | if ( this.hasClass( className ) ) | ||
453 | return; | ||
454 | |||
455 | var c = this.attributes[ 'class' ] || ''; | ||
456 | |||
457 | this.attributes[ 'class' ] = c + ( c ? ' ' : '' ) + className; | ||
458 | }, | ||
459 | |||
460 | /** | ||
461 | * Removes a class name from the list of classes. | ||
462 | * | ||
463 | * @since 4.3 | ||
464 | * @param {String} className The class name to be removed. | ||
465 | */ | ||
466 | removeClass: function( className ) { | ||
467 | var classes = this.attributes[ 'class' ]; | ||
468 | |||
469 | if ( !classes ) | ||
470 | return; | ||
471 | |||
472 | // We can safely assume that className won't break regexp. | ||
473 | // http://stackoverflow.com/questions/448981/what-characters-are-valid-in-css-class-names | ||
474 | classes = CKEDITOR.tools.trim( classes.replace( new RegExp( '(?:\\s+|^)' + className + '(?:\\s+|$)' ), ' ' ) ); | ||
475 | |||
476 | if ( classes ) | ||
477 | this.attributes[ 'class' ] = classes; | ||
478 | else | ||
479 | delete this.attributes[ 'class' ]; | ||
480 | }, | ||
481 | |||
482 | /** | ||
483 | * Checkes whether this element has a class name. | ||
484 | * | ||
485 | * @since 4.3 | ||
486 | * @param {String} className The class name to be checked. | ||
487 | * @returns {Boolean} Whether this element has a `className`. | ||
488 | */ | ||
489 | hasClass: function( className ) { | ||
490 | var classes = this.attributes[ 'class' ]; | ||
491 | |||
492 | if ( !classes ) | ||
493 | return false; | ||
494 | |||
495 | return ( new RegExp( '(?:^|\\s)' + className + '(?=\\s|$)' ) ).test( classes ); | ||
496 | }, | ||
497 | |||
498 | getFilterContext: function( ctx ) { | ||
499 | var changes = []; | ||
500 | |||
501 | if ( !ctx ) { | ||
502 | ctx = { | ||
503 | off: false, | ||
504 | nonEditable: false, | ||
505 | nestedEditable: false | ||
506 | }; | ||
507 | } | ||
508 | |||
509 | if ( !ctx.off && this.attributes[ 'data-cke-processor' ] == 'off' ) | ||
510 | changes.push( 'off', true ); | ||
511 | |||
512 | if ( !ctx.nonEditable && this.attributes.contenteditable == 'false' ) | ||
513 | changes.push( 'nonEditable', true ); | ||
514 | // A context to be given nestedEditable must be nonEditable first (by inheritance) (#11372, #11698). | ||
515 | // Special case: #11504 - filter starts on <body contenteditable=true>, | ||
516 | // so ctx.nonEditable has not been yet set to true. | ||
517 | else if ( ctx.nonEditable && !ctx.nestedEditable && this.attributes.contenteditable == 'true' ) | ||
518 | changes.push( 'nestedEditable', true ); | ||
519 | |||
520 | if ( changes.length ) { | ||
521 | ctx = CKEDITOR.tools.copy( ctx ); | ||
522 | for ( var i = 0; i < changes.length; i += 2 ) | ||
523 | ctx[ changes[ i ] ] = changes[ i + 1 ]; | ||
524 | } | ||
525 | |||
526 | return ctx; | ||
527 | } | ||
528 | }, true ); | ||
529 | |||
530 | function nameCondition( condition ) { | ||
531 | return function( el ) { | ||
532 | return el.type == CKEDITOR.NODE_ELEMENT && | ||
533 | ( typeof condition == 'string' ? el.name == condition : el.name in condition ); | ||
534 | }; | ||
535 | } | ||
536 | } )(); | ||
diff --git a/sources/core/htmlparser/filter.js b/sources/core/htmlparser/filter.js new file mode 100644 index 0000000..72767b5 --- /dev/null +++ b/sources/core/htmlparser/filter.js | |||
@@ -0,0 +1,407 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 | * Filter is a configurable tool for transforming and filtering {@link CKEDITOR.htmlParser.node nodes}. | ||
11 | * It is mainly used during data processing phase which is done not on real DOM nodes, | ||
12 | * but on their simplified form represented by {@link CKEDITOR.htmlParser.node} class and its subclasses. | ||
13 | * | ||
14 | * var filter = new CKEDITOR.htmlParser.filter( { | ||
15 | * text: function( value ) { | ||
16 | * return '@' + value + '@'; | ||
17 | * }, | ||
18 | * elements: { | ||
19 | * p: function( element ) { | ||
20 | * element.attributes.foo = '1'; | ||
21 | * } | ||
22 | * } | ||
23 | * } ); | ||
24 | * | ||
25 | * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p>Foo<b>bar!</b></p>' ), | ||
26 | * writer = new CKEDITOR.htmlParser.basicWriter(); | ||
27 | * filter.applyTo( fragment ); | ||
28 | * fragment.writeHtml( writer ); | ||
29 | * writer.getHtml(); // '<p foo="1">@Foo@<b>@bar!@</b></p>' | ||
30 | * | ||
31 | * @class | ||
32 | */ | ||
33 | CKEDITOR.htmlParser.filter = CKEDITOR.tools.createClass( { | ||
34 | /** | ||
35 | * @constructor Creates a filter class instance. | ||
36 | * @param {CKEDITOR.htmlParser.filterRulesDefinition} [rules] | ||
37 | */ | ||
38 | $: function( rules ) { | ||
39 | /** | ||
40 | * ID of filter instance, which is used to mark elements | ||
41 | * to which this filter has been already applied. | ||
42 | * | ||
43 | * @property {Number} id | ||
44 | * @readonly | ||
45 | */ | ||
46 | this.id = CKEDITOR.tools.getNextNumber(); | ||
47 | |||
48 | /** | ||
49 | * Rules for element names. | ||
50 | * | ||
51 | * @property {CKEDITOR.htmlParser.filterRulesGroup} | ||
52 | * @readonly | ||
53 | */ | ||
54 | this.elementNameRules = new filterRulesGroup(); | ||
55 | |||
56 | /** | ||
57 | * Rules for attribute names. | ||
58 | * | ||
59 | * @property {CKEDITOR.htmlParser.filterRulesGroup} | ||
60 | * @readonly | ||
61 | */ | ||
62 | this.attributeNameRules = new filterRulesGroup(); | ||
63 | |||
64 | /** | ||
65 | * Hash of elementName => {@link CKEDITOR.htmlParser.filterRulesGroup rules for elements}. | ||
66 | * | ||
67 | * @readonly | ||
68 | */ | ||
69 | this.elementsRules = {}; | ||
70 | |||
71 | /** | ||
72 | * Hash of attributeName => {@link CKEDITOR.htmlParser.filterRulesGroup rules for attributes}. | ||
73 | * | ||
74 | * @readonly | ||
75 | */ | ||
76 | this.attributesRules = {}; | ||
77 | |||
78 | /** | ||
79 | * Rules for text nodes. | ||
80 | * | ||
81 | * @property {CKEDITOR.htmlParser.filterRulesGroup} | ||
82 | * @readonly | ||
83 | */ | ||
84 | this.textRules = new filterRulesGroup(); | ||
85 | |||
86 | /** | ||
87 | * Rules for comment nodes. | ||
88 | * | ||
89 | * @property {CKEDITOR.htmlParser.filterRulesGroup} | ||
90 | * @readonly | ||
91 | */ | ||
92 | this.commentRules = new filterRulesGroup(); | ||
93 | |||
94 | /** | ||
95 | * Rules for a root node. | ||
96 | * | ||
97 | * @property {CKEDITOR.htmlParser.filterRulesGroup} | ||
98 | * @readonly | ||
99 | */ | ||
100 | this.rootRules = new filterRulesGroup(); | ||
101 | |||
102 | if ( rules ) | ||
103 | this.addRules( rules, 10 ); | ||
104 | }, | ||
105 | |||
106 | proto: { | ||
107 | /** | ||
108 | * Add rules to this filter. | ||
109 | * | ||
110 | * @param {CKEDITOR.htmlParser.filterRulesDefinition} rules Object containing filter rules. | ||
111 | * @param {Object/Number} [options] Object containing rules' options or a priority | ||
112 | * (for a backward compatibility with CKEditor versions up to 4.2.x). | ||
113 | * @param {Number} [options.priority=10] The priority of a rule. | ||
114 | * @param {Boolean} [options.applyToAll=false] Whether to apply rule to non-editable | ||
115 | * elements and their descendants too. | ||
116 | */ | ||
117 | addRules: function( rules, options ) { | ||
118 | var priority; | ||
119 | |||
120 | // Backward compatibility. | ||
121 | if ( typeof options == 'number' ) | ||
122 | priority = options; | ||
123 | // New version - try reading from options. | ||
124 | else if ( options && ( 'priority' in options ) ) | ||
125 | priority = options.priority; | ||
126 | |||
127 | // Defaults. | ||
128 | if ( typeof priority != 'number' ) | ||
129 | priority = 10; | ||
130 | if ( typeof options != 'object' ) | ||
131 | options = {}; | ||
132 | |||
133 | // Add the elementNames. | ||
134 | if ( rules.elementNames ) | ||
135 | this.elementNameRules.addMany( rules.elementNames, priority, options ); | ||
136 | |||
137 | // Add the attributeNames. | ||
138 | if ( rules.attributeNames ) | ||
139 | this.attributeNameRules.addMany( rules.attributeNames, priority, options ); | ||
140 | |||
141 | // Add the elements. | ||
142 | if ( rules.elements ) | ||
143 | addNamedRules( this.elementsRules, rules.elements, priority, options ); | ||
144 | |||
145 | // Add the attributes. | ||
146 | if ( rules.attributes ) | ||
147 | addNamedRules( this.attributesRules, rules.attributes, priority, options ); | ||
148 | |||
149 | // Add the text. | ||
150 | if ( rules.text ) | ||
151 | this.textRules.add( rules.text, priority, options ); | ||
152 | |||
153 | // Add the comment. | ||
154 | if ( rules.comment ) | ||
155 | this.commentRules.add( rules.comment, priority, options ); | ||
156 | |||
157 | // Add root node rules. | ||
158 | if ( rules.root ) | ||
159 | this.rootRules.add( rules.root, priority, options ); | ||
160 | }, | ||
161 | |||
162 | /** | ||
163 | * Apply this filter to given node. | ||
164 | * | ||
165 | * @param {CKEDITOR.htmlParser.node} node The node to be filtered. | ||
166 | */ | ||
167 | applyTo: function( node ) { | ||
168 | node.filter( this ); | ||
169 | }, | ||
170 | |||
171 | onElementName: function( context, name ) { | ||
172 | return this.elementNameRules.execOnName( context, name ); | ||
173 | }, | ||
174 | |||
175 | onAttributeName: function( context, name ) { | ||
176 | return this.attributeNameRules.execOnName( context, name ); | ||
177 | }, | ||
178 | |||
179 | onText: function( context, text, node ) { | ||
180 | return this.textRules.exec( context, text, node ); | ||
181 | }, | ||
182 | |||
183 | onComment: function( context, commentText, comment ) { | ||
184 | return this.commentRules.exec( context, commentText, comment ); | ||
185 | }, | ||
186 | |||
187 | onRoot: function( context, element ) { | ||
188 | return this.rootRules.exec( context, element ); | ||
189 | }, | ||
190 | |||
191 | onElement: function( context, element ) { | ||
192 | // We must apply filters set to the specific element name as | ||
193 | // well as those set to the generic ^/$ name. So, add both to an | ||
194 | // array and process them in a small loop. | ||
195 | var rulesGroups = [ this.elementsRules[ '^' ], this.elementsRules[ element.name ], this.elementsRules.$ ], | ||
196 | rulesGroup, ret; | ||
197 | |||
198 | for ( var i = 0; i < 3; i++ ) { | ||
199 | rulesGroup = rulesGroups[ i ]; | ||
200 | if ( rulesGroup ) { | ||
201 | ret = rulesGroup.exec( context, element, this ); | ||
202 | |||
203 | if ( ret === false ) | ||
204 | return null; | ||
205 | |||
206 | if ( ret && ret != element ) | ||
207 | return this.onNode( context, ret ); | ||
208 | |||
209 | // The non-root element has been dismissed by one of the filters. | ||
210 | if ( element.parent && !element.name ) | ||
211 | break; | ||
212 | } | ||
213 | } | ||
214 | |||
215 | return element; | ||
216 | }, | ||
217 | |||
218 | onNode: function( context, node ) { | ||
219 | var type = node.type; | ||
220 | |||
221 | return type == CKEDITOR.NODE_ELEMENT ? this.onElement( context, node ) : | ||
222 | type == CKEDITOR.NODE_TEXT ? new CKEDITOR.htmlParser.text( this.onText( context, node.value ) ) : | ||
223 | type == CKEDITOR.NODE_COMMENT ? new CKEDITOR.htmlParser.comment( this.onComment( context, node.value ) ) : null; | ||
224 | }, | ||
225 | |||
226 | onAttribute: function( context, element, name, value ) { | ||
227 | var rulesGroup = this.attributesRules[ name ]; | ||
228 | |||
229 | if ( rulesGroup ) | ||
230 | return rulesGroup.exec( context, value, element, this ); | ||
231 | return value; | ||
232 | } | ||
233 | } | ||
234 | } ); | ||
235 | |||
236 | /** | ||
237 | * Class grouping filter rules for one subject (like element or attribute names). | ||
238 | * | ||
239 | * @class CKEDITOR.htmlParser.filterRulesGroup | ||
240 | */ | ||
241 | function filterRulesGroup() { | ||
242 | /** | ||
243 | * Array of objects containing rule, priority and options. | ||
244 | * | ||
245 | * @property {Object[]} | ||
246 | * @readonly | ||
247 | */ | ||
248 | this.rules = []; | ||
249 | } | ||
250 | |||
251 | CKEDITOR.htmlParser.filterRulesGroup = filterRulesGroup; | ||
252 | |||
253 | filterRulesGroup.prototype = { | ||
254 | /** | ||
255 | * Adds specified rule to this group. | ||
256 | * | ||
257 | * @param {Function/Array} rule Function for function based rule or [ pattern, replacement ] array for | ||
258 | * rule applicable to names. | ||
259 | * @param {Number} priority | ||
260 | * @param options | ||
261 | */ | ||
262 | add: function( rule, priority, options ) { | ||
263 | this.rules.splice( this.findIndex( priority ), 0, { | ||
264 | value: rule, | ||
265 | priority: priority, | ||
266 | options: options | ||
267 | } ); | ||
268 | }, | ||
269 | |||
270 | /** | ||
271 | * Adds specified rules to this group. | ||
272 | * | ||
273 | * @param {Array} rules Array of rules - see {@link #add}. | ||
274 | * @param {Number} priority | ||
275 | * @param options | ||
276 | */ | ||
277 | addMany: function( rules, priority, options ) { | ||
278 | var args = [ this.findIndex( priority ), 0 ]; | ||
279 | |||
280 | for ( var i = 0, len = rules.length; i < len; i++ ) { | ||
281 | args.push( { | ||
282 | value: rules[ i ], | ||
283 | priority: priority, | ||
284 | options: options | ||
285 | } ); | ||
286 | } | ||
287 | |||
288 | this.rules.splice.apply( this.rules, args ); | ||
289 | }, | ||
290 | |||
291 | /** | ||
292 | * Finds an index at which rule with given priority should be inserted. | ||
293 | * | ||
294 | * @param {Number} priority | ||
295 | * @returns {Number} Index. | ||
296 | */ | ||
297 | findIndex: function( priority ) { | ||
298 | var rules = this.rules, | ||
299 | len = rules.length, | ||
300 | i = len - 1; | ||
301 | |||
302 | // Search from the end, because usually rules will be added with default priority, so | ||
303 | // we will be able to stop loop quickly. | ||
304 | while ( i >= 0 && priority < rules[ i ].priority ) | ||
305 | i--; | ||
306 | |||
307 | return i + 1; | ||
308 | }, | ||
309 | |||
310 | /** | ||
311 | * Executes this rules group on given value. Applicable only if function based rules were added. | ||
312 | * | ||
313 | * All arguments passed to this function will be forwarded to rules' functions. | ||
314 | * | ||
315 | * @param {CKEDITOR.htmlParser.node/CKEDITOR.htmlParser.fragment/String} currentValue The value to be filtered. | ||
316 | * @returns {CKEDITOR.htmlParser.node/CKEDITOR.htmlParser.fragment/String} Filtered value. | ||
317 | */ | ||
318 | exec: function( context, currentValue ) { | ||
319 | var isNode = currentValue instanceof CKEDITOR.htmlParser.node || currentValue instanceof CKEDITOR.htmlParser.fragment, | ||
320 | // Splice '1' to remove context, which we don't want to pass to filter rules. | ||
321 | args = Array.prototype.slice.call( arguments, 1 ), | ||
322 | rules = this.rules, | ||
323 | len = rules.length, | ||
324 | orgType, orgName, ret, i, rule; | ||
325 | |||
326 | for ( i = 0; i < len; i++ ) { | ||
327 | // Backup the node info before filtering. | ||
328 | if ( isNode ) { | ||
329 | orgType = currentValue.type; | ||
330 | orgName = currentValue.name; | ||
331 | } | ||
332 | |||
333 | rule = rules[ i ]; | ||
334 | if ( isRuleApplicable( context, rule ) ) { | ||
335 | ret = rule.value.apply( null, args ); | ||
336 | |||
337 | if ( ret === false ) | ||
338 | return ret; | ||
339 | |||
340 | // We're filtering node (element/fragment). | ||
341 | // No further filtering if it's not anymore fitable for the subsequent filters. | ||
342 | if ( isNode && ret && ( ret.name != orgName || ret.type != orgType ) ) | ||
343 | return ret; | ||
344 | |||
345 | // Update currentValue and corresponding argument in args array. | ||
346 | // Updated values will be used in next for-loop step. | ||
347 | if ( ret != null ) | ||
348 | args[ 0 ] = currentValue = ret; | ||
349 | |||
350 | // ret == undefined will continue loop as nothing has happened. | ||
351 | } | ||
352 | } | ||
353 | |||
354 | return currentValue; | ||
355 | }, | ||
356 | |||
357 | /** | ||
358 | * Executes this rules group on name. Applicable only if filter rules for names were added. | ||
359 | * | ||
360 | * @param {String} currentName The name to be filtered. | ||
361 | * @returns {String} Filtered name. | ||
362 | */ | ||
363 | execOnName: function( context, currentName ) { | ||
364 | var i = 0, | ||
365 | rules = this.rules, | ||
366 | len = rules.length, | ||
367 | rule; | ||
368 | |||
369 | for ( ; currentName && i < len; i++ ) { | ||
370 | rule = rules[ i ]; | ||
371 | if ( isRuleApplicable( context, rule ) ) | ||
372 | currentName = currentName.replace( rule.value[ 0 ], rule.value[ 1 ] ); | ||
373 | } | ||
374 | |||
375 | return currentName; | ||
376 | } | ||
377 | }; | ||
378 | |||
379 | function addNamedRules( rulesGroups, newRules, priority, options ) { | ||
380 | var ruleName, rulesGroup; | ||
381 | |||
382 | for ( ruleName in newRules ) { | ||
383 | rulesGroup = rulesGroups[ ruleName ]; | ||
384 | |||
385 | if ( !rulesGroup ) | ||
386 | rulesGroup = rulesGroups[ ruleName ] = new filterRulesGroup(); | ||
387 | |||
388 | rulesGroup.add( newRules[ ruleName ], priority, options ); | ||
389 | } | ||
390 | } | ||
391 | |||
392 | function isRuleApplicable( context, rule ) { | ||
393 | if ( context.nonEditable && !rule.options.applyToAll ) | ||
394 | return false; | ||
395 | |||
396 | if ( context.nestedEditable && rule.options.excludeNestedEditable ) | ||
397 | return false; | ||
398 | |||
399 | return true; | ||
400 | } | ||
401 | |||
402 | } )(); | ||
403 | |||
404 | /** | ||
405 | * @class CKEDITOR.htmlParser.filterRulesDefinition | ||
406 | * @abstract | ||
407 | */ | ||
diff --git a/sources/core/htmlparser/fragment.js b/sources/core/htmlparser/fragment.js new file mode 100644 index 0000000..c062986 --- /dev/null +++ b/sources/core/htmlparser/fragment.js | |||
@@ -0,0 +1,646 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | 'use strict'; | ||
7 | |||
8 | /** | ||
9 | * A lightweight representation of an HTML DOM structure. | ||
10 | * | ||
11 | * @class | ||
12 | * @constructor Creates a fragment class instance. | ||
13 | */ | ||
14 | CKEDITOR.htmlParser.fragment = function() { | ||
15 | /** | ||
16 | * The nodes contained in the root of this fragment. | ||
17 | * | ||
18 | * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' ); | ||
19 | * alert( fragment.children.length ); // 2 | ||
20 | */ | ||
21 | this.children = []; | ||
22 | |||
23 | /** | ||
24 | * Get the fragment parent. Should always be null. | ||
25 | * | ||
26 | * @property {Object} [=null] | ||
27 | */ | ||
28 | this.parent = null; | ||
29 | |||
30 | /** @private */ | ||
31 | this._ = { | ||
32 | isBlockLike: true, | ||
33 | hasInlineStarted: false | ||
34 | }; | ||
35 | }; | ||
36 | |||
37 | ( function() { | ||
38 | // Block-level elements whose internal structure should be respected during | ||
39 | // parser fixing. | ||
40 | var nonBreakingBlocks = CKEDITOR.tools.extend( { table: 1, ul: 1, ol: 1, dl: 1 }, CKEDITOR.dtd.table, CKEDITOR.dtd.ul, CKEDITOR.dtd.ol, CKEDITOR.dtd.dl ); | ||
41 | |||
42 | var listBlocks = { ol: 1, ul: 1 }; | ||
43 | |||
44 | // Dtd of the fragment element, basically it accept anything except for intermediate structure, e.g. orphan <li>. | ||
45 | var rootDtd = CKEDITOR.tools.extend( {}, { html: 1 }, CKEDITOR.dtd.html, CKEDITOR.dtd.body, CKEDITOR.dtd.head, { style: 1, script: 1 } ); | ||
46 | |||
47 | // Which element to create when encountered not allowed content. | ||
48 | var structureFixes = { | ||
49 | ul: 'li', | ||
50 | ol: 'li', | ||
51 | dl: 'dd', | ||
52 | table: 'tbody', | ||
53 | tbody: 'tr', | ||
54 | thead: 'tr', | ||
55 | tfoot: 'tr', | ||
56 | tr: 'td' | ||
57 | }; | ||
58 | |||
59 | function isRemoveEmpty( node ) { | ||
60 | // Keep marked element event if it is empty. | ||
61 | if ( node.attributes[ 'data-cke-survive' ] ) | ||
62 | return false; | ||
63 | |||
64 | // Empty link is to be removed when empty but not anchor. (#7894) | ||
65 | return node.name == 'a' && node.attributes.href || CKEDITOR.dtd.$removeEmpty[ node.name ]; | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string. | ||
70 | * | ||
71 | * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' ); | ||
72 | * alert( fragment.children[ 0 ].name ); // 'b' | ||
73 | * alert( fragment.children[ 1 ].value ); // ' Text' | ||
74 | * | ||
75 | * @static | ||
76 | * @param {String} fragmentHtml The HTML to be parsed, filling the fragment. | ||
77 | * @param {CKEDITOR.htmlParser.element/String} [parent] Optional contextual | ||
78 | * element which makes the content been parsed as the content of this element and fix | ||
79 | * to match it. | ||
80 | * If not provided, then {@link CKEDITOR.htmlParser.fragment} will be used | ||
81 | * as the parent and it will be returned. | ||
82 | * @param {String/Boolean} [fixingBlock] When `parent` is a block limit element, | ||
83 | * and the param is a string value other than `false`, it is to | ||
84 | * avoid having block-less content as the direct children of parent by wrapping | ||
85 | * the content with a block element of the specified tag, e.g. | ||
86 | * when `fixingBlock` specified as `p`, the content `<body><i>foo</i></body>` | ||
87 | * will be fixed into `<body><p><i>foo</i></p></body>`. | ||
88 | * @returns {CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} The created fragment or passed `parent`. | ||
89 | */ | ||
90 | CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, parent, fixingBlock ) { | ||
91 | var parser = new CKEDITOR.htmlParser(); | ||
92 | |||
93 | var root = parent instanceof CKEDITOR.htmlParser.element ? parent : typeof parent == 'string' ? new CKEDITOR.htmlParser.element( parent ) : new CKEDITOR.htmlParser.fragment(); | ||
94 | |||
95 | var pendingInline = [], | ||
96 | pendingBRs = [], | ||
97 | currentNode = root, | ||
98 | // Indicate we're inside a <textarea> element, spaces should be touched differently. | ||
99 | inTextarea = root.name == 'textarea', | ||
100 | // Indicate we're inside a <pre> element, spaces should be touched differently. | ||
101 | inPre = root.name == 'pre'; | ||
102 | |||
103 | function checkPending( newTagName ) { | ||
104 | var pendingBRsSent; | ||
105 | |||
106 | if ( pendingInline.length > 0 ) { | ||
107 | for ( var i = 0; i < pendingInline.length; i++ ) { | ||
108 | var pendingElement = pendingInline[ i ], | ||
109 | pendingName = pendingElement.name, | ||
110 | pendingDtd = CKEDITOR.dtd[ pendingName ], | ||
111 | currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ]; | ||
112 | |||
113 | if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) ) { | ||
114 | if ( !pendingBRsSent ) { | ||
115 | sendPendingBRs(); | ||
116 | pendingBRsSent = 1; | ||
117 | } | ||
118 | |||
119 | // Get a clone for the pending element. | ||
120 | pendingElement = pendingElement.clone(); | ||
121 | |||
122 | // Add it to the current node and make it the current, | ||
123 | // so the new element will be added inside of it. | ||
124 | pendingElement.parent = currentNode; | ||
125 | currentNode = pendingElement; | ||
126 | |||
127 | // Remove the pending element (back the index by one | ||
128 | // to properly process the next entry). | ||
129 | pendingInline.splice( i, 1 ); | ||
130 | i--; | ||
131 | } else { | ||
132 | // Some element of the same type cannot be nested, flat them, | ||
133 | // e.g. <a href="#">foo<a href="#">bar</a></a>. (#7894) | ||
134 | if ( pendingName == currentNode.name ) | ||
135 | addElement( currentNode, currentNode.parent, 1 ), i--; | ||
136 | } | ||
137 | } | ||
138 | } | ||
139 | } | ||
140 | |||
141 | function sendPendingBRs() { | ||
142 | while ( pendingBRs.length ) | ||
143 | addElement( pendingBRs.shift(), currentNode ); | ||
144 | } | ||
145 | |||
146 | // Rtrim empty spaces on block end boundary. (#3585) | ||
147 | function removeTailWhitespace( element ) { | ||
148 | if ( element._.isBlockLike && element.name != 'pre' && element.name != 'textarea' ) { | ||
149 | |||
150 | var length = element.children.length, | ||
151 | lastChild = element.children[ length - 1 ], | ||
152 | text; | ||
153 | if ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT ) { | ||
154 | if ( !( text = CKEDITOR.tools.rtrim( lastChild.value ) ) ) | ||
155 | element.children.length = length - 1; | ||
156 | else | ||
157 | lastChild.value = text; | ||
158 | } | ||
159 | } | ||
160 | } | ||
161 | |||
162 | // Beside of simply append specified element to target, this function also takes | ||
163 | // care of other dirty lifts like forcing block in body, trimming spaces at | ||
164 | // the block boundaries etc. | ||
165 | // | ||
166 | // @param {Element} element The element to be added as the last child of {@link target}. | ||
167 | // @param {Element} target The parent element to relieve the new node. | ||
168 | // @param {Boolean} [moveCurrent=false] Don't change the "currentNode" global unless | ||
169 | // there's a return point node specified on the element, otherwise move current onto {@link target} node. | ||
170 | // | ||
171 | function addElement( element, target, moveCurrent ) { | ||
172 | target = target || currentNode || root; | ||
173 | |||
174 | // Current element might be mangled by fix body below, | ||
175 | // save it for restore later. | ||
176 | var savedCurrent = currentNode; | ||
177 | |||
178 | // Ignore any element that has already been added. | ||
179 | if ( element.previous === undefined ) { | ||
180 | if ( checkAutoParagraphing( target, element ) ) { | ||
181 | // Create a <p> in the fragment. | ||
182 | currentNode = target; | ||
183 | parser.onTagOpen( fixingBlock, {} ); | ||
184 | |||
185 | // The new target now is the <p>. | ||
186 | element.returnPoint = target = currentNode; | ||
187 | } | ||
188 | |||
189 | removeTailWhitespace( element ); | ||
190 | |||
191 | // Avoid adding empty inline. | ||
192 | if ( !( isRemoveEmpty( element ) && !element.children.length ) ) | ||
193 | target.add( element ); | ||
194 | |||
195 | if ( element.name == 'pre' ) | ||
196 | inPre = false; | ||
197 | |||
198 | if ( element.name == 'textarea' ) | ||
199 | inTextarea = false; | ||
200 | } | ||
201 | |||
202 | if ( element.returnPoint ) { | ||
203 | currentNode = element.returnPoint; | ||
204 | delete element.returnPoint; | ||
205 | } else { | ||
206 | currentNode = moveCurrent ? target : savedCurrent; | ||
207 | } | ||
208 | } | ||
209 | |||
210 | // Auto paragraphing should happen when inline content enters the root element. | ||
211 | function checkAutoParagraphing( parent, node ) { | ||
212 | |||
213 | // Check for parent that can contain block. | ||
214 | if ( ( parent == root || parent.name == 'body' ) && fixingBlock && | ||
215 | ( !parent.name || CKEDITOR.dtd[ parent.name ][ fixingBlock ] ) ) { | ||
216 | var name, realName; | ||
217 | |||
218 | if ( node.attributes && ( realName = node.attributes[ 'data-cke-real-element-type' ] ) ) | ||
219 | name = realName; | ||
220 | else | ||
221 | name = node.name; | ||
222 | |||
223 | // Text node, inline elements are subjected, except for <script>/<style>. | ||
224 | return name && name in CKEDITOR.dtd.$inline && | ||
225 | !( name in CKEDITOR.dtd.head ) && | ||
226 | !node.isOrphan || | ||
227 | node.type == CKEDITOR.NODE_TEXT; | ||
228 | } | ||
229 | } | ||
230 | |||
231 | // Judge whether two element tag names are likely the siblings from the same | ||
232 | // structural element. | ||
233 | function possiblySibling( tag1, tag2 ) { | ||
234 | |||
235 | if ( tag1 in CKEDITOR.dtd.$listItem || tag1 in CKEDITOR.dtd.$tableContent ) | ||
236 | return tag1 == tag2 || tag1 == 'dt' && tag2 == 'dd' || tag1 == 'dd' && tag2 == 'dt'; | ||
237 | |||
238 | return false; | ||
239 | } | ||
240 | |||
241 | parser.onTagOpen = function( tagName, attributes, selfClosing, optionalClose ) { | ||
242 | var element = new CKEDITOR.htmlParser.element( tagName, attributes ); | ||
243 | |||
244 | // "isEmpty" will be always "false" for unknown elements, so we | ||
245 | // must force it if the parser has identified it as a selfClosing tag. | ||
246 | if ( element.isUnknown && selfClosing ) | ||
247 | element.isEmpty = true; | ||
248 | |||
249 | // Check for optional closed elements, including browser quirks and manually opened blocks. | ||
250 | element.isOptionalClose = optionalClose; | ||
251 | |||
252 | // This is a tag to be removed if empty, so do not add it immediately. | ||
253 | if ( isRemoveEmpty( element ) ) { | ||
254 | pendingInline.push( element ); | ||
255 | return; | ||
256 | } else if ( tagName == 'pre' ) | ||
257 | inPre = true; | ||
258 | else if ( tagName == 'br' && inPre ) { | ||
259 | currentNode.add( new CKEDITOR.htmlParser.text( '\n' ) ); | ||
260 | return; | ||
261 | } else if ( tagName == 'textarea' ) { | ||
262 | inTextarea = true; | ||
263 | } | ||
264 | |||
265 | if ( tagName == 'br' ) { | ||
266 | pendingBRs.push( element ); | ||
267 | return; | ||
268 | } | ||
269 | |||
270 | while ( 1 ) { | ||
271 | var currentName = currentNode.name; | ||
272 | |||
273 | var currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd; | ||
274 | |||
275 | // If the element cannot be child of the current element. | ||
276 | if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) { | ||
277 | // Current node doesn't have a close tag, time for a close | ||
278 | // as this element isn't fit in. (#7497) | ||
279 | if ( currentNode.isOptionalClose ) | ||
280 | parser.onTagClose( currentName ); | ||
281 | // Fixing malformed nested lists by moving it into a previous list item. (#3828) | ||
282 | else if ( tagName in listBlocks && currentName in listBlocks ) { | ||
283 | var children = currentNode.children, | ||
284 | lastChild = children[ children.length - 1 ]; | ||
285 | |||
286 | // Establish the list item if it's not existed. | ||
287 | if ( !( lastChild && lastChild.name == 'li' ) ) | ||
288 | addElement( ( lastChild = new CKEDITOR.htmlParser.element( 'li' ) ), currentNode ); | ||
289 | |||
290 | !element.returnPoint && ( element.returnPoint = currentNode ); | ||
291 | currentNode = lastChild; | ||
292 | } | ||
293 | // Establish new list root for orphan list items, but NOT to create | ||
294 | // new list for the following ones, fix them instead. (#6975) | ||
295 | // <dl><dt>foo<dd>bar</dl> | ||
296 | // <ul><li>foo<li>bar</ul> | ||
297 | else if ( tagName in CKEDITOR.dtd.$listItem && | ||
298 | !possiblySibling( tagName, currentName ) ) { | ||
299 | parser.onTagOpen( tagName == 'li' ? 'ul' : 'dl', {}, 0, 1 ); | ||
300 | } | ||
301 | // We're inside a structural block like table and list, AND the incoming element | ||
302 | // is not of the same type (e.g. <td>td1<td>td2</td>), we simply add this new one before it, | ||
303 | // and most importantly, return back to here once this element is added, | ||
304 | // e.g. <table><tr><td>td1</td><p>p1</p><td>td2</td></tr></table> | ||
305 | else if ( currentName in nonBreakingBlocks && | ||
306 | !possiblySibling( tagName, currentName ) ) { | ||
307 | !element.returnPoint && ( element.returnPoint = currentNode ); | ||
308 | currentNode = currentNode.parent; | ||
309 | } else { | ||
310 | // The current element is an inline element, which | ||
311 | // need to be continued even after the close, so put | ||
312 | // it in the pending list. | ||
313 | if ( currentName in CKEDITOR.dtd.$inline ) | ||
314 | pendingInline.unshift( currentNode ); | ||
315 | |||
316 | // The most common case where we just need to close the | ||
317 | // current one and append the new one to the parent. | ||
318 | if ( currentNode.parent ) | ||
319 | addElement( currentNode, currentNode.parent, 1 ); | ||
320 | // We've tried our best to fix the embarrassment here, while | ||
321 | // this element still doesn't find it's parent, mark it as | ||
322 | // orphan and show our tolerance to it. | ||
323 | else { | ||
324 | element.isOrphan = 1; | ||
325 | break; | ||
326 | } | ||
327 | } | ||
328 | } else { | ||
329 | break; | ||
330 | } | ||
331 | } | ||
332 | |||
333 | checkPending( tagName ); | ||
334 | sendPendingBRs(); | ||
335 | |||
336 | element.parent = currentNode; | ||
337 | |||
338 | if ( element.isEmpty ) | ||
339 | addElement( element ); | ||
340 | else | ||
341 | currentNode = element; | ||
342 | }; | ||
343 | |||
344 | parser.onTagClose = function( tagName ) { | ||
345 | // Check if there is any pending tag to be closed. | ||
346 | for ( var i = pendingInline.length - 1; i >= 0; i-- ) { | ||
347 | // If found, just remove it from the list. | ||
348 | if ( tagName == pendingInline[ i ].name ) { | ||
349 | pendingInline.splice( i, 1 ); | ||
350 | return; | ||
351 | } | ||
352 | } | ||
353 | |||
354 | var pendingAdd = [], | ||
355 | newPendingInline = [], | ||
356 | candidate = currentNode; | ||
357 | |||
358 | while ( candidate != root && candidate.name != tagName ) { | ||
359 | // If this is an inline element, add it to the pending list, if we're | ||
360 | // really closing one of the parents element later, they will continue | ||
361 | // after it. | ||
362 | if ( !candidate._.isBlockLike ) | ||
363 | newPendingInline.unshift( candidate ); | ||
364 | |||
365 | // This node should be added to it's parent at this point. But, | ||
366 | // it should happen only if the closing tag is really closing | ||
367 | // one of the nodes. So, for now, we just cache it. | ||
368 | pendingAdd.push( candidate ); | ||
369 | |||
370 | // Make sure return point is properly restored. | ||
371 | candidate = candidate.returnPoint || candidate.parent; | ||
372 | } | ||
373 | |||
374 | if ( candidate != root ) { | ||
375 | // Add all elements that have been found in the above loop. | ||
376 | for ( i = 0; i < pendingAdd.length; i++ ) { | ||
377 | var node = pendingAdd[ i ]; | ||
378 | addElement( node, node.parent ); | ||
379 | } | ||
380 | |||
381 | currentNode = candidate; | ||
382 | |||
383 | if ( candidate._.isBlockLike ) | ||
384 | sendPendingBRs(); | ||
385 | |||
386 | addElement( candidate, candidate.parent ); | ||
387 | |||
388 | // The parent should start receiving new nodes now, except if | ||
389 | // addElement changed the currentNode. | ||
390 | if ( candidate == currentNode ) | ||
391 | currentNode = currentNode.parent; | ||
392 | |||
393 | pendingInline = pendingInline.concat( newPendingInline ); | ||
394 | } | ||
395 | |||
396 | if ( tagName == 'body' ) | ||
397 | fixingBlock = false; | ||
398 | }; | ||
399 | |||
400 | parser.onText = function( text ) { | ||
401 | // Trim empty spaces at beginning of text contents except <pre> and <textarea>. | ||
402 | if ( ( !currentNode._.hasInlineStarted || pendingBRs.length ) && !inPre && !inTextarea ) { | ||
403 | text = CKEDITOR.tools.ltrim( text ); | ||
404 | |||
405 | if ( text.length === 0 ) | ||
406 | return; | ||
407 | } | ||
408 | |||
409 | var currentName = currentNode.name, | ||
410 | currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd; | ||
411 | |||
412 | // Fix orphan text in list/table. (#8540) (#8870) | ||
413 | if ( !inTextarea && !currentDtd[ '#' ] && currentName in nonBreakingBlocks ) { | ||
414 | parser.onTagOpen( structureFixes[ currentName ] || '' ); | ||
415 | parser.onText( text ); | ||
416 | return; | ||
417 | } | ||
418 | |||
419 | sendPendingBRs(); | ||
420 | checkPending(); | ||
421 | |||
422 | // Shrinking consequential spaces into one single for all elements | ||
423 | // text contents. | ||
424 | if ( !inPre && !inTextarea ) | ||
425 | text = text.replace( /[\t\r\n ]{2,}|[\t\r\n]/g, ' ' ); | ||
426 | |||
427 | text = new CKEDITOR.htmlParser.text( text ); | ||
428 | |||
429 | |||
430 | if ( checkAutoParagraphing( currentNode, text ) ) | ||
431 | this.onTagOpen( fixingBlock, {}, 0, 1 ); | ||
432 | |||
433 | currentNode.add( text ); | ||
434 | }; | ||
435 | |||
436 | parser.onCDATA = function( cdata ) { | ||
437 | currentNode.add( new CKEDITOR.htmlParser.cdata( cdata ) ); | ||
438 | }; | ||
439 | |||
440 | parser.onComment = function( comment ) { | ||
441 | sendPendingBRs(); | ||
442 | checkPending(); | ||
443 | currentNode.add( new CKEDITOR.htmlParser.comment( comment ) ); | ||
444 | }; | ||
445 | |||
446 | // Parse it. | ||
447 | parser.parse( fragmentHtml ); | ||
448 | |||
449 | sendPendingBRs(); | ||
450 | |||
451 | // Close all pending nodes, make sure return point is properly restored. | ||
452 | while ( currentNode != root ) | ||
453 | addElement( currentNode, currentNode.parent, 1 ); | ||
454 | |||
455 | removeTailWhitespace( root ); | ||
456 | |||
457 | return root; | ||
458 | }; | ||
459 | |||
460 | CKEDITOR.htmlParser.fragment.prototype = { | ||
461 | |||
462 | /** | ||
463 | * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}. | ||
464 | * | ||
465 | * @readonly | ||
466 | * @property {Number} [=CKEDITOR.NODE_DOCUMENT_FRAGMENT] | ||
467 | */ | ||
468 | type: CKEDITOR.NODE_DOCUMENT_FRAGMENT, | ||
469 | |||
470 | /** | ||
471 | * Adds a node to this fragment. | ||
472 | * | ||
473 | * @param {CKEDITOR.htmlParser.node} node The node to be added. | ||
474 | * @param {Number} [index] From where the insertion happens. | ||
475 | */ | ||
476 | add: function( node, index ) { | ||
477 | isNaN( index ) && ( index = this.children.length ); | ||
478 | |||
479 | var previous = index > 0 ? this.children[ index - 1 ] : null; | ||
480 | if ( previous ) { | ||
481 | // If the block to be appended is following text, trim spaces at | ||
482 | // the right of it. | ||
483 | if ( node._.isBlockLike && previous.type == CKEDITOR.NODE_TEXT ) { | ||
484 | previous.value = CKEDITOR.tools.rtrim( previous.value ); | ||
485 | |||
486 | // If we have completely cleared the previous node. | ||
487 | if ( previous.value.length === 0 ) { | ||
488 | // Remove it from the list and add the node again. | ||
489 | this.children.pop(); | ||
490 | this.add( node ); | ||
491 | return; | ||
492 | } | ||
493 | } | ||
494 | |||
495 | previous.next = node; | ||
496 | } | ||
497 | |||
498 | node.previous = previous; | ||
499 | node.parent = this; | ||
500 | |||
501 | this.children.splice( index, 0, node ); | ||
502 | |||
503 | if ( !this._.hasInlineStarted ) | ||
504 | this._.hasInlineStarted = node.type == CKEDITOR.NODE_TEXT || ( node.type == CKEDITOR.NODE_ELEMENT && !node._.isBlockLike ); | ||
505 | }, | ||
506 | |||
507 | /** | ||
508 | * Filter this fragment's content with given filter. | ||
509 | * | ||
510 | * @since 4.1 | ||
511 | * @param {CKEDITOR.htmlParser.filter} filter | ||
512 | */ | ||
513 | filter: function( filter, context ) { | ||
514 | context = this.getFilterContext( context ); | ||
515 | |||
516 | // Apply the root filter. | ||
517 | filter.onRoot( context, this ); | ||
518 | |||
519 | this.filterChildren( filter, false, context ); | ||
520 | }, | ||
521 | |||
522 | /** | ||
523 | * Filter this fragment's children with given filter. | ||
524 | * | ||
525 | * Element's children may only be filtered once by one | ||
526 | * instance of filter. | ||
527 | * | ||
528 | * @since 4.1 | ||
529 | * @param {CKEDITOR.htmlParser.filter} filter | ||
530 | * @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`. | ||
531 | */ | ||
532 | filterChildren: function( filter, filterRoot, context ) { | ||
533 | // If this element's children were already filtered | ||
534 | // by current filter, don't filter them 2nd time. | ||
535 | // This situation may occur when filtering bottom-up | ||
536 | // (filterChildren() called manually in element's filter), | ||
537 | // or in unpredictable edge cases when filter | ||
538 | // is manipulating DOM structure. | ||
539 | if ( this.childrenFilteredBy == filter.id ) | ||
540 | return; | ||
541 | |||
542 | context = this.getFilterContext( context ); | ||
543 | |||
544 | // Filtering root if enforced. | ||
545 | if ( filterRoot && !this.parent ) | ||
546 | filter.onRoot( context, this ); | ||
547 | |||
548 | this.childrenFilteredBy = filter.id; | ||
549 | |||
550 | // Don't cache anything, children array may be modified by filter rule. | ||
551 | for ( var i = 0; i < this.children.length; i++ ) { | ||
552 | // Stay in place if filter returned false, what means | ||
553 | // that node has been removed. | ||
554 | if ( this.children[ i ].filter( filter, context ) === false ) | ||
555 | i--; | ||
556 | } | ||
557 | }, | ||
558 | |||
559 | /** | ||
560 | * Writes the fragment HTML to a {@link CKEDITOR.htmlParser.basicWriter}. | ||
561 | * | ||
562 | * var writer = new CKEDITOR.htmlWriter(); | ||
563 | * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<P><B>Example' ); | ||
564 | * fragment.writeHtml( writer ); | ||
565 | * alert( writer.getHtml() ); // '<p><b>Example</b></p>' | ||
566 | * | ||
567 | * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. | ||
568 | * @param {CKEDITOR.htmlParser.filter} [filter] The filter to use when writing the HTML. | ||
569 | */ | ||
570 | writeHtml: function( writer, filter ) { | ||
571 | if ( filter ) | ||
572 | this.filter( filter ); | ||
573 | |||
574 | this.writeChildrenHtml( writer ); | ||
575 | }, | ||
576 | |||
577 | /** | ||
578 | * Write and filtering the child nodes of this fragment. | ||
579 | * | ||
580 | * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. | ||
581 | * @param {CKEDITOR.htmlParser.filter} [filter] The filter to use when writing the HTML. | ||
582 | * @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`. | ||
583 | */ | ||
584 | writeChildrenHtml: function( writer, filter, filterRoot ) { | ||
585 | var context = this.getFilterContext(); | ||
586 | |||
587 | // Filtering root if enforced. | ||
588 | if ( filterRoot && !this.parent && filter ) | ||
589 | filter.onRoot( context, this ); | ||
590 | |||
591 | if ( filter ) | ||
592 | this.filterChildren( filter, false, context ); | ||
593 | |||
594 | for ( var i = 0, children = this.children, l = children.length; i < l; i++ ) | ||
595 | children[ i ].writeHtml( writer ); | ||
596 | }, | ||
597 | |||
598 | /** | ||
599 | * Execute callback on each node (of given type) in this document fragment. | ||
600 | * | ||
601 | * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p>foo<b>bar</b>bom</p>' ); | ||
602 | * fragment.forEach( function( node ) { | ||
603 | * console.log( node ); | ||
604 | * } ); | ||
605 | * // Will log: | ||
606 | * // 1. document fragment, | ||
607 | * // 2. <p> element, | ||
608 | * // 3. "foo" text node, | ||
609 | * // 4. <b> element, | ||
610 | * // 5. "bar" text node, | ||
611 | * // 6. "bom" text node. | ||
612 | * | ||
613 | * @since 4.1 | ||
614 | * @param {Function} callback Function to be executed on every node. | ||
615 | * **Since 4.3** if `callback` returned `false` descendants of current node will be ignored. | ||
616 | * @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument. | ||
617 | * @param {Number} [type] If specified `callback` will be executed only on nodes of this type. | ||
618 | * @param {Boolean} [skipRoot] Don't execute `callback` on this fragment. | ||
619 | */ | ||
620 | forEach: function( callback, type, skipRoot ) { | ||
621 | if ( !skipRoot && ( !type || this.type == type ) ) | ||
622 | var ret = callback( this ); | ||
623 | |||
624 | // Do not filter children if callback returned false. | ||
625 | if ( ret === false ) | ||
626 | return; | ||
627 | |||
628 | var children = this.children, | ||
629 | node, | ||
630 | i = 0; | ||
631 | |||
632 | // We do not cache the size, because the list of nodes may be changed by the callback. | ||
633 | for ( ; i < children.length; i++ ) { | ||
634 | node = children[ i ]; | ||
635 | if ( node.type == CKEDITOR.NODE_ELEMENT ) | ||
636 | node.forEach( callback, type ); | ||
637 | else if ( !type || node.type == type ) | ||
638 | callback( node ); | ||
639 | } | ||
640 | }, | ||
641 | |||
642 | getFilterContext: function( context ) { | ||
643 | return context || {}; | ||
644 | } | ||
645 | }; | ||
646 | } )(); | ||
diff --git a/sources/core/htmlparser/node.js b/sources/core/htmlparser/node.js new file mode 100644 index 0000000..0f1b307 --- /dev/null +++ b/sources/core/htmlparser/node.js | |||
@@ -0,0 +1,156 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 | * A lightweight representation of HTML node. | ||
11 | * | ||
12 | * @since 4.1 | ||
13 | * @class | ||
14 | * @constructor Creates a node class instance. | ||
15 | */ | ||
16 | CKEDITOR.htmlParser.node = function() {}; | ||
17 | |||
18 | CKEDITOR.htmlParser.node.prototype = { | ||
19 | /** | ||
20 | * Remove this node from a tree. | ||
21 | * | ||
22 | * @since 4.1 | ||
23 | */ | ||
24 | remove: function() { | ||
25 | var children = this.parent.children, | ||
26 | index = CKEDITOR.tools.indexOf( children, this ), | ||
27 | previous = this.previous, | ||
28 | next = this.next; | ||
29 | |||
30 | previous && ( previous.next = next ); | ||
31 | next && ( next.previous = previous ); | ||
32 | children.splice( index, 1 ); | ||
33 | this.parent = null; | ||
34 | }, | ||
35 | |||
36 | /** | ||
37 | * Replace this node with given one. | ||
38 | * | ||
39 | * @since 4.1 | ||
40 | * @param {CKEDITOR.htmlParser.node} node The node that will replace this one. | ||
41 | */ | ||
42 | replaceWith: function( node ) { | ||
43 | var children = this.parent.children, | ||
44 | index = CKEDITOR.tools.indexOf( children, this ), | ||
45 | previous = node.previous = this.previous, | ||
46 | next = node.next = this.next; | ||
47 | |||
48 | previous && ( previous.next = node ); | ||
49 | next && ( next.previous = node ); | ||
50 | |||
51 | children[ index ] = node; | ||
52 | |||
53 | node.parent = this.parent; | ||
54 | this.parent = null; | ||
55 | }, | ||
56 | |||
57 | /** | ||
58 | * Insert this node after given one. | ||
59 | * | ||
60 | * @since 4.1 | ||
61 | * @param {CKEDITOR.htmlParser.node} node The node that will precede this element. | ||
62 | */ | ||
63 | insertAfter: function( node ) { | ||
64 | var children = node.parent.children, | ||
65 | index = CKEDITOR.tools.indexOf( children, node ), | ||
66 | next = node.next; | ||
67 | |||
68 | children.splice( index + 1, 0, this ); | ||
69 | |||
70 | this.next = node.next; | ||
71 | this.previous = node; | ||
72 | node.next = this; | ||
73 | next && ( next.previous = this ); | ||
74 | |||
75 | this.parent = node.parent; | ||
76 | }, | ||
77 | |||
78 | /** | ||
79 | * Insert this node before given one. | ||
80 | * | ||
81 | * @since 4.1 | ||
82 | * @param {CKEDITOR.htmlParser.node} node The node that will follow this element. | ||
83 | */ | ||
84 | insertBefore: function( node ) { | ||
85 | var children = node.parent.children, | ||
86 | index = CKEDITOR.tools.indexOf( children, node ); | ||
87 | |||
88 | children.splice( index, 0, this ); | ||
89 | |||
90 | this.next = node; | ||
91 | this.previous = node.previous; | ||
92 | node.previous && ( node.previous.next = this ); | ||
93 | node.previous = this; | ||
94 | |||
95 | this.parent = node.parent; | ||
96 | }, | ||
97 | |||
98 | /** | ||
99 | * Gets the closest ancestor element of this element which satisfies given condition | ||
100 | * | ||
101 | * @since 4.3 | ||
102 | * @param {String/Object/Function} condition Name of an ancestor, hash of names or validator function. | ||
103 | * @returns {CKEDITOR.htmlParser.element} The closest ancestor which satisfies given condition or `null`. | ||
104 | */ | ||
105 | getAscendant: function( condition ) { | ||
106 | var checkFn = | ||
107 | typeof condition == 'function' ? | ||
108 | condition : | ||
109 | typeof condition == 'string' ? | ||
110 | function( el ) { | ||
111 | return el.name == condition; | ||
112 | } : | ||
113 | function( el ) { | ||
114 | return el.name in condition; | ||
115 | }; | ||
116 | |||
117 | var parent = this.parent; | ||
118 | |||
119 | // Parent has to be an element - don't check doc fragment. | ||
120 | while ( parent && parent.type == CKEDITOR.NODE_ELEMENT ) { | ||
121 | if ( checkFn( parent ) ) | ||
122 | return parent; | ||
123 | parent = parent.parent; | ||
124 | } | ||
125 | |||
126 | return null; | ||
127 | }, | ||
128 | |||
129 | /** | ||
130 | * Wraps this element with given `wrapper`. | ||
131 | * | ||
132 | * @since 4.3 | ||
133 | * @param {CKEDITOR.htmlParser.element} wrapper The element which will be this element's new parent. | ||
134 | * @returns {CKEDITOR.htmlParser.element} Wrapper. | ||
135 | */ | ||
136 | wrapWith: function( wrapper ) { | ||
137 | this.replaceWith( wrapper ); | ||
138 | wrapper.add( this ); | ||
139 | return wrapper; | ||
140 | }, | ||
141 | |||
142 | /** | ||
143 | * Gets this node's index in its parent's children array. | ||
144 | * | ||
145 | * @since 4.3 | ||
146 | * @returns {Number} | ||
147 | */ | ||
148 | getIndex: function() { | ||
149 | return CKEDITOR.tools.indexOf( this.parent.children, this ); | ||
150 | }, | ||
151 | |||
152 | getFilterContext: function( context ) { | ||
153 | return context || {}; | ||
154 | } | ||
155 | }; | ||
156 | } )(); | ||
diff --git a/sources/core/htmlparser/text.js b/sources/core/htmlparser/text.js new file mode 100644 index 0000000..07cb865 --- /dev/null +++ b/sources/core/htmlparser/text.js | |||
@@ -0,0 +1,70 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 | * A lightweight representation of HTML text. | ||
11 | * | ||
12 | * @class | ||
13 | * @extends CKEDITOR.htmlParser.node | ||
14 | * @constructor Creates a text class instance. | ||
15 | * @param {String} value The text node value. | ||
16 | */ | ||
17 | CKEDITOR.htmlParser.text = function( value ) { | ||
18 | /** | ||
19 | * The text value. | ||
20 | * | ||
21 | * @property {String} | ||
22 | */ | ||
23 | this.value = value; | ||
24 | |||
25 | /** @private */ | ||
26 | this._ = { | ||
27 | isBlockLike: false | ||
28 | }; | ||
29 | }; | ||
30 | |||
31 | CKEDITOR.htmlParser.text.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), { | ||
32 | /** | ||
33 | * The node type. This is a constant value set to {@link CKEDITOR#NODE_TEXT}. | ||
34 | * | ||
35 | * @readonly | ||
36 | * @property {Number} [=CKEDITOR.NODE_TEXT] | ||
37 | */ | ||
38 | type: CKEDITOR.NODE_TEXT, | ||
39 | |||
40 | /** | ||
41 | * Filter this text node with given filter. | ||
42 | * | ||
43 | * @since 4.1 | ||
44 | * @param {CKEDITOR.htmlParser.filter} filter | ||
45 | * @returns {Boolean} Method returns `false` when this text node has | ||
46 | * been removed. This is an information for {@link CKEDITOR.htmlParser.element#filterChildren} | ||
47 | * that it has to repeat filter on current position in parent's children array. | ||
48 | */ | ||
49 | filter: function( filter, context ) { | ||
50 | if ( !( this.value = filter.onText( context, this.value, this ) ) ) { | ||
51 | this.remove(); | ||
52 | return false; | ||
53 | } | ||
54 | }, | ||
55 | |||
56 | /** | ||
57 | * Writes the HTML representation of this text to a {CKEDITOR.htmlParser.basicWriter}. | ||
58 | * | ||
59 | * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. | ||
60 | * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node. | ||
61 | * **Note:** it's unsafe to filter offline (not appended) node. | ||
62 | */ | ||
63 | writeHtml: function( writer, filter ) { | ||
64 | if ( filter ) | ||
65 | this.filter( filter ); | ||
66 | |||
67 | writer.text( this.value ); | ||
68 | } | ||
69 | } ); | ||
70 | } )(); | ||
diff --git a/sources/core/keystrokehandler.js b/sources/core/keystrokehandler.js new file mode 100644 index 0000000..e2a6bcd --- /dev/null +++ b/sources/core/keystrokehandler.js | |||
@@ -0,0 +1,169 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * Controls keystrokes typing in an editor instance. | ||
8 | * | ||
9 | * @class | ||
10 | * @constructor Creates a keystrokeHandler class instance. | ||
11 | * @param {CKEDITOR.editor} editor The editor instance. | ||
12 | */ | ||
13 | CKEDITOR.keystrokeHandler = function( editor ) { | ||
14 | if ( editor.keystrokeHandler ) | ||
15 | return editor.keystrokeHandler; | ||
16 | |||
17 | /** | ||
18 | * A list of keystrokes associated with commands. Each entry points to the | ||
19 | * command to be executed. | ||
20 | * | ||
21 | * Since CKEditor 4 there is no need to modify this property directly during the runtime. | ||
22 | * Use {@link CKEDITOR.editor#setKeystroke} instead. | ||
23 | */ | ||
24 | this.keystrokes = {}; | ||
25 | |||
26 | /** | ||
27 | * A list of keystrokes that should be blocked if not defined in | ||
28 | * {@link #keystrokes}. In this way it is possible to block the default | ||
29 | * browser behavior for those keystrokes. | ||
30 | */ | ||
31 | this.blockedKeystrokes = {}; | ||
32 | |||
33 | this._ = { | ||
34 | editor: editor | ||
35 | }; | ||
36 | |||
37 | return this; | ||
38 | }; | ||
39 | |||
40 | ( function() { | ||
41 | var cancel; | ||
42 | |||
43 | var onKeyDown = function( event ) { | ||
44 | // The DOM event object is passed by the "data" property. | ||
45 | event = event.data; | ||
46 | |||
47 | var keyCombination = event.getKeystroke(); | ||
48 | var command = this.keystrokes[ keyCombination ]; | ||
49 | var editor = this._.editor; | ||
50 | |||
51 | cancel = ( editor.fire( 'key', { keyCode: keyCombination, domEvent: event } ) === false ); | ||
52 | |||
53 | if ( !cancel ) { | ||
54 | if ( command ) { | ||
55 | var data = { from: 'keystrokeHandler' }; | ||
56 | cancel = ( editor.execCommand( command, data ) !== false ); | ||
57 | } | ||
58 | |||
59 | if ( !cancel ) | ||
60 | cancel = !!this.blockedKeystrokes[ keyCombination ]; | ||
61 | } | ||
62 | |||
63 | if ( cancel ) | ||
64 | event.preventDefault( true ); | ||
65 | |||
66 | return !cancel; | ||
67 | }; | ||
68 | |||
69 | var onKeyPress = function( event ) { | ||
70 | if ( cancel ) { | ||
71 | cancel = false; | ||
72 | event.data.preventDefault( true ); | ||
73 | } | ||
74 | }; | ||
75 | |||
76 | CKEDITOR.keystrokeHandler.prototype = { | ||
77 | /** | ||
78 | * Attaches this keystroke handle to a DOM object. Keystrokes typed | ||
79 | * over this object will be handled by this keystrokeHandler. | ||
80 | * | ||
81 | * @param {CKEDITOR.dom.domObject} domObject The DOM object to attach to. | ||
82 | */ | ||
83 | attach: function( domObject ) { | ||
84 | // For most browsers, it is enough to listen to the keydown event | ||
85 | // only. | ||
86 | domObject.on( 'keydown', onKeyDown, this ); | ||
87 | |||
88 | // Some browsers instead, don't cancel key events in the keydown, but in the | ||
89 | // keypress. So we must do a longer trip in those cases. | ||
90 | if ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) | ||
91 | domObject.on( 'keypress', onKeyPress, this ); | ||
92 | } | ||
93 | }; | ||
94 | } )(); | ||
95 | |||
96 | /** | ||
97 | * A list associating keystrokes with editor commands. Each element in the list | ||
98 | * is an array where the first item is the keystroke, and the second is the | ||
99 | * name of the command to be executed. | ||
100 | * | ||
101 | * This setting should be used to define (as well as to overwrite or remove) keystrokes | ||
102 | * set by plugins (like `link` and `basicstyles`). If you want to set a keystroke | ||
103 | * for your plugin or during the runtime, use {@link CKEDITOR.editor#setKeystroke} instead. | ||
104 | * | ||
105 | * Since default keystrokes are set by the {@link CKEDITOR.editor#setKeystroke} | ||
106 | * method, by default `config.keystrokes` is an empty array. | ||
107 | * | ||
108 | * See {@link CKEDITOR.editor#setKeystroke} documentation for more details | ||
109 | * regarding the start up order. | ||
110 | * | ||
111 | * // Change default Ctrl+L keystroke for 'link' command to Ctrl+Shift+L. | ||
112 | * config.keystrokes = [ | ||
113 | * ... | ||
114 | * [ CKEDITOR.CTRL + CKEDITOR.SHIFT + 76, 'link' ], // Ctrl+Shift+L | ||
115 | * ... | ||
116 | * ]; | ||
117 | * | ||
118 | * To reset a particular keystroke, the following approach can be used: | ||
119 | * | ||
120 | * // Disable default Ctrl+L keystroke which executes the 'link' command by default. | ||
121 | * config.keystrokes = [ | ||
122 | * ... | ||
123 | * [ CKEDITOR.CTRL + 76, null ], // Ctrl+L | ||
124 | * ... | ||
125 | * ]; | ||
126 | * | ||
127 | * In order to reset all default keystrokes, a {@link CKEDITOR#instanceReady} callback should be | ||
128 | * used. This is since editor defaults are merged rather than overwritten by | ||
129 | * user keystrokes. | ||
130 | * | ||
131 | * **Note**: This can be potentially harmful for the editor. Avoid this unless you are | ||
132 | * aware of the consequences. | ||
133 | * | ||
134 | * // Reset all default keystrokes. | ||
135 | * config.on.instanceReady = function() { | ||
136 | * this.keystrokeHandler.keystrokes = []; | ||
137 | * }; | ||
138 | * | ||
139 | * @cfg {Array} [keystrokes=[]] | ||
140 | * @member CKEDITOR.config | ||
141 | */ | ||
142 | |||
143 | /** | ||
144 | * Fired when any keyboard key (or a combination thereof) is pressed in the editing area. | ||
145 | * | ||
146 | * editor.on( 'key', function( evt ) { | ||
147 | * if ( evt.data.keyCode == CKEDITOR.CTRL + 90 ) { | ||
148 | * // Do something... | ||
149 | * | ||
150 | * // Cancel the event, so other listeners will not be executed and | ||
151 | * // the keydown's default behavior will be prevented. | ||
152 | * evt.cancel(); | ||
153 | * } | ||
154 | * } ); | ||
155 | * | ||
156 | * Usually you will want to use the {@link CKEDITOR.editor#setKeystroke} method or | ||
157 | * the {@link CKEDITOR.config#keystrokes} option to attach a keystroke to some {@link CKEDITOR.command command}. | ||
158 | * Key event listeners are usuful when some action should be executed conditionally, based | ||
159 | * for example on precise selection location. | ||
160 | * | ||
161 | * @event key | ||
162 | * @member CKEDITOR.editor | ||
163 | * @param data | ||
164 | * @param {Number} data.keyCode A number representing the key code (or a combination thereof). | ||
165 | * It is the sum of the current key code and the {@link CKEDITOR#CTRL}, {@link CKEDITOR#SHIFT} | ||
166 | * and {@link CKEDITOR#ALT} constants, if those are pressed. | ||
167 | * @param {CKEDITOR.dom.event} data.domEvent A `keydown` DOM event instance. Available since CKEditor 4.4.1. | ||
168 | * @param {CKEDITOR.editor} editor This editor instance. | ||
169 | */ | ||
diff --git a/sources/core/lang.js b/sources/core/lang.js new file mode 100644 index 0000000..3519923 --- /dev/null +++ b/sources/core/lang.js | |||
@@ -0,0 +1,103 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | ( function() { | ||
7 | /** | ||
8 | * Stores language-related functions. | ||
9 | * | ||
10 | * @class | ||
11 | * @singleton | ||
12 | */ | ||
13 | CKEDITOR.lang = { | ||
14 | /** | ||
15 | * The list of languages available in the editor core. | ||
16 | * | ||
17 | * alert( CKEDITOR.lang.languages.en ); // 1 | ||
18 | */ | ||
19 | languages: { | ||
20 | af: 1, ar: 1, bg: 1, bn: 1, bs: 1, ca: 1, cs: 1, cy: 1, da: 1, de: 1, 'de-ch': 1, el: 1, | ||
21 | 'en-au': 1, 'en-ca': 1, 'en-gb': 1, en: 1, eo: 1, es: 1, et: 1, eu: 1, fa: 1, fi: 1, fo: 1, | ||
22 | 'fr-ca': 1, fr: 1, gl: 1, gu: 1, he: 1, hi: 1, hr: 1, hu: 1, id: 1, is: 1, it: 1, ja: 1, ka: 1, | ||
23 | km: 1, ko: 1, ku: 1, lt: 1, lv: 1, mk: 1, mn: 1, ms: 1, nb: 1, nl: 1, no: 1, pl: 1, 'pt-br': 1, | ||
24 | pt: 1, ro: 1, ru: 1, si: 1, sk: 1, sl: 1, sq: 1, 'sr-latn': 1, sr: 1, sv: 1, th: 1, tr: 1, tt: 1, ug: 1, | ||
25 | uk: 1, vi: 1, 'zh-cn': 1, zh: 1 | ||
26 | }, | ||
27 | |||
28 | /** | ||
29 | * The list of languages that are written Right-To-Left (RTL) and are supported by the editor. | ||
30 | */ | ||
31 | rtl: { ar: 1, fa: 1, he: 1, ku: 1, ug: 1 }, | ||
32 | |||
33 | /** | ||
34 | * Loads a specific language file, or auto detects it. A callback is | ||
35 | * then called when the file gets loaded. | ||
36 | * | ||
37 | * @param {String} languageCode The code of the language file to be | ||
38 | * loaded. If null or empty, autodetection will be performed. The | ||
39 | * same happens if the language is not supported. | ||
40 | * @param {String} defaultLanguage The language to be used if | ||
41 | * `languageCode` is not supported or if the autodetection fails. | ||
42 | * @param {Function} callback A function to be called once the | ||
43 | * language file is loaded. Two parameters are passed to this | ||
44 | * function: the language code and the loaded language entries. | ||
45 | */ | ||
46 | load: function( languageCode, defaultLanguage, callback ) { | ||
47 | // If no languageCode - fallback to browser or default. | ||
48 | // If languageCode - fallback to no-localized version or default. | ||
49 | if ( !languageCode || !CKEDITOR.lang.languages[ languageCode ] ) | ||
50 | languageCode = this.detect( defaultLanguage, languageCode ); | ||
51 | |||
52 | var that = this, | ||
53 | loadedCallback = function() { | ||
54 | that[ languageCode ].dir = that.rtl[ languageCode ] ? 'rtl' : 'ltr'; | ||
55 | callback( languageCode, that[ languageCode ] ); | ||
56 | }; | ||
57 | |||
58 | if ( !this[ languageCode ] ) | ||
59 | CKEDITOR.scriptLoader.load( CKEDITOR.getUrl( 'lang/' + languageCode + '.js' ), loadedCallback, this ); | ||
60 | else | ||
61 | loadedCallback(); | ||
62 | }, | ||
63 | |||
64 | /** | ||
65 | * Returns the language that best fits the user language. For example, | ||
66 | * suppose that the user language is "pt-br". If this language is | ||
67 | * supported by the editor, it is returned. Otherwise, if only "pt" is | ||
68 | * supported, it is returned instead. If none of the previous are | ||
69 | * supported, a default language is then returned. | ||
70 | * | ||
71 | * alert( CKEDITOR.lang.detect( 'en' ) ); // e.g., in a German browser: 'de' | ||
72 | * | ||
73 | * @param {String} defaultLanguage The default language to be returned | ||
74 | * if the user language is not supported. | ||
75 | * @param {String} [probeLanguage] A language code to try to use, | ||
76 | * instead of the browser-based autodetection. | ||
77 | * @returns {String} The detected language code. | ||
78 | */ | ||
79 | detect: function( defaultLanguage, probeLanguage ) { | ||
80 | var languages = this.languages; | ||
81 | probeLanguage = probeLanguage || navigator.userLanguage || navigator.language || defaultLanguage; | ||
82 | |||
83 | var parts = probeLanguage.toLowerCase().match( /([a-z]+)(?:-([a-z]+))?/ ), | ||
84 | lang = parts[ 1 ], | ||
85 | locale = parts[ 2 ]; | ||
86 | |||
87 | if ( languages[ lang + '-' + locale ] ) | ||
88 | lang = lang + '-' + locale; | ||
89 | else if ( !languages[ lang ] ) | ||
90 | lang = null; | ||
91 | |||
92 | CKEDITOR.lang.detect = lang ? | ||
93 | function() { | ||
94 | return lang; | ||
95 | } : function( defaultLanguage ) { | ||
96 | return defaultLanguage; | ||
97 | }; | ||
98 | |||
99 | return lang || defaultLanguage; | ||
100 | } | ||
101 | }; | ||
102 | |||
103 | } )(); | ||
diff --git a/sources/core/loader.js b/sources/core/loader.js new file mode 100644 index 0000000..5a108df --- /dev/null +++ b/sources/core/loader.js | |||
@@ -0,0 +1,225 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.loader} objects, which is used to | ||
8 | * load core scripts and their dependencies from _source. | ||
9 | */ | ||
10 | |||
11 | if ( typeof CKEDITOR == 'undefined' ) | ||
12 | CKEDITOR = {}; // jshint ignore:line | ||
13 | |||
14 | if ( !CKEDITOR.loader ) { | ||
15 | /** | ||
16 | * Load core scripts and their dependencies from _source. | ||
17 | * | ||
18 | * @class | ||
19 | * @singleton | ||
20 | */ | ||
21 | CKEDITOR.loader = ( function() { | ||
22 | // Table of script names and their dependencies. | ||
23 | var scripts = { | ||
24 | '_bootstrap': [ | ||
25 | 'config', 'creators/inline', 'creators/themedui', 'editable', 'ckeditor', 'plugins', | ||
26 | 'scriptloader', 'style', 'tools', | ||
27 | // The following are entries that we want to force loading at the end to avoid dependence recursion. | ||
28 | 'dom/comment', 'dom/elementpath', 'dom/text', 'dom/rangelist', 'skin' | ||
29 | ], | ||
30 | 'ckeditor': [ | ||
31 | 'ckeditor_basic', 'log', 'dom', 'dtd', 'dom/document', 'dom/element', 'dom/iterator', 'editor', 'event', | ||
32 | 'htmldataprocessor', 'htmlparser', 'htmlparser/element', 'htmlparser/fragment', 'htmlparser/filter', | ||
33 | 'htmlparser/basicwriter', 'template', 'tools' | ||
34 | ], | ||
35 | 'ckeditor_base': [], | ||
36 | 'ckeditor_basic': [ 'editor_basic', 'env', 'event' ], | ||
37 | 'command': [], | ||
38 | 'config': [ 'ckeditor_base' ], | ||
39 | 'dom': [], | ||
40 | 'dom/comment': [ 'dom/node' ], | ||
41 | 'dom/document': [ 'dom/node', 'dom/window' ], | ||
42 | 'dom/documentfragment': [ 'dom/element' ], | ||
43 | 'dom/element': [ 'dom', 'dom/document', 'dom/domobject', 'dom/node', 'dom/nodelist', 'tools' ], | ||
44 | 'dom/elementpath': [ 'dom/element' ], | ||
45 | 'dom/event': [], | ||
46 | 'dom/iterator': [ 'dom/range' ], | ||
47 | 'dom/node': [ 'dom/domobject', 'tools' ], | ||
48 | 'dom/nodelist': [ 'dom/node' ], | ||
49 | 'dom/domobject': [ 'dom/event' ], | ||
50 | 'dom/range': [ 'dom/document', 'dom/documentfragment', 'dom/element', 'dom/walker' ], | ||
51 | 'dom/rangelist': [ 'dom/range' ], | ||
52 | 'dom/text': [ 'dom/node', 'dom/domobject' ], | ||
53 | 'dom/walker': [ 'dom/node' ], | ||
54 | 'dom/window': [ 'dom/domobject' ], | ||
55 | 'dtd': [ 'tools' ], | ||
56 | 'editable': [ 'editor', 'tools' ], | ||
57 | 'editor': [ | ||
58 | 'command', 'config', 'editor_basic', 'filter', 'focusmanager', 'keystrokehandler', 'lang', | ||
59 | 'plugins', 'tools', 'ui' | ||
60 | ], | ||
61 | 'editor_basic': [ 'event' ], | ||
62 | 'env': [], | ||
63 | 'event': [], | ||
64 | 'filter': [ 'dtd', 'tools' ], | ||
65 | 'focusmanager': [], | ||
66 | 'htmldataprocessor': [ 'htmlparser', 'htmlparser/basicwriter', 'htmlparser/fragment', 'htmlparser/filter' ], | ||
67 | 'htmlparser': [], | ||
68 | 'htmlparser/comment': [ 'htmlparser', 'htmlparser/node' ], | ||
69 | 'htmlparser/element': [ 'htmlparser', 'htmlparser/fragment', 'htmlparser/node' ], | ||
70 | 'htmlparser/fragment': [ 'htmlparser', 'htmlparser/comment', 'htmlparser/text', 'htmlparser/cdata' ], | ||
71 | 'htmlparser/text': [ 'htmlparser', 'htmlparser/node' ], | ||
72 | 'htmlparser/cdata': [ 'htmlparser', 'htmlparser/node' ], | ||
73 | 'htmlparser/filter': [ 'htmlparser' ], | ||
74 | 'htmlparser/basicwriter': [ 'htmlparser' ], | ||
75 | 'htmlparser/node': [ 'htmlparser' ], | ||
76 | 'keystrokehandler': [ 'event' ], | ||
77 | 'lang': [], | ||
78 | 'log': [ 'ckeditor_basic' ], | ||
79 | 'plugins': [ 'resourcemanager' ], | ||
80 | 'resourcemanager': [ 'scriptloader', 'tools' ], | ||
81 | 'scriptloader': [ 'dom/element', 'env' ], | ||
82 | 'selection': [ 'dom/range', 'dom/walker' ], | ||
83 | 'skin': [], | ||
84 | 'style': [ 'selection' ], | ||
85 | 'template': [], | ||
86 | 'tools': [ 'env' ], | ||
87 | 'ui': [], | ||
88 | 'creators/themedui': [], | ||
89 | 'creators/inline': [] | ||
90 | }; | ||
91 | |||
92 | // The production implementation contains a fixed timestamp generated by the releaser. | ||
93 | var timestamp = '%TIMESTAMP%'; | ||
94 | // The development implementation contains a current timestamp. // %REMOVE_LINE% | ||
95 | timestamp = ( CKEDITOR && CKEDITOR.timestamp ) || ( new Date() ).valueOf(); // %REMOVE_LINE% | ||
96 | |||
97 | var getUrl = function( resource ) { | ||
98 | if ( CKEDITOR && CKEDITOR.getUrl ) | ||
99 | return CKEDITOR.getUrl( resource ); | ||
100 | |||
101 | return CKEDITOR.basePath + resource + ( resource.indexOf( '?' ) >= 0 ? '&' : '?' ) + 't=' + timestamp; | ||
102 | }; | ||
103 | |||
104 | var pendingLoad = []; | ||
105 | |||
106 | return { | ||
107 | /** | ||
108 | * The list of loaded scripts in their loading order. | ||
109 | * | ||
110 | * // Alert the loaded script names. | ||
111 | * alert( CKEDITOR.loader.loadedScripts ); | ||
112 | */ | ||
113 | loadedScripts: [], | ||
114 | /** | ||
115 | * Table of script names and their dependencies. | ||
116 | * | ||
117 | * @property {Array} | ||
118 | */ | ||
119 | scripts: scripts, | ||
120 | |||
121 | /** | ||
122 | * @todo | ||
123 | */ | ||
124 | loadPending: function() { | ||
125 | var scriptName = pendingLoad.shift(); | ||
126 | |||
127 | if ( !scriptName ) | ||
128 | return; | ||
129 | |||
130 | var scriptSrc = getUrl( 'core/' + scriptName + '.js' ); | ||
131 | |||
132 | var script = document.createElement( 'script' ); | ||
133 | script.type = 'text/javascript'; | ||
134 | script.src = scriptSrc; | ||
135 | |||
136 | function onScriptLoaded() { | ||
137 | // Append this script to the list of loaded scripts. | ||
138 | CKEDITOR.loader.loadedScripts.push( scriptName ); | ||
139 | |||
140 | // Load the next. | ||
141 | CKEDITOR.loader.loadPending(); | ||
142 | } | ||
143 | |||
144 | // We must guarantee the execution order of the scripts, so we | ||
145 | // need to load them one by one. (#4145) | ||
146 | // The following if/else block has been taken from the scriptloader core code. | ||
147 | if ( typeof script.onreadystatechange !== 'undefined' ) { | ||
148 | /** @ignore */ | ||
149 | script.onreadystatechange = function() { | ||
150 | if ( script.readyState == 'loaded' || script.readyState == 'complete' ) { | ||
151 | script.onreadystatechange = null; | ||
152 | onScriptLoaded(); | ||
153 | } | ||
154 | }; | ||
155 | } else { | ||
156 | /** @ignore */ | ||
157 | script.onload = function() { | ||
158 | // Some browsers, such as Safari, may call the onLoad function | ||
159 | // immediately. Which will break the loading sequence. (#3661) | ||
160 | setTimeout( function() { | ||
161 | onScriptLoaded( scriptName ); | ||
162 | }, 0 ); | ||
163 | }; | ||
164 | } | ||
165 | |||
166 | document.body.appendChild( script ); | ||
167 | }, | ||
168 | |||
169 | /** | ||
170 | * Loads a specific script, including its dependencies. This is not a | ||
171 | * synchronous loading, which means that the code to be loaded will | ||
172 | * not necessarily be available after this call. | ||
173 | * | ||
174 | * CKEDITOR.loader.load( 'dom/element' ); | ||
175 | * | ||
176 | * @param {String} scriptName | ||
177 | * @param {Boolean} [defer=false] | ||
178 | * @todo params | ||
179 | */ | ||
180 | load: function( scriptName, defer ) { | ||
181 | // Check if the script has already been loaded. | ||
182 | if ( ( 's:' + scriptName ) in this.loadedScripts ) | ||
183 | return; | ||
184 | |||
185 | // Get the script dependencies list. | ||
186 | var dependencies = scripts[ scriptName ]; | ||
187 | if ( !dependencies ) | ||
188 | throw 'The script name"' + scriptName + '" is not defined.'; | ||
189 | |||
190 | // Mark the script as loaded, even before really loading it, to | ||
191 | // avoid cross references recursion. | ||
192 | // Prepend script name with 's:' to avoid conflict with Array's methods. | ||
193 | this.loadedScripts[ 's:' + scriptName ] = true; | ||
194 | |||
195 | // Load all dependencies first. | ||
196 | for ( var i = 0; i < dependencies.length; i++ ) | ||
197 | this.load( dependencies[ i ], true ); | ||
198 | |||
199 | var scriptSrc = getUrl( 'core/' + scriptName + '.js' ); | ||
200 | |||
201 | // Append the <script> element to the DOM. | ||
202 | // If the page is fully loaded, we can't use document.write | ||
203 | // but if the script is run while the body is loading then it's safe to use it | ||
204 | // Unfortunately, Firefox <3.6 doesn't support document.readyState, so it won't get this improvement | ||
205 | if ( document.body && ( !document.readyState || document.readyState == 'complete' ) ) { | ||
206 | pendingLoad.push( scriptName ); | ||
207 | |||
208 | if ( !defer ) | ||
209 | this.loadPending(); | ||
210 | } else { | ||
211 | // Append this script to the list of loaded scripts. | ||
212 | this.loadedScripts.push( scriptName ); | ||
213 | |||
214 | document.write( '<script src="' + scriptSrc + '" type="text/javascript"><\/script>' ); | ||
215 | } | ||
216 | } | ||
217 | }; | ||
218 | } )(); | ||
219 | } | ||
220 | |||
221 | // Check if any script has been defined for autoload. | ||
222 | if ( CKEDITOR._autoLoad ) { | ||
223 | CKEDITOR.loader.load( CKEDITOR._autoLoad ); | ||
224 | delete CKEDITOR._autoLoad; | ||
225 | } | ||
diff --git a/sources/core/log.js b/sources/core/log.js new file mode 100644 index 0000000..6981612 --- /dev/null +++ b/sources/core/log.js | |||
@@ -0,0 +1,127 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Defines {@link CKEDITOR#verbosity} and binary flags {@link CKEDITOR#VERBOSITY_WARN} and | ||
8 | * {@link CKEDITOR#VERBOSITY_ERROR}. Defines also the {@link CKEDITOR#error} and {@link CKEDITOR#warn} functions | ||
9 | * and the default handler for the {@link CKEDITOR#log} event. | ||
10 | */ | ||
11 | |||
12 | /* global console */ | ||
13 | |||
14 | 'use strict'; | ||
15 | |||
16 | /** | ||
17 | * Warning reporting verbosity level. When {@link CKEDITOR#verbosity} is set to this value, only {@link CKEDITOR#warn} | ||
18 | * messages will be output to the console. It is a binary flag so it might be combined with | ||
19 | * the {@link CKEDITOR#VERBOSITY_ERROR} flag. | ||
20 | * | ||
21 | * @since 4.5.4 | ||
22 | * @readonly | ||
23 | * @property {Number} [=1] | ||
24 | * @member CKEDITOR | ||
25 | */ | ||
26 | CKEDITOR.VERBOSITY_WARN = 1; | ||
27 | |||
28 | /** | ||
29 | * Error reporting verbosity level. When {@link CKEDITOR#verbosity} is set to this value, only {@link CKEDITOR#error} | ||
30 | * messages will be output to the console. It is a binary flag so it might be combined with | ||
31 | * the {@link CKEDITOR#VERBOSITY_WARN} flag. | ||
32 | * | ||
33 | * @since 4.5.4 | ||
34 | * @readonly | ||
35 | * @property {Number} [=2] | ||
36 | * @member CKEDITOR | ||
37 | */ | ||
38 | CKEDITOR.VERBOSITY_ERROR = 2; | ||
39 | |||
40 | /** | ||
41 | * Verbosity of {@link CKEDITOR#error} and {@link CKEDITOR#warn} methods. Accepts binary flags | ||
42 | * {@link CKEDITOR#VERBOSITY_WARN} and {@link CKEDITOR#VERBOSITY_ERROR}. | ||
43 | * | ||
44 | * CKEDITOR.verbosity = 0; // No console output after CKEDITOR.warn and CKEDITOR.error methods. | ||
45 | * CKEDITOR.verbosity = CKEDITOR.VERBOSITY_WARN; // Console output after CKEDITOR.warn only. | ||
46 | * CKEDITOR.verbosity = CKEDITOR.VERBOSITY_ERROR; // Console output after CKEDITOR.error only. | ||
47 | * CKEDITOR.verbosity = CKEDITOR.VERBOSITY_WARN | CKEDITOR.VERBOSITY_ERROR; // Console output after both methods. | ||
48 | * | ||
49 | * Default value enables both {@link CKEDITOR#VERBOSITY_WARN} and {@link CKEDITOR#VERBOSITY_ERROR}. | ||
50 | * | ||
51 | * @since 4.5.4 | ||
52 | * @member CKEDITOR | ||
53 | * @type {Number} | ||
54 | */ | ||
55 | CKEDITOR.verbosity = CKEDITOR.VERBOSITY_WARN | CKEDITOR.VERBOSITY_ERROR; | ||
56 | |||
57 | /** | ||
58 | * Warning reporting function. When {@link CKEDITOR#verbosity} has the {@link CKEDITOR#VERBOSITY_WARN} flag set, it fires | ||
59 | * the {@link CKEDITOR#log} event with type set to `warn`. Fired event contains also provided `errorCode` and `additionalData`. | ||
60 | * | ||
61 | * @since 4.5.4 | ||
62 | * @member CKEDITOR | ||
63 | * @param {String} errorCode Error code describing reported problem. | ||
64 | * @param {Object} [additionalData] Additional data associated with reported problem. | ||
65 | */ | ||
66 | CKEDITOR.warn = function( errorCode, additionalData ) { | ||
67 | if ( CKEDITOR.verbosity & CKEDITOR.VERBOSITY_WARN ) { | ||
68 | CKEDITOR.fire( 'log', { type: 'warn', errorCode: errorCode, additionalData: additionalData } ); | ||
69 | } | ||
70 | }; | ||
71 | |||
72 | /** | ||
73 | * Error reporting function. When {@link CKEDITOR#verbosity} has {@link CKEDITOR#VERBOSITY_ERROR} flag set, it fires | ||
74 | * {@link CKEDITOR#log} event with the type set to `error`. The fired event also contains the provided `errorCode` and | ||
75 | * `additionalData`. | ||
76 | * | ||
77 | * @since 4.5.4 | ||
78 | * @member CKEDITOR | ||
79 | * @param {String} errorCode Error code describing the reported problem. | ||
80 | * @param {Object} [additionalData] Additional data associated with the reported problem. | ||
81 | */ | ||
82 | CKEDITOR.error = function( errorCode, additionalData ) { | ||
83 | if ( CKEDITOR.verbosity & CKEDITOR.VERBOSITY_ERROR ) { | ||
84 | CKEDITOR.fire( 'log', { type: 'error', errorCode: errorCode, additionalData: additionalData } ); | ||
85 | } | ||
86 | }; | ||
87 | |||
88 | /** | ||
89 | * Fired by {@link CKEDITOR#warn} and {@link CKEDITOR#error} methods. | ||
90 | * Default listener logs provided information to the console. | ||
91 | * | ||
92 | * This event can be used to provide a custom error/warning handler: | ||
93 | * | ||
94 | * CKEDTIOR.on( 'log', function( evt ) { | ||
95 | * // Cancel default listener. | ||
96 | * evt.cancel(); | ||
97 | * // Log event data. | ||
98 | * console.log( evt.data.type, evt.data.errorCode, evt.data.additionalData ); | ||
99 | * } ); | ||
100 | * | ||
101 | * @since 4.5.4 | ||
102 | * @event log | ||
103 | * @member CKEDITOR | ||
104 | * @param data | ||
105 | * @param {String} data.type Log type. Can be `error` or `warn`. | ||
106 | * @param {String} data.errorCode Error code describing the reported problem. | ||
107 | * @param {Object} [data.additionalData] Additional data associated with this log event. | ||
108 | */ | ||
109 | CKEDITOR.on( 'log', function( evt ) { | ||
110 | if ( !window.console || !window.console.log ) { | ||
111 | return; | ||
112 | } | ||
113 | |||
114 | var type = console[ evt.data.type ] ? evt.data.type : 'log', | ||
115 | errorCode = evt.data.errorCode, | ||
116 | additionalData = evt.data.additionalData, | ||
117 | prefix = '[CKEDITOR] ', | ||
118 | errorCodeLabel = 'Error code: '; | ||
119 | |||
120 | if ( additionalData ) { | ||
121 | console[ type ]( prefix + errorCodeLabel + errorCode + '.', additionalData ); | ||
122 | } else { | ||
123 | console[ type ]( prefix + errorCodeLabel + errorCode + '.' ); | ||
124 | } | ||
125 | |||
126 | console[ type ]( prefix + 'For more information about this error go to http://docs.ckeditor.com/#!/guide/dev_errors-section-' + errorCode ); | ||
127 | }, null, null, 999 ); | ||
diff --git a/sources/core/plugindefinition.js b/sources/core/plugindefinition.js new file mode 100644 index 0000000..caff957 --- /dev/null +++ b/sources/core/plugindefinition.js | |||
@@ -0,0 +1,177 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Defines the "virtual" {@link CKEDITOR.pluginDefinition} class which | ||
8 | * contains the defintion of a plugin. This file serves documentation | ||
9 | * purposes only. | ||
10 | */ | ||
11 | |||
12 | /** | ||
13 | * A virtual class that just illustrates the features of plugin objects which are | ||
14 | * passed to the {@link CKEDITOR.plugins#add} method. | ||
15 | * | ||
16 | * This class is not really a part of the API, so its constructor should not be called. | ||
17 | * | ||
18 | * See also: | ||
19 | * | ||
20 | * * [The Plugin SDK](#!/guide/plugin_sdk_intro) | ||
21 | * * [Creating a CKEditor plugin in 20 Lines of Code](#!/guide/plugin_sdk_sample) | ||
22 | * * [Creating a Simple Plugin Tutorial](#!/guide/plugin_sdk_sample_1) | ||
23 | * | ||
24 | * @class CKEDITOR.pluginDefinition | ||
25 | * @abstract | ||
26 | */ | ||
27 | |||
28 | /** | ||
29 | * A list of plugins that are required by this plugin. Note that this property | ||
30 | * does not determine the loading order of the plugins. | ||
31 | * | ||
32 | * CKEDITOR.plugins.add( 'sample', { | ||
33 | * requires: 'button,selection' | ||
34 | * } ); | ||
35 | * | ||
36 | * Or: | ||
37 | * | ||
38 | * CKEDITOR.plugins.add( 'sample', { | ||
39 | * requires: [ 'button', 'selection' ] | ||
40 | * } ); | ||
41 | * | ||
42 | * @property {String/String[]} requires | ||
43 | */ | ||
44 | |||
45 | /** | ||
46 | * The list of language files available for this plugin. These files are stored inside | ||
47 | * the `lang` directory in the plugin directory, follow the name | ||
48 | * pattern of `langCode.js`, and contain the language definition created with | ||
49 | * {@link CKEDITOR.plugins#setLang}. | ||
50 | * | ||
51 | * When the plugin is being loaded, the editor checks this list to see if | ||
52 | * a language file in the current editor language ({@link CKEDITOR.editor#langCode}) | ||
53 | * is available, and if so, loads it. Otherwise, the file represented by the first item | ||
54 | * in the list is loaded. | ||
55 | * | ||
56 | * CKEDITOR.plugins.add( 'sample', { | ||
57 | * lang: 'en,fr' | ||
58 | * } ); | ||
59 | * | ||
60 | * Or: | ||
61 | * | ||
62 | * CKEDITOR.plugins.add( 'sample', { | ||
63 | * lang: [ 'en', 'fr' ] | ||
64 | * } ); | ||
65 | * | ||
66 | * @property {String/String[]} lang | ||
67 | */ | ||
68 | |||
69 | /** | ||
70 | * A function called when the plugin definition is loaded for the first time. | ||
71 | * It is usually used to execute some code once for the entire page, | ||
72 | * for instance code that uses the {@link CKEDITOR}'s methods such as the {@link CKEDITOR#addCss} method. | ||
73 | * | ||
74 | * CKEDITOR.plugins.add( 'sample', { | ||
75 | * onLoad: function() { | ||
76 | * CKEDITOR.addCss( '.cke_some_class { ... }' ); | ||
77 | * } | ||
78 | * } ); | ||
79 | * | ||
80 | * Read more about the initialization order in the {@link #init} method documentation. | ||
81 | * | ||
82 | * @method onLoad | ||
83 | */ | ||
84 | |||
85 | /** | ||
86 | * A function called on initialization of every editor instance created on the | ||
87 | * page before the {@link #init} call task. This feature makes it possible to | ||
88 | * initialize things that could be used in the `init` function of other plugins. | ||
89 | * | ||
90 | * CKEDITOR.plugins.add( 'sample1', { | ||
91 | * beforeInit: function( editor ) { | ||
92 | * editor.foo = 'bar'; | ||
93 | * } | ||
94 | * } ); | ||
95 | * | ||
96 | * CKEDITOR.plugins.add( 'sample2', { | ||
97 | * init: function( editor ) { | ||
98 | * // This will work regardless of order in which | ||
99 | * // plugins sample1 and sample2 where initialized. | ||
100 | * console.log( editor.foo ); // 'bar' | ||
101 | * } | ||
102 | * } ); | ||
103 | * | ||
104 | * Read more about the initialization order in the {@link #init} method documentation. | ||
105 | * | ||
106 | * @method beforeInit | ||
107 | * @param {CKEDITOR.editor} editor The editor instance being initialized. | ||
108 | */ | ||
109 | |||
110 | /** | ||
111 | * A function called on initialization of every editor instance created on the page. | ||
112 | * | ||
113 | * CKEDITOR.plugins.add( 'sample', { | ||
114 | * init: function( editor ) { | ||
115 | * console.log( 'Editor "' + editor.name + '" is being initialized!' ); | ||
116 | * } | ||
117 | * } ); | ||
118 | * | ||
119 | * Initialization order: | ||
120 | * | ||
121 | * 1. The {@link #beforeInit} methods of all enabled plugins are executed. | ||
122 | * 2. The {@link #init} methods of all enabled plugins are executed. | ||
123 | * 3. The {@link #afterInit} methods of all enabled plugins are executed. | ||
124 | * 4. The {@link CKEDITOR.editor#pluginsLoaded} event is fired. | ||
125 | * | ||
126 | * **Note:** The order in which the `init` methods are called does not depend on the plugins' {@link #requires requirements} | ||
127 | * or the order set in the {@link CKEDITOR.config#plugins} option. It may be random and therefore it is | ||
128 | * recommended to use the {@link #beforeInit} and {@link #afterInit} methods in order to ensure | ||
129 | * the right execution sequence. | ||
130 | * | ||
131 | * See also the {@link #onLoad} method. | ||
132 | * | ||
133 | * @method init | ||
134 | * @param {CKEDITOR.editor} editor The editor instance being initialized. | ||
135 | */ | ||
136 | |||
137 | /** | ||
138 | * A function called on initialization of every editor instance created on the | ||
139 | * page after the {@link #init} call task. This feature makes it possible to use things | ||
140 | * that were initialized in the `init` function of other plugins. | ||
141 | * | ||
142 | * CKEDITOR.plugins.add( 'sample1', { | ||
143 | * afterInit: function( editor ) { | ||
144 | * // This will work regardless of order in which | ||
145 | * // plugins sample1 and sample2 where initialized. | ||
146 | * console.log( editor.foo ); // 'bar' | ||
147 | * } | ||
148 | * } ); | ||
149 | * | ||
150 | * CKEDITOR.plugins.add( 'sample2', { | ||
151 | * init: function( editor ) { | ||
152 | * editor.foo = 'bar'; | ||
153 | * } | ||
154 | * } ); | ||
155 | * | ||
156 | * Read more about the initialization order in the {@link #init} method documentation. | ||
157 | * | ||
158 | * @method afterInit | ||
159 | * @param {CKEDITOR.editor} editor The editor instance being initialized. | ||
160 | */ | ||
161 | |||
162 | /** | ||
163 | * Announces the plugin as HiDPI-ready (optimized for high pixel density screens, e.g. *Retina*) | ||
164 | * by providing high-resolution icons and images. HiDPI icons must be twice as big | ||
165 | * (defaults are `16px x 16px`) and stored under `plugin_name/icons/hidpi/` directory. | ||
166 | * | ||
167 | * The common place for additional HiDPI images used by the plugin (**but not icons**) | ||
168 | * is the `plugin_name/images/hidpi/` directory. | ||
169 | * | ||
170 | * This property is optional and only makes sense if `32px x 32px` icons | ||
171 | * and high-resolution images actually exist. If this flag is set to `true`, the editor | ||
172 | * will automatically detect the HiDPI environment and attempt to load the | ||
173 | * high-resolution resources. | ||
174 | * | ||
175 | * @since 4.2 | ||
176 | * @property {Boolean} hidpi | ||
177 | */ | ||
diff --git a/sources/core/plugins.js b/sources/core/plugins.js new file mode 100644 index 0000000..8e6c952 --- /dev/null +++ b/sources/core/plugins.js | |||
@@ -0,0 +1,119 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.plugins} object, which is used to | ||
8 | * manage plugins registration and loading. | ||
9 | */ | ||
10 | |||
11 | /** | ||
12 | * Manages plugins registration and loading. | ||
13 | * | ||
14 | * @class | ||
15 | * @extends CKEDITOR.resourceManager | ||
16 | * @singleton | ||
17 | */ | ||
18 | CKEDITOR.plugins = new CKEDITOR.resourceManager( 'plugins/', 'plugin' ); | ||
19 | |||
20 | // PACKAGER_RENAME( CKEDITOR.plugins ) | ||
21 | |||
22 | CKEDITOR.plugins.load = CKEDITOR.tools.override( CKEDITOR.plugins.load, function( originalLoad ) { | ||
23 | var initialized = {}; | ||
24 | |||
25 | return function( name, callback, scope ) { | ||
26 | var allPlugins = {}; | ||
27 | |||
28 | var loadPlugins = function( names ) { | ||
29 | originalLoad.call( this, names, function( plugins ) { | ||
30 | CKEDITOR.tools.extend( allPlugins, plugins ); | ||
31 | |||
32 | var requiredPlugins = []; | ||
33 | for ( var pluginName in plugins ) { | ||
34 | var plugin = plugins[ pluginName ], | ||
35 | requires = plugin && plugin.requires; | ||
36 | |||
37 | if ( !initialized[ pluginName ] ) { | ||
38 | // Register all icons eventually defined by this plugin. | ||
39 | if ( plugin.icons ) { | ||
40 | var icons = plugin.icons.split( ',' ); | ||
41 | for ( var ic = icons.length; ic--; ) { | ||
42 | CKEDITOR.skin.addIcon( icons[ ic ], | ||
43 | plugin.path + | ||
44 | 'icons/' + | ||
45 | ( CKEDITOR.env.hidpi && plugin.hidpi ? 'hidpi/' : '' ) + | ||
46 | icons[ ic ] + | ||
47 | '.png' ); | ||
48 | } | ||
49 | } | ||
50 | initialized[ pluginName ] = 1; | ||
51 | } | ||
52 | |||
53 | if ( requires ) { | ||
54 | // Trasnform it into an array, if it's not one. | ||
55 | if ( requires.split ) | ||
56 | requires = requires.split( ',' ); | ||
57 | |||
58 | for ( var i = 0; i < requires.length; i++ ) { | ||
59 | if ( !allPlugins[ requires[ i ] ] ) | ||
60 | requiredPlugins.push( requires[ i ] ); | ||
61 | } | ||
62 | } | ||
63 | } | ||
64 | |||
65 | if ( requiredPlugins.length ) | ||
66 | loadPlugins.call( this, requiredPlugins ); | ||
67 | else { | ||
68 | // Call the "onLoad" function for all plugins. | ||
69 | for ( pluginName in allPlugins ) { | ||
70 | plugin = allPlugins[ pluginName ]; | ||
71 | if ( plugin.onLoad && !plugin.onLoad._called ) { | ||
72 | // Make it possible to return false from plugin::onLoad to disable it. | ||
73 | if ( plugin.onLoad() === false ) | ||
74 | delete allPlugins[ pluginName ]; | ||
75 | |||
76 | plugin.onLoad._called = 1; | ||
77 | } | ||
78 | } | ||
79 | |||
80 | // Call the callback. | ||
81 | if ( callback ) | ||
82 | callback.call( scope || window, allPlugins ); | ||
83 | } | ||
84 | }, this ); | ||
85 | |||
86 | }; | ||
87 | |||
88 | loadPlugins.call( this, name ); | ||
89 | }; | ||
90 | } ); | ||
91 | |||
92 | /** | ||
93 | * Loads a specific language file, or auto detect it. A callback is | ||
94 | * then called when the file gets loaded. | ||
95 | * | ||
96 | * CKEDITOR.plugins.setLang( 'myPlugin', 'en', { | ||
97 | * title: 'My plugin', | ||
98 | * selectOption: 'Please select an option' | ||
99 | * } ); | ||
100 | * | ||
101 | * @param {String} pluginName The name of the plugin to which the provided translation | ||
102 | * should be attached. | ||
103 | * @param {String} languageCode The code of the language translation provided. | ||
104 | * @param {Object} languageEntries An object that contains pairs of label and | ||
105 | * the respective translation. | ||
106 | */ | ||
107 | CKEDITOR.plugins.setLang = function( pluginName, languageCode, languageEntries ) { | ||
108 | var plugin = this.get( pluginName ), | ||
109 | pluginLangEntries = plugin.langEntries || ( plugin.langEntries = {} ), | ||
110 | pluginLang = plugin.lang || ( plugin.lang = [] ); | ||
111 | |||
112 | if ( pluginLang.split ) | ||
113 | pluginLang = pluginLang.split( ',' ); | ||
114 | |||
115 | if ( CKEDITOR.tools.indexOf( pluginLang, languageCode ) == -1 ) | ||
116 | pluginLang.push( languageCode ); | ||
117 | |||
118 | pluginLangEntries[ languageCode ] = languageEntries; | ||
119 | }; | ||
diff --git a/sources/core/resourcemanager.js b/sources/core/resourcemanager.js new file mode 100644 index 0000000..7ba88a8 --- /dev/null +++ b/sources/core/resourcemanager.js | |||
@@ -0,0 +1,228 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.resourceManager} class, which is | ||
8 | * the base for resource managers, like plugins. | ||
9 | */ | ||
10 | |||
11 | /** | ||
12 | * Base class for resource managers, like plugins. This class is not | ||
13 | * intended to be used out of the CKEditor core code. | ||
14 | * | ||
15 | * @class | ||
16 | * @constructor Creates a resourceManager class instance. | ||
17 | * @param {String} basePath The path for the resources folder. | ||
18 | * @param {String} fileName The name used for resource files. | ||
19 | */ | ||
20 | CKEDITOR.resourceManager = function( basePath, fileName ) { | ||
21 | /** | ||
22 | * The base directory containing all resources. | ||
23 | * | ||
24 | * @property {String} | ||
25 | */ | ||
26 | this.basePath = basePath; | ||
27 | |||
28 | /** | ||
29 | * The name used for resource files. | ||
30 | * | ||
31 | * @property {String} | ||
32 | */ | ||
33 | this.fileName = fileName; | ||
34 | |||
35 | /** | ||
36 | * Contains references to all resources that have already been registered | ||
37 | * with {@link #add}. | ||
38 | */ | ||
39 | this.registered = {}; | ||
40 | |||
41 | /** | ||
42 | * Contains references to all resources that have already been loaded | ||
43 | * with {@link #load}. | ||
44 | */ | ||
45 | this.loaded = {}; | ||
46 | |||
47 | /** | ||
48 | * Contains references to all resources that have already been registered | ||
49 | * with {@link #addExternal}. | ||
50 | */ | ||
51 | this.externals = {}; | ||
52 | |||
53 | /** | ||
54 | * @private | ||
55 | */ | ||
56 | this._ = { | ||
57 | // List of callbacks waiting for plugins to be loaded. | ||
58 | waitingList: {} | ||
59 | }; | ||
60 | }; | ||
61 | |||
62 | CKEDITOR.resourceManager.prototype = { | ||
63 | /** | ||
64 | * Registers a resource. | ||
65 | * | ||
66 | * CKEDITOR.plugins.add( 'sample', { ... plugin definition ... } ); | ||
67 | * | ||
68 | * @param {String} name The resource name. | ||
69 | * @param {Object} [definition] The resource definition. | ||
70 | * @see CKEDITOR.pluginDefinition | ||
71 | */ | ||
72 | add: function( name, definition ) { | ||
73 | if ( this.registered[ name ] ) | ||
74 | throw new Error( '[CKEDITOR.resourceManager.add] The resource name "' + name + '" is already registered.' ); | ||
75 | |||
76 | var resource = this.registered[ name ] = definition || {}; | ||
77 | resource.name = name; | ||
78 | resource.path = this.getPath( name ); | ||
79 | |||
80 | CKEDITOR.fire( name + CKEDITOR.tools.capitalize( this.fileName ) + 'Ready', resource ); | ||
81 | |||
82 | return this.get( name ); | ||
83 | }, | ||
84 | |||
85 | /** | ||
86 | * Gets the definition of a specific resource. | ||
87 | * | ||
88 | * var definition = CKEDITOR.plugins.get( 'sample' ); | ||
89 | * | ||
90 | * @param {String} name The resource name. | ||
91 | * @returns {Object} The registered object. | ||
92 | */ | ||
93 | get: function( name ) { | ||
94 | return this.registered[ name ] || null; | ||
95 | }, | ||
96 | |||
97 | /** | ||
98 | * Get the folder path for a specific loaded resource. | ||
99 | * | ||
100 | * alert( CKEDITOR.plugins.getPath( 'sample' ) ); // '<editor path>/plugins/sample/' | ||
101 | * | ||
102 | * @param {String} name The resource name. | ||
103 | * @returns {String} | ||
104 | */ | ||
105 | getPath: function( name ) { | ||
106 | var external = this.externals[ name ]; | ||
107 | return CKEDITOR.getUrl( ( external && external.dir ) || this.basePath + name + '/' ); | ||
108 | }, | ||
109 | |||
110 | /** | ||
111 | * Get the file path for a specific loaded resource. | ||
112 | * | ||
113 | * alert( CKEDITOR.plugins.getFilePath( 'sample' ) ); // '<editor path>/plugins/sample/plugin.js' | ||
114 | * | ||
115 | * @param {String} name The resource name. | ||
116 | * @returns {String} | ||
117 | */ | ||
118 | getFilePath: function( name ) { | ||
119 | var external = this.externals[ name ]; | ||
120 | return CKEDITOR.getUrl( this.getPath( name ) + ( external ? external.file : this.fileName + '.js' ) ); | ||
121 | }, | ||
122 | |||
123 | /** | ||
124 | * Registers one or more resources to be loaded from an external path | ||
125 | * instead of the core base path. | ||
126 | * | ||
127 | * // Loads a plugin from '/myplugin/samples/plugin.js'. | ||
128 | * CKEDITOR.plugins.addExternal( 'sample', '/myplugins/sample/' ); | ||
129 | * | ||
130 | * // Loads a plugin from '/myplugin/samples/my_plugin.js'. | ||
131 | * CKEDITOR.plugins.addExternal( 'sample', '/myplugins/sample/', 'my_plugin.js' ); | ||
132 | * | ||
133 | * // Loads a plugin from '/myplugin/samples/my_plugin.js'. | ||
134 | * CKEDITOR.plugins.addExternal( 'sample', '/myplugins/sample/my_plugin.js', '' ); | ||
135 | * | ||
136 | * @param {String} names The resource names, separated by commas. | ||
137 | * @param {String} path The path of the folder containing the resource. | ||
138 | * @param {String} [fileName] The resource file name. If not provided, the | ||
139 | * default name is used. If provided with a empty string, will implicitly indicates that `path` argument | ||
140 | * is already the full path. | ||
141 | */ | ||
142 | addExternal: function( names, path, fileName ) { | ||
143 | names = names.split( ',' ); | ||
144 | for ( var i = 0; i < names.length; i++ ) { | ||
145 | var name = names[ i ]; | ||
146 | |||
147 | // If "fileName" is not provided, we assume that it may be available | ||
148 | // in "path". Try to extract it in this case. | ||
149 | if ( !fileName ) { | ||
150 | path = path.replace( /[^\/]+$/, function( match ) { | ||
151 | fileName = match; | ||
152 | return ''; | ||
153 | } ); | ||
154 | } | ||
155 | |||
156 | this.externals[ name ] = { | ||
157 | dir: path, | ||
158 | |||
159 | // Use the default file name if there is no "fileName" and it | ||
160 | // was not found in "path". | ||
161 | file: fileName || ( this.fileName + '.js' ) | ||
162 | }; | ||
163 | } | ||
164 | }, | ||
165 | |||
166 | /** | ||
167 | * Loads one or more resources. | ||
168 | * | ||
169 | * CKEDITOR.plugins.load( 'myplugin', function( plugins ) { | ||
170 | * alert( plugins[ 'myplugin' ] ); // object | ||
171 | * } ); | ||
172 | * | ||
173 | * @param {String/Array} name The name of the resource to load. It may be a | ||
174 | * string with a single resource name, or an array with several names. | ||
175 | * @param {Function} callback A function to be called when all resources | ||
176 | * are loaded. The callback will receive an array containing all loaded names. | ||
177 | * @param {Object} [scope] The scope object to be used for the callback call. | ||
178 | */ | ||
179 | load: function( names, callback, scope ) { | ||
180 | // Ensure that we have an array of names. | ||
181 | if ( !CKEDITOR.tools.isArray( names ) ) | ||
182 | names = names ? [ names ] : []; | ||
183 | |||
184 | var loaded = this.loaded, | ||
185 | registered = this.registered, | ||
186 | urls = [], | ||
187 | urlsNames = {}, | ||
188 | resources = {}; | ||
189 | |||
190 | // Loop through all names. | ||
191 | for ( var i = 0; i < names.length; i++ ) { | ||
192 | var name = names[ i ]; | ||
193 | |||
194 | if ( !name ) | ||
195 | continue; | ||
196 | |||
197 | // If not available yet. | ||
198 | if ( !loaded[ name ] && !registered[ name ] ) { | ||
199 | var url = this.getFilePath( name ); | ||
200 | urls.push( url ); | ||
201 | if ( !( url in urlsNames ) ) | ||
202 | urlsNames[ url ] = []; | ||
203 | urlsNames[ url ].push( name ); | ||
204 | } else { | ||
205 | resources[ name ] = this.get( name ); | ||
206 | } | ||
207 | } | ||
208 | |||
209 | CKEDITOR.scriptLoader.load( urls, function( completed, failed ) { | ||
210 | if ( failed.length ) { | ||
211 | throw new Error( '[CKEDITOR.resourceManager.load] Resource name "' + urlsNames[ failed[ 0 ] ].join( ',' ) + | ||
212 | '" was not found at "' + failed[ 0 ] + '".' ); | ||
213 | } | ||
214 | |||
215 | for ( var i = 0; i < completed.length; i++ ) { | ||
216 | var nameList = urlsNames[ completed[ i ] ]; | ||
217 | for ( var j = 0; j < nameList.length; j++ ) { | ||
218 | var name = nameList[ j ]; | ||
219 | resources[ name ] = this.get( name ); | ||
220 | |||
221 | loaded[ name ] = 1; | ||
222 | } | ||
223 | } | ||
224 | |||
225 | callback.call( scope, resources ); | ||
226 | }, this ); | ||
227 | } | ||
228 | }; | ||
diff --git a/sources/core/scriptloader.js b/sources/core/scriptloader.js new file mode 100644 index 0000000..9ad536e --- /dev/null +++ b/sources/core/scriptloader.js | |||
@@ -0,0 +1,203 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.scriptLoader} object, used to load scripts | ||
8 | * asynchronously. | ||
9 | */ | ||
10 | |||
11 | /** | ||
12 | * Load scripts asynchronously. | ||
13 | * | ||
14 | * @class | ||
15 | * @singleton | ||
16 | */ | ||
17 | CKEDITOR.scriptLoader = ( function() { | ||
18 | var uniqueScripts = {}, | ||
19 | waitingList = {}; | ||
20 | |||
21 | return { | ||
22 | /** | ||
23 | * Loads one or more external script checking if not already loaded | ||
24 | * previously by this function. | ||
25 | * | ||
26 | * CKEDITOR.scriptLoader.load( '/myscript.js' ); | ||
27 | * | ||
28 | * CKEDITOR.scriptLoader.load( '/myscript.js', function( success ) { | ||
29 | * // Alerts true if the script has been properly loaded. | ||
30 | * // HTTP error 404 should return false. | ||
31 | * alert( success ); | ||
32 | * } ); | ||
33 | * | ||
34 | * CKEDITOR.scriptLoader.load( [ '/myscript1.js', '/myscript2.js' ], function( completed, failed ) { | ||
35 | * alert( 'Number of scripts loaded: ' + completed.length ); | ||
36 | * alert( 'Number of failures: ' + failed.length ); | ||
37 | * } ); | ||
38 | * | ||
39 | * @param {String/Array} scriptUrl One or more URLs pointing to the | ||
40 | * scripts to be loaded. | ||
41 | * @param {Function} [callback] A function to be called when the script | ||
42 | * is loaded and executed. If a string is passed to `scriptUrl`, a | ||
43 | * boolean parameter is passed to the callback, indicating the | ||
44 | * success of the load. If an array is passed instead, two arrays | ||
45 | * parameters are passed to the callback - the first contains the | ||
46 | * URLs that have been properly loaded and the second the failed ones. | ||
47 | * @param {Object} [scope] The scope (`this` reference) to be used for | ||
48 | * the callback call. Defaults to {@link CKEDITOR}. | ||
49 | * @param {Boolean} [showBusy] Changes the cursor of the document while | ||
50 | * the script is loaded. | ||
51 | */ | ||
52 | load: function( scriptUrl, callback, scope, showBusy ) { | ||
53 | var isString = ( typeof scriptUrl == 'string' ); | ||
54 | |||
55 | if ( isString ) | ||
56 | scriptUrl = [ scriptUrl ]; | ||
57 | |||
58 | if ( !scope ) | ||
59 | scope = CKEDITOR; | ||
60 | |||
61 | var scriptCount = scriptUrl.length, | ||
62 | completed = [], | ||
63 | failed = []; | ||
64 | |||
65 | var doCallback = function( success ) { | ||
66 | if ( callback ) { | ||
67 | if ( isString ) | ||
68 | callback.call( scope, success ); | ||
69 | else | ||
70 | callback.call( scope, completed, failed ); | ||
71 | } | ||
72 | }; | ||
73 | |||
74 | if ( scriptCount === 0 ) { | ||
75 | doCallback( true ); | ||
76 | return; | ||
77 | } | ||
78 | |||
79 | var checkLoaded = function( url, success ) { | ||
80 | ( success ? completed : failed ).push( url ); | ||
81 | |||
82 | if ( --scriptCount <= 0 ) { | ||
83 | showBusy && CKEDITOR.document.getDocumentElement().removeStyle( 'cursor' ); | ||
84 | doCallback( success ); | ||
85 | } | ||
86 | }; | ||
87 | |||
88 | var onLoad = function( url, success ) { | ||
89 | // Mark this script as loaded. | ||
90 | uniqueScripts[ url ] = 1; | ||
91 | |||
92 | // Get the list of callback checks waiting for this file. | ||
93 | var waitingInfo = waitingList[ url ]; | ||
94 | delete waitingList[ url ]; | ||
95 | |||
96 | // Check all callbacks waiting for this file. | ||
97 | for ( var i = 0; i < waitingInfo.length; i++ ) | ||
98 | waitingInfo[ i ]( url, success ); | ||
99 | }; | ||
100 | |||
101 | var loadScript = function( url ) { | ||
102 | if ( uniqueScripts[ url ] ) { | ||
103 | checkLoaded( url, true ); | ||
104 | return; | ||
105 | } | ||
106 | |||
107 | var waitingInfo = waitingList[ url ] || ( waitingList[ url ] = [] ); | ||
108 | waitingInfo.push( checkLoaded ); | ||
109 | |||
110 | // Load it only for the first request. | ||
111 | if ( waitingInfo.length > 1 ) | ||
112 | return; | ||
113 | |||
114 | // Create the <script> element. | ||
115 | var script = new CKEDITOR.dom.element( 'script' ); | ||
116 | script.setAttributes( { | ||
117 | type: 'text/javascript', | ||
118 | src: url | ||
119 | } ); | ||
120 | |||
121 | if ( callback ) { | ||
122 | if ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 ) { | ||
123 | // FIXME: For IE, we are not able to return false on error (like 404). | ||
124 | script.$.onreadystatechange = function() { | ||
125 | if ( script.$.readyState == 'loaded' || script.$.readyState == 'complete' ) { | ||
126 | script.$.onreadystatechange = null; | ||
127 | onLoad( url, true ); | ||
128 | } | ||
129 | }; | ||
130 | } else { | ||
131 | script.$.onload = function() { | ||
132 | // Some browsers, such as Safari, may call the onLoad function | ||
133 | // immediately. Which will break the loading sequence. (#3661) | ||
134 | setTimeout( function() { | ||
135 | onLoad( url, true ); | ||
136 | }, 0 ); | ||
137 | }; | ||
138 | |||
139 | // FIXME: Opera and Safari will not fire onerror. | ||
140 | script.$.onerror = function() { | ||
141 | onLoad( url, false ); | ||
142 | }; | ||
143 | } | ||
144 | } | ||
145 | |||
146 | // Append it to <head>. | ||
147 | script.appendTo( CKEDITOR.document.getHead() ); | ||
148 | |||
149 | CKEDITOR.fire( 'download', url ); // %REMOVE_LINE% | ||
150 | }; | ||
151 | |||
152 | showBusy && CKEDITOR.document.getDocumentElement().setStyle( 'cursor', 'wait' ); | ||
153 | for ( var i = 0; i < scriptCount; i++ ) { | ||
154 | loadScript( scriptUrl[ i ] ); | ||
155 | } | ||
156 | }, | ||
157 | |||
158 | /** | ||
159 | * Loads a script in a queue, so only one is loaded at the same time. | ||
160 | * | ||
161 | * @since 4.1.2 | ||
162 | * @param {String} scriptUrl URL pointing to the script to be loaded. | ||
163 | * @param {Function} [callback] A function to be called when the script | ||
164 | * is loaded and executed. A boolean parameter is passed to the callback, | ||
165 | * indicating the success of the load. | ||
166 | * | ||
167 | * @see CKEDITOR.scriptLoader#load | ||
168 | */ | ||
169 | queue: ( function() { | ||
170 | var pending = []; | ||
171 | |||
172 | // Loads the very first script from queue and removes it. | ||
173 | function loadNext() { | ||
174 | var script; | ||
175 | |||
176 | if ( ( script = pending[ 0 ] ) ) | ||
177 | this.load( script.scriptUrl, script.callback, CKEDITOR, 0 ); | ||
178 | } | ||
179 | |||
180 | return function( scriptUrl, callback ) { | ||
181 | var that = this; | ||
182 | |||
183 | // This callback calls the standard callback for the script | ||
184 | // and loads the very next script from pending list. | ||
185 | function callbackWrapper() { | ||
186 | callback && callback.apply( this, arguments ); | ||
187 | |||
188 | // Removed the just loaded script from the queue. | ||
189 | pending.shift(); | ||
190 | |||
191 | loadNext.call( that ); | ||
192 | } | ||
193 | |||
194 | // Let's add this script to the queue | ||
195 | pending.push( { scriptUrl: scriptUrl, callback: callbackWrapper } ); | ||
196 | |||
197 | // If the queue was empty, then start loading. | ||
198 | if ( pending.length == 1 ) | ||
199 | loadNext.call( this ); | ||
200 | }; | ||
201 | } )() | ||
202 | }; | ||
203 | } )(); | ||
diff --git a/sources/core/selection.js b/sources/core/selection.js new file mode 100644 index 0000000..573b890 --- /dev/null +++ b/sources/core/selection.js | |||
@@ -0,0 +1,2185 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | ( function() { | ||
7 | // #### checkSelectionChange : START | ||
8 | |||
9 | // The selection change check basically saves the element parent tree of | ||
10 | // the current node and check it on successive requests. If there is any | ||
11 | // change on the tree, then the selectionChange event gets fired. | ||
12 | function checkSelectionChange() { | ||
13 | // A possibly available fake-selection. | ||
14 | var sel = this._.fakeSelection, | ||
15 | realSel; | ||
16 | |||
17 | if ( sel ) { | ||
18 | realSel = this.getSelection( 1 ); | ||
19 | |||
20 | // If real (not locked/stored) selection was moved from hidden container, | ||
21 | // then the fake-selection must be invalidated. | ||
22 | if ( !realSel || !realSel.isHidden() ) { | ||
23 | // Remove the cache from fake-selection references in use elsewhere. | ||
24 | sel.reset(); | ||
25 | |||
26 | // Have the code using the native selection. | ||
27 | sel = 0; | ||
28 | } | ||
29 | } | ||
30 | |||
31 | // If not fake-selection is available then get the native selection. | ||
32 | if ( !sel ) { | ||
33 | sel = realSel || this.getSelection( 1 ); | ||
34 | |||
35 | // Editor may have no selection at all. | ||
36 | if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE ) | ||
37 | return; | ||
38 | } | ||
39 | |||
40 | this.fire( 'selectionCheck', sel ); | ||
41 | |||
42 | var currentPath = this.elementPath(); | ||
43 | if ( !currentPath.compare( this._.selectionPreviousPath ) ) { | ||
44 | // Cache the active element, which we'll eventually lose on Webkit. | ||
45 | if ( CKEDITOR.env.webkit ) | ||
46 | this._.previousActive = this.document.getActive(); | ||
47 | |||
48 | this._.selectionPreviousPath = currentPath; | ||
49 | this.fire( 'selectionChange', { selection: sel, path: currentPath } ); | ||
50 | } | ||
51 | } | ||
52 | |||
53 | var checkSelectionChangeTimer, checkSelectionChangeTimeoutPending; | ||
54 | |||
55 | function checkSelectionChangeTimeout() { | ||
56 | // Firing the "OnSelectionChange" event on every key press started to | ||
57 | // be too slow. This function guarantees that there will be at least | ||
58 | // 200ms delay between selection checks. | ||
59 | |||
60 | checkSelectionChangeTimeoutPending = true; | ||
61 | |||
62 | if ( checkSelectionChangeTimer ) | ||
63 | return; | ||
64 | |||
65 | checkSelectionChangeTimeoutExec.call( this ); | ||
66 | |||
67 | checkSelectionChangeTimer = CKEDITOR.tools.setTimeout( checkSelectionChangeTimeoutExec, 200, this ); | ||
68 | } | ||
69 | |||
70 | function checkSelectionChangeTimeoutExec() { | ||
71 | checkSelectionChangeTimer = null; | ||
72 | |||
73 | if ( checkSelectionChangeTimeoutPending ) { | ||
74 | // Call this with a timeout so the browser properly moves the | ||
75 | // selection after the mouseup. It happened that the selection was | ||
76 | // being moved after the mouseup when clicking inside selected text | ||
77 | // with Firefox. | ||
78 | CKEDITOR.tools.setTimeout( checkSelectionChange, 0, this ); | ||
79 | |||
80 | checkSelectionChangeTimeoutPending = false; | ||
81 | } | ||
82 | } | ||
83 | |||
84 | // #### checkSelectionChange : END | ||
85 | |||
86 | var isVisible = CKEDITOR.dom.walker.invisible( 1 ); | ||
87 | |||
88 | // May absorb the caret if: | ||
89 | // * is a visible node, | ||
90 | // * is a non-empty element (this rule will accept elements like <strong></strong> because they | ||
91 | // they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret). | ||
92 | // See #12621. | ||
93 | function mayAbsorbCaret( node ) { | ||
94 | if ( isVisible( node ) ) | ||
95 | return true; | ||
96 | |||
97 | if ( node.type == CKEDITOR.NODE_ELEMENT && !node.is( CKEDITOR.dtd.$empty ) ) | ||
98 | return true; | ||
99 | |||
100 | return false; | ||
101 | } | ||
102 | |||
103 | function rangeRequiresFix( range ) { | ||
104 | // Whether we must prevent from absorbing caret by this context node. | ||
105 | // Also checks whether there's an editable position next to that node. | ||
106 | function ctxRequiresFix( node, isAtEnd ) { | ||
107 | // It's ok for us if a text node absorbs the caret, because | ||
108 | // the caret container element isn't changed then. | ||
109 | if ( !node || node.type == CKEDITOR.NODE_TEXT ) | ||
110 | return false; | ||
111 | |||
112 | var testRng = range.clone(); | ||
113 | return testRng[ 'moveToElementEdit' + ( isAtEnd ? 'End' : 'Start' ) ]( node ); | ||
114 | } | ||
115 | |||
116 | // Range root must be the editable element, it's to avoid creating filler char | ||
117 | // on any temporary internal selection. | ||
118 | if ( !( range.root instanceof CKEDITOR.editable ) ) | ||
119 | return false; | ||
120 | |||
121 | var ct = range.startContainer; | ||
122 | |||
123 | var previous = range.getPreviousNode( mayAbsorbCaret, null, ct ), | ||
124 | next = range.getNextNode( mayAbsorbCaret, null, ct ); | ||
125 | |||
126 | // Any adjacent text container may absorb the caret, e.g. | ||
127 | // <p><strong>text</strong>^foo</p> | ||
128 | // <p>foo^<strong>text</strong></p> | ||
129 | // <div>^<p>foo</p></div> | ||
130 | if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) ) | ||
131 | return true; | ||
132 | |||
133 | // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (#7222) | ||
134 | // If you found this line confusing check #12655. | ||
135 | if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) ) | ||
136 | return true; | ||
137 | |||
138 | return false; | ||
139 | } | ||
140 | |||
141 | function createFillingCharSequenceNode( editable ) { | ||
142 | removeFillingCharSequenceNode( editable, false ); | ||
143 | |||
144 | var fillingChar = editable.getDocument().createText( fillingCharSequence ); | ||
145 | editable.setCustomData( 'cke-fillingChar', fillingChar ); | ||
146 | |||
147 | return fillingChar; | ||
148 | } | ||
149 | |||
150 | // Checks if a filling char has been used, eventualy removing it (#1272). | ||
151 | function checkFillingCharSequenceNodeReady( editable ) { | ||
152 | var fillingChar = editable.getCustomData( 'cke-fillingChar' ); | ||
153 | |||
154 | if ( fillingChar ) { | ||
155 | // Use this flag to avoid removing the filling char right after | ||
156 | // creating it. | ||
157 | if ( fillingChar.getCustomData( 'ready' ) ) { | ||
158 | removeFillingCharSequenceNode( editable ); | ||
159 | } else { | ||
160 | fillingChar.setCustomData( 'ready', 1 ); | ||
161 | } | ||
162 | } | ||
163 | } | ||
164 | |||
165 | function removeFillingCharSequenceNode( editable, keepSelection ) { | ||
166 | var fillingChar = editable && editable.removeCustomData( 'cke-fillingChar' ); | ||
167 | |||
168 | if ( fillingChar ) { | ||
169 | // Text selection position might get mangled by | ||
170 | // subsequent dom modification, save it now for restoring. (#8617) | ||
171 | if ( keepSelection !== false ) { | ||
172 | var sel = editable.getDocument().getSelection().getNative(), | ||
173 | // Be error proof. | ||
174 | range = sel && sel.type != 'None' && sel.getRangeAt( 0 ), | ||
175 | fillingCharSeqLength = fillingCharSequence.length; | ||
176 | |||
177 | // If there's some text other than the sequence in the FC text node and the range | ||
178 | // intersects with that node... | ||
179 | if ( fillingChar.getLength() > fillingCharSeqLength && range && range.intersectsNode( fillingChar.$ ) ) { | ||
180 | var bm = createNativeSelectionBookmark( sel ); | ||
181 | |||
182 | // Correct start offset anticipating the removal of FC. | ||
183 | if ( sel.anchorNode == fillingChar.$ && sel.anchorOffset > fillingCharSeqLength ) { | ||
184 | bm[ 0 ].offset -= fillingCharSeqLength; | ||
185 | } | ||
186 | |||
187 | // Correct end offset anticipating the removal of FC. | ||
188 | if ( sel.focusNode == fillingChar.$ && sel.focusOffset > fillingCharSeqLength ) { | ||
189 | bm[ 1 ].offset -= fillingCharSeqLength; | ||
190 | } | ||
191 | } | ||
192 | } | ||
193 | |||
194 | // We can't simply remove the filling node because the user | ||
195 | // will actually enlarge it when typing, so we just remove the | ||
196 | // invisible char from it. | ||
197 | fillingChar.setText( removeFillingCharSequenceString( fillingChar.getText(), 1 ) ); | ||
198 | |||
199 | // Restore the bookmark preserving selection's direction. | ||
200 | if ( bm ) { | ||
201 | moveNativeSelectionToBookmark( editable.getDocument().$, bm ); | ||
202 | } | ||
203 | } | ||
204 | } | ||
205 | |||
206 | // #13816 | ||
207 | function removeFillingCharSequenceString( str, nbspAware ) { | ||
208 | if ( nbspAware ) { | ||
209 | return str.replace( fillingCharSequenceRegExp, function( m, p ) { | ||
210 | // #10291 if filling char is followed by a space replace it with NBSP. | ||
211 | return p ? '\xa0' : ''; | ||
212 | } ); | ||
213 | } else { | ||
214 | return str.replace( fillingCharSequence, '' ); | ||
215 | } | ||
216 | } | ||
217 | |||
218 | function createNativeSelectionBookmark( sel ) { | ||
219 | return [ | ||
220 | { node: sel.anchorNode, offset: sel.anchorOffset }, | ||
221 | { node: sel.focusNode, offset: sel.focusOffset } | ||
222 | ]; | ||
223 | } | ||
224 | |||
225 | function moveNativeSelectionToBookmark( document, bm ) { | ||
226 | var sel = document.getSelection(), | ||
227 | range = document.createRange(); | ||
228 | |||
229 | range.setStart( bm[ 0 ].node, bm[ 0 ].offset ); | ||
230 | range.collapse( true ); | ||
231 | sel.removeAllRanges(); | ||
232 | sel.addRange( range ); | ||
233 | sel.extend( bm[ 1 ].node, bm[ 1 ].offset ); | ||
234 | } | ||
235 | |||
236 | // Creates cke_hidden_sel container and puts real selection there. | ||
237 | function hideSelection( editor ) { | ||
238 | var style = CKEDITOR.env.ie ? 'display:none' : 'position:fixed;top:0;left:-1000px', | ||
239 | hiddenEl = CKEDITOR.dom.element.createFromHtml( | ||
240 | '<div data-cke-hidden-sel="1" data-cke-temp="1" style="' + style + '"> </div>', | ||
241 | editor.document ); | ||
242 | |||
243 | editor.fire( 'lockSnapshot' ); | ||
244 | |||
245 | editor.editable().append( hiddenEl ); | ||
246 | |||
247 | // Always use real selection to avoid overriding locked one (http://dev.ckeditor.com/ticket/11104#comment:13). | ||
248 | var sel = editor.getSelection( 1 ), | ||
249 | range = editor.createRange(), | ||
250 | // Cancel selectionchange fired by selectRanges - prevent from firing selectionChange. | ||
251 | listener = sel.root.on( 'selectionchange', function( evt ) { | ||
252 | evt.cancel(); | ||
253 | }, null, null, 0 ); | ||
254 | |||
255 | range.setStartAt( hiddenEl, CKEDITOR.POSITION_AFTER_START ); | ||
256 | range.setEndAt( hiddenEl, CKEDITOR.POSITION_BEFORE_END ); | ||
257 | sel.selectRanges( [ range ] ); | ||
258 | |||
259 | listener.removeListener(); | ||
260 | |||
261 | editor.fire( 'unlockSnapshot' ); | ||
262 | |||
263 | // Set this value at the end, so reset() executed by selectRanges() | ||
264 | // will clean up old hidden selection container. | ||
265 | editor._.hiddenSelectionContainer = hiddenEl; | ||
266 | } | ||
267 | |||
268 | function removeHiddenSelectionContainer( editor ) { | ||
269 | var hiddenEl = editor._.hiddenSelectionContainer; | ||
270 | |||
271 | if ( hiddenEl ) { | ||
272 | var isDirty = editor.checkDirty(); | ||
273 | |||
274 | editor.fire( 'lockSnapshot' ); | ||
275 | hiddenEl.remove(); | ||
276 | editor.fire( 'unlockSnapshot' ); | ||
277 | |||
278 | !isDirty && editor.resetDirty(); | ||
279 | } | ||
280 | |||
281 | delete editor._.hiddenSelectionContainer; | ||
282 | } | ||
283 | |||
284 | // Object containing keystroke handlers for fake selection. | ||
285 | var fakeSelectionDefaultKeystrokeHandlers = ( function() { | ||
286 | function leave( right ) { | ||
287 | return function( evt ) { | ||
288 | var range = evt.editor.createRange(); | ||
289 | |||
290 | // Move selection only if there's a editable place for it. | ||
291 | // It no, then do nothing (keystroke will be blocked, widget selection kept). | ||
292 | if ( range.moveToClosestEditablePosition( evt.selected, right ) ) | ||
293 | evt.editor.getSelection().selectRanges( [ range ] ); | ||
294 | |||
295 | // Prevent default. | ||
296 | return false; | ||
297 | }; | ||
298 | } | ||
299 | |||
300 | function del( right ) { | ||
301 | return function( evt ) { | ||
302 | var editor = evt.editor, | ||
303 | range = editor.createRange(), | ||
304 | found; | ||
305 | |||
306 | // If haven't found place for caret on the default side, | ||
307 | // try to find it on the other side. | ||
308 | if ( !( found = range.moveToClosestEditablePosition( evt.selected, right ) ) ) | ||
309 | found = range.moveToClosestEditablePosition( evt.selected, !right ); | ||
310 | |||
311 | if ( found ) | ||
312 | editor.getSelection().selectRanges( [ range ] ); | ||
313 | |||
314 | // Save the state before removing selected element. | ||
315 | editor.fire( 'saveSnapshot' ); | ||
316 | |||
317 | evt.selected.remove(); | ||
318 | |||
319 | // Haven't found any editable space before removing element, | ||
320 | // try to place the caret anywhere (most likely, in empty editable). | ||
321 | if ( !found ) { | ||
322 | range.moveToElementEditablePosition( editor.editable() ); | ||
323 | editor.getSelection().selectRanges( [ range ] ); | ||
324 | } | ||
325 | |||
326 | editor.fire( 'saveSnapshot' ); | ||
327 | |||
328 | // Prevent default. | ||
329 | return false; | ||
330 | }; | ||
331 | } | ||
332 | |||
333 | var leaveLeft = leave(), | ||
334 | leaveRight = leave( 1 ); | ||
335 | |||
336 | return { | ||
337 | 37: leaveLeft, // LEFT | ||
338 | 38: leaveLeft, // UP | ||
339 | 39: leaveRight, // RIGHT | ||
340 | 40: leaveRight, // DOWN | ||
341 | 8: del(), // BACKSPACE | ||
342 | 46: del( 1 ) // DELETE | ||
343 | }; | ||
344 | } )(); | ||
345 | |||
346 | // Handle left, right, delete and backspace keystrokes next to non-editable elements | ||
347 | // by faking selection on them. | ||
348 | function getOnKeyDownListener( editor ) { | ||
349 | var keystrokes = { 37: 1, 39: 1, 8: 1, 46: 1 }; | ||
350 | |||
351 | return function( evt ) { | ||
352 | var keystroke = evt.data.getKeystroke(); | ||
353 | |||
354 | // Handle only left/right/del/bspace keys. | ||
355 | if ( !keystrokes[ keystroke ] ) | ||
356 | return; | ||
357 | |||
358 | var sel = editor.getSelection(), | ||
359 | ranges = sel.getRanges(), | ||
360 | range = ranges[ 0 ]; | ||
361 | |||
362 | // Handle only single range and it has to be collapsed. | ||
363 | if ( ranges.length != 1 || !range.collapsed ) | ||
364 | return; | ||
365 | |||
366 | var next = range[ keystroke < 38 ? 'getPreviousEditableNode' : 'getNextEditableNode' ](); | ||
367 | |||
368 | if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getAttribute( 'contenteditable' ) == 'false' ) { | ||
369 | editor.getSelection().fake( next ); | ||
370 | evt.data.preventDefault(); | ||
371 | evt.cancel(); | ||
372 | } | ||
373 | }; | ||
374 | } | ||
375 | |||
376 | // If fake selection should be applied this function will return instance of | ||
377 | // CKEDITOR.dom.element which should gain fake selection. | ||
378 | function getNonEditableFakeSelectionReceiver( ranges ) { | ||
379 | var enclosedNode, shrinkedNode, clone, range; | ||
380 | |||
381 | if ( ranges.length == 1 && !( range = ranges[ 0 ] ).collapsed && | ||
382 | ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) { | ||
383 | // So far we can't say that enclosed element is non-editable. Before checking, | ||
384 | // we'll shrink range (clone). Shrinking will stop on non-editable range, or | ||
385 | // innermost element (#11114). | ||
386 | clone = range.clone(); | ||
387 | clone.shrink( CKEDITOR.SHRINK_ELEMENT, true ); | ||
388 | |||
389 | // If shrinked range still encloses an element, check this one (shrink stops only on non-editable elements). | ||
390 | if ( ( shrinkedNode = clone.getEnclosedNode() ) && shrinkedNode.type == CKEDITOR.NODE_ELEMENT ) | ||
391 | enclosedNode = shrinkedNode; | ||
392 | |||
393 | if ( enclosedNode.getAttribute( 'contenteditable' ) == 'false' ) | ||
394 | return enclosedNode; | ||
395 | } | ||
396 | } | ||
397 | |||
398 | // Fix ranges which may end after hidden selection container. | ||
399 | // Note: this function may only be used if hidden selection container | ||
400 | // is not in DOM any more. | ||
401 | function fixRangesAfterHiddenSelectionContainer( ranges, root ) { | ||
402 | var range; | ||
403 | for ( var i = 0; i < ranges.length; ++i ) { | ||
404 | range = ranges[ i ]; | ||
405 | if ( range.endContainer.equals( root ) ) { | ||
406 | // We can use getChildCount() because hidden selection container is not in DOM. | ||
407 | range.endOffset = Math.min( range.endOffset, root.getChildCount() ); | ||
408 | } | ||
409 | } | ||
410 | } | ||
411 | |||
412 | // Extract only editable part or ranges. | ||
413 | // Note: this function modifies ranges list! | ||
414 | // @param {CKEDITOR.dom.rangeList} ranges | ||
415 | function extractEditableRanges( ranges ) { | ||
416 | for ( var i = 0; i < ranges.length; i++ ) { | ||
417 | var range = ranges[ i ]; | ||
418 | |||
419 | // Drop range spans inside one ready-only node. | ||
420 | var parent = range.getCommonAncestor(); | ||
421 | if ( parent.isReadOnly() ) | ||
422 | ranges.splice( i, 1 ); | ||
423 | |||
424 | if ( range.collapsed ) | ||
425 | continue; | ||
426 | |||
427 | // Range may start inside a non-editable element, | ||
428 | // replace the range start after it. | ||
429 | if ( range.startContainer.isReadOnly() ) { | ||
430 | var current = range.startContainer, | ||
431 | isElement; | ||
432 | |||
433 | while ( current ) { | ||
434 | isElement = current.type == CKEDITOR.NODE_ELEMENT; | ||
435 | |||
436 | if ( ( isElement && current.is( 'body' ) ) || !current.isReadOnly() ) | ||
437 | break; | ||
438 | |||
439 | if ( isElement && current.getAttribute( 'contentEditable' ) == 'false' ) | ||
440 | range.setStartAfter( current ); | ||
441 | |||
442 | current = current.getParent(); | ||
443 | } | ||
444 | } | ||
445 | |||
446 | var startContainer = range.startContainer, | ||
447 | endContainer = range.endContainer, | ||
448 | startOffset = range.startOffset, | ||
449 | endOffset = range.endOffset, | ||
450 | walkerRange = range.clone(); | ||
451 | |||
452 | // Enlarge range start/end with text node to avoid walker | ||
453 | // being DOM destructive, it doesn't interfere our checking | ||
454 | // of elements below as well. | ||
455 | if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) { | ||
456 | if ( startOffset >= startContainer.getLength() ) | ||
457 | walkerRange.setStartAfter( startContainer ); | ||
458 | else | ||
459 | walkerRange.setStartBefore( startContainer ); | ||
460 | } | ||
461 | |||
462 | if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) { | ||
463 | if ( !endOffset ) | ||
464 | walkerRange.setEndBefore( endContainer ); | ||
465 | else | ||
466 | walkerRange.setEndAfter( endContainer ); | ||
467 | } | ||
468 | |||
469 | // Looking for non-editable element inside the range. | ||
470 | var walker = new CKEDITOR.dom.walker( walkerRange ); | ||
471 | walker.evaluator = function( node ) { | ||
472 | if ( node.type == CKEDITOR.NODE_ELEMENT && node.isReadOnly() ) { | ||
473 | var newRange = range.clone(); | ||
474 | range.setEndBefore( node ); | ||
475 | |||
476 | // Drop collapsed range around read-only elements, | ||
477 | // it make sure the range list empty when selecting | ||
478 | // only non-editable elements. | ||
479 | if ( range.collapsed ) | ||
480 | ranges.splice( i--, 1 ); | ||
481 | |||
482 | // Avoid creating invalid range. | ||
483 | if ( !( node.getPosition( walkerRange.endContainer ) & CKEDITOR.POSITION_CONTAINS ) ) { | ||
484 | newRange.setStartAfter( node ); | ||
485 | if ( !newRange.collapsed ) | ||
486 | ranges.splice( i + 1, 0, newRange ); | ||
487 | } | ||
488 | |||
489 | return true; | ||
490 | } | ||
491 | |||
492 | return false; | ||
493 | }; | ||
494 | |||
495 | walker.next(); | ||
496 | } | ||
497 | |||
498 | return ranges; | ||
499 | } | ||
500 | |||
501 | // Setup all editor instances for the necessary selection hooks. | ||
502 | CKEDITOR.on( 'instanceCreated', function( ev ) { | ||
503 | var editor = ev.editor; | ||
504 | |||
505 | editor.on( 'contentDom', function() { | ||
506 | var doc = editor.document, | ||
507 | outerDoc = CKEDITOR.document, | ||
508 | editable = editor.editable(), | ||
509 | body = doc.getBody(), | ||
510 | html = doc.getDocumentElement(); | ||
511 | |||
512 | var isInline = editable.isInline(); | ||
513 | |||
514 | var restoreSel, | ||
515 | lastSel; | ||
516 | |||
517 | // Give the editable an initial selection on first focus, | ||
518 | // put selection at a consistent position at the start | ||
519 | // of the contents. (#9507) | ||
520 | if ( CKEDITOR.env.gecko ) { | ||
521 | editable.attachListener( editable, 'focus', function( evt ) { | ||
522 | evt.removeListener(); | ||
523 | |||
524 | if ( restoreSel !== 0 ) { | ||
525 | var nativ = editor.getSelection().getNative(); | ||
526 | // Do it only if the native selection is at an unwanted | ||
527 | // place (at the very start of the editable). #10119 | ||
528 | if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) { | ||
529 | var rng = editor.createRange(); | ||
530 | rng.moveToElementEditStart( editable ); | ||
531 | rng.select(); | ||
532 | } | ||
533 | } | ||
534 | }, null, null, -2 ); | ||
535 | } | ||
536 | |||
537 | // Plays the magic here to restore/save dom selection on editable focus/blur. | ||
538 | editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusIn' : 'focus', function() { | ||
539 | // On Webkit we use DOMFocusIn which is fired more often than focus - e.g. when moving from main editable | ||
540 | // to nested editable (or the opposite). Unlock selection all, but restore only when it was locked | ||
541 | // for the same active element, what will e.g. mean restoring after displaying dialog. | ||
542 | if ( restoreSel && CKEDITOR.env.webkit ) | ||
543 | restoreSel = editor._.previousActive && editor._.previousActive.equals( doc.getActive() ); | ||
544 | |||
545 | editor.unlockSelection( restoreSel ); | ||
546 | restoreSel = 0; | ||
547 | }, null, null, -1 ); | ||
548 | |||
549 | // Disable selection restoring when clicking in. | ||
550 | editable.attachListener( editable, 'mousedown', function() { | ||
551 | restoreSel = 0; | ||
552 | } ); | ||
553 | |||
554 | // Save a cloned version of current selection. | ||
555 | function saveSel() { | ||
556 | lastSel = new CKEDITOR.dom.selection( editor.getSelection() ); | ||
557 | lastSel.lock(); | ||
558 | } | ||
559 | |||
560 | // Browsers could loose the selection once the editable lost focus, | ||
561 | // in such case we need to reproduce it by saving a locked selection | ||
562 | // and restoring it upon focus gain. | ||
563 | if ( CKEDITOR.env.ie || isInline ) { | ||
564 | // For old IEs, we can retrieve the last correct DOM selection upon the "beforedeactivate" event. | ||
565 | // For the rest, a more frequent check is required for each selection change made. | ||
566 | if ( isMSSelection ) | ||
567 | editable.attachListener( editable, 'beforedeactivate', saveSel, null, null, -1 ); | ||
568 | else | ||
569 | editable.attachListener( editor, 'selectionCheck', saveSel, null, null, -1 ); | ||
570 | |||
571 | // Lock the selection and mark it to be restored. | ||
572 | // On Webkit we use DOMFocusOut which is fired more often than blur. I.e. it will also be | ||
573 | // fired when nested editable is blurred. | ||
574 | editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusOut' : 'blur', function() { | ||
575 | editor.lockSelection( lastSel ); | ||
576 | restoreSel = 1; | ||
577 | }, null, null, -1 ); | ||
578 | |||
579 | // Disable selection restoring when clicking in. | ||
580 | editable.attachListener( editable, 'mousedown', function() { | ||
581 | restoreSel = 0; | ||
582 | } ); | ||
583 | } | ||
584 | |||
585 | // The following selection-related fixes only apply to classic (`iframe`-based) editable. | ||
586 | if ( CKEDITOR.env.ie && !isInline ) { | ||
587 | var scroll; | ||
588 | editable.attachListener( editable, 'mousedown', function( evt ) { | ||
589 | // IE scrolls document to top on right mousedown | ||
590 | // when editor has no focus, remember this scroll | ||
591 | // position and revert it before context menu opens. (#5778) | ||
592 | if ( evt.data.$.button == 2 ) { | ||
593 | var sel = editor.document.getSelection(); | ||
594 | if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE ) | ||
595 | scroll = editor.window.getScrollPosition(); | ||
596 | } | ||
597 | } ); | ||
598 | |||
599 | editable.attachListener( editable, 'mouseup', function( evt ) { | ||
600 | // Restore recorded scroll position when needed on right mouseup. | ||
601 | if ( evt.data.$.button == 2 && scroll ) { | ||
602 | editor.document.$.documentElement.scrollLeft = scroll.x; | ||
603 | editor.document.$.documentElement.scrollTop = scroll.y; | ||
604 | } | ||
605 | scroll = null; | ||
606 | } ); | ||
607 | |||
608 | // When content doc is in standards mode, IE doesn't focus the editor when | ||
609 | // clicking at the region below body (on html element) content, we emulate | ||
610 | // the normal behavior on old IEs. (#1659, #7932) | ||
611 | if ( doc.$.compatMode != 'BackCompat' ) { | ||
612 | if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) { | ||
613 | html.on( 'mousedown', function( evt ) { | ||
614 | evt = evt.data; | ||
615 | |||
616 | // Expand the text range along with mouse move. | ||
617 | function onHover( evt ) { | ||
618 | evt = evt.data.$; | ||
619 | if ( textRng ) { | ||
620 | // Read the current cursor. | ||
621 | var rngEnd = body.$.createTextRange(); | ||
622 | |||
623 | moveRangeToPoint( rngEnd, evt.clientX, evt.clientY ); | ||
624 | |||
625 | // Handle drag directions. | ||
626 | textRng.setEndPoint( | ||
627 | startRng.compareEndPoints( 'StartToStart', rngEnd ) < 0 ? | ||
628 | 'EndToEnd' : 'StartToStart', rngEnd ); | ||
629 | |||
630 | // Update selection with new range. | ||
631 | textRng.select(); | ||
632 | } | ||
633 | } | ||
634 | |||
635 | function removeListeners() { | ||
636 | outerDoc.removeListener( 'mouseup', onSelectEnd ); | ||
637 | html.removeListener( 'mouseup', onSelectEnd ); | ||
638 | } | ||
639 | |||
640 | function onSelectEnd() { | ||
641 | html.removeListener( 'mousemove', onHover ); | ||
642 | removeListeners(); | ||
643 | |||
644 | // Make it in effect on mouse up. (#9022) | ||
645 | textRng.select(); | ||
646 | } | ||
647 | |||
648 | |||
649 | // We're sure that the click happens at the region | ||
650 | // below body, but not on scrollbar. | ||
651 | if ( evt.getTarget().is( 'html' ) && | ||
652 | evt.$.y < html.$.clientHeight && | ||
653 | evt.$.x < html.$.clientWidth ) { | ||
654 | // Start to build the text range. | ||
655 | var textRng = body.$.createTextRange(); | ||
656 | moveRangeToPoint( textRng, evt.$.clientX, evt.$.clientY ); | ||
657 | |||
658 | // Records the dragging start of the above text range. | ||
659 | var startRng = textRng.duplicate(); | ||
660 | |||
661 | html.on( 'mousemove', onHover ); | ||
662 | outerDoc.on( 'mouseup', onSelectEnd ); | ||
663 | html.on( 'mouseup', onSelectEnd ); | ||
664 | } | ||
665 | } ); | ||
666 | } | ||
667 | |||
668 | // It's much simpler for IE8+, we just need to reselect the reported range. | ||
669 | // This hack does not work on IE>=11 because there's no old selection&range APIs. | ||
670 | if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) { | ||
671 | html.on( 'mousedown', function( evt ) { | ||
672 | if ( evt.data.getTarget().is( 'html' ) ) { | ||
673 | // Limit the text selection mouse move inside of editable. (#9715) | ||
674 | outerDoc.on( 'mouseup', onSelectEnd ); | ||
675 | html.on( 'mouseup', onSelectEnd ); | ||
676 | } | ||
677 | } ); | ||
678 | } | ||
679 | } | ||
680 | } | ||
681 | |||
682 | // We check the selection change: | ||
683 | // 1. Upon "selectionchange" event from the editable element. (which might be faked event fired by our code) | ||
684 | // 2. After the accomplish of keyboard and mouse events. | ||
685 | editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor ); | ||
686 | editable.attachListener( editable, 'keyup', checkSelectionChangeTimeout, editor ); | ||
687 | // Always fire the selection change on focus gain. | ||
688 | // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and | ||
689 | // we need synchronization between those listeners to not lost cached editor._.previousActive property | ||
690 | // (which is updated on selectionCheck). | ||
691 | editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusIn' : 'focus', function() { | ||
692 | editor.forceNextSelectionCheck(); | ||
693 | editor.selectionChange( 1 ); | ||
694 | } ); | ||
695 | |||
696 | // #9699: On Webkit&Gecko in inline editor we have to check selection when it was changed | ||
697 | // by dragging and releasing mouse button outside editable. Dragging (mousedown) | ||
698 | // has to be initialized in editable, but for mouseup we listen on document element. | ||
699 | if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) { | ||
700 | var mouseDown; | ||
701 | editable.attachListener( editable, 'mousedown', function() { | ||
702 | mouseDown = 1; | ||
703 | } ); | ||
704 | editable.attachListener( doc.getDocumentElement(), 'mouseup', function() { | ||
705 | if ( mouseDown ) | ||
706 | checkSelectionChangeTimeout.call( editor ); | ||
707 | mouseDown = 0; | ||
708 | } ); | ||
709 | } | ||
710 | // In all other cases listen on simple mouseup over editable, as we did before #9699. | ||
711 | // | ||
712 | // Use document instead of editable in non-IEs for observing mouseup | ||
713 | // since editable won't fire the event if selection process started within iframe and ended out | ||
714 | // of the editor (#9851). | ||
715 | else { | ||
716 | editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor ); | ||
717 | } | ||
718 | |||
719 | if ( CKEDITOR.env.webkit ) { | ||
720 | // Before keystroke is handled by editor, check to remove the filling char. | ||
721 | editable.attachListener( doc, 'keydown', function( evt ) { | ||
722 | var key = evt.data.getKey(); | ||
723 | // Remove the filling char before some keys get | ||
724 | // executed, so they'll not get blocked by it. | ||
725 | switch ( key ) { | ||
726 | case 13: // ENTER | ||
727 | case 33: // PAGEUP | ||
728 | case 34: // PAGEDOWN | ||
729 | case 35: // HOME | ||
730 | case 36: // END | ||
731 | case 37: // LEFT-ARROW | ||
732 | case 39: // RIGHT-ARROW | ||
733 | case 8: // BACKSPACE | ||
734 | case 45: // INS | ||
735 | case 46: // DEl | ||
736 | removeFillingCharSequenceNode( editable ); | ||
737 | } | ||
738 | |||
739 | }, null, null, -1 ); | ||
740 | } | ||
741 | |||
742 | // Automatically select non-editable element when navigating into | ||
743 | // it by left/right or backspace/del keys. | ||
744 | editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 ); | ||
745 | |||
746 | function moveRangeToPoint( range, x, y ) { | ||
747 | // Error prune in IE7. (#9034, #9110) | ||
748 | try { | ||
749 | range.moveToPoint( x, y ); | ||
750 | } catch ( e ) {} | ||
751 | } | ||
752 | |||
753 | function removeListeners() { | ||
754 | outerDoc.removeListener( 'mouseup', onSelectEnd ); | ||
755 | html.removeListener( 'mouseup', onSelectEnd ); | ||
756 | } | ||
757 | |||
758 | function onSelectEnd() { | ||
759 | removeListeners(); | ||
760 | |||
761 | // The event is not fired when clicking on the scrollbars, | ||
762 | // so we can safely check the following to understand | ||
763 | // whether the empty space following <body> has been clicked. | ||
764 | var sel = CKEDITOR.document.$.selection, | ||
765 | range = sel.createRange(); | ||
766 | |||
767 | // The selection range is reported on host, but actually it should applies to the content doc. | ||
768 | if ( sel.type != 'None' && range.parentElement().ownerDocument == doc.$ ) | ||
769 | range.select(); | ||
770 | } | ||
771 | } ); | ||
772 | |||
773 | editor.on( 'setData', function() { | ||
774 | // Invalidate locked selection when unloading DOM. | ||
775 | // (#9521, #5217#comment:32 and #11500#comment:11) | ||
776 | editor.unlockSelection(); | ||
777 | |||
778 | // Webkit's selection will mess up after the data loading. | ||
779 | if ( CKEDITOR.env.webkit ) | ||
780 | clearSelection(); | ||
781 | } ); | ||
782 | |||
783 | // Catch all the cases which above setData listener couldn't catch. | ||
784 | // For example: switching to source mode and destroying editor. | ||
785 | editor.on( 'contentDomUnload', function() { | ||
786 | editor.unlockSelection(); | ||
787 | } ); | ||
788 | |||
789 | // IE9 might cease to work if there's an object selection inside the iframe (#7639). | ||
790 | if ( CKEDITOR.env.ie9Compat ) | ||
791 | editor.on( 'beforeDestroy', clearSelection, null, null, 9 ); | ||
792 | |||
793 | // Check selection change on data reload. | ||
794 | editor.on( 'dataReady', function() { | ||
795 | // Clean up fake selection after setting data. | ||
796 | delete editor._.fakeSelection; | ||
797 | delete editor._.hiddenSelectionContainer; | ||
798 | |||
799 | editor.selectionChange( 1 ); | ||
800 | } ); | ||
801 | |||
802 | // When loaded data are ready check whether hidden selection container was not loaded. | ||
803 | editor.on( 'loadSnapshot', function() { | ||
804 | var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ), | ||
805 | // TODO replace with el.find() which will be introduced in #9764, | ||
806 | // because it may happen that hidden sel container won't be the last element. | ||
807 | last = editor.editable().getLast( isElement ); | ||
808 | |||
809 | if ( last && last.hasAttribute( 'data-cke-hidden-sel' ) ) { | ||
810 | last.remove(); | ||
811 | |||
812 | // Firefox does a very unfortunate thing. When a non-editable element is the only | ||
813 | // element in the editable, when we remove the hidden selection container, Firefox | ||
814 | // will insert a bogus <br> at the beginning of the editable... | ||
815 | // See: https://bugzilla.mozilla.org/show_bug.cgi?id=911201 | ||
816 | // | ||
817 | // This behavior is never desired because this <br> pushes the content lower, but in | ||
818 | // this case it is especially dangerous, because it happens when a bookmark is being restored. | ||
819 | // Since this <br> is inserted at the beginning it changes indexes and thus breaks the bookmark2 | ||
820 | // what results in errors. | ||
821 | // | ||
822 | // So... let's revert what Firefox broke. | ||
823 | if ( CKEDITOR.env.gecko ) { | ||
824 | var first = editor.editable().getFirst( isElement ); | ||
825 | if ( first && first.is( 'br' ) && first.getAttribute( '_moz_editor_bogus_node' ) ) { | ||
826 | first.remove(); | ||
827 | } | ||
828 | } | ||
829 | } | ||
830 | }, null, null, 100 ); | ||
831 | |||
832 | editor.on( 'key', function( evt ) { | ||
833 | if ( editor.mode != 'wysiwyg' ) | ||
834 | return; | ||
835 | |||
836 | var sel = editor.getSelection(); | ||
837 | if ( !sel.isFake ) | ||
838 | return; | ||
839 | |||
840 | var handler = fakeSelectionDefaultKeystrokeHandlers[ evt.data.keyCode ]; | ||
841 | if ( handler ) | ||
842 | return handler( { editor: editor, selected: sel.getSelectedElement(), selection: sel, keyEvent: evt } ); | ||
843 | } ); | ||
844 | |||
845 | function clearSelection() { | ||
846 | var sel = editor.getSelection(); | ||
847 | sel && sel.removeAllRanges(); | ||
848 | } | ||
849 | } ); | ||
850 | |||
851 | // On WebKit only, we need a special "filling" char on some situations | ||
852 | // (#1272). Here we set the events that should invalidate that char. | ||
853 | if ( CKEDITOR.env.webkit ) { | ||
854 | CKEDITOR.on( 'instanceReady', function( evt ) { | ||
855 | var editor = evt.editor; | ||
856 | |||
857 | editor.on( 'selectionChange', function() { | ||
858 | checkFillingCharSequenceNodeReady( editor.editable() ); | ||
859 | }, null, null, -1 ); | ||
860 | |||
861 | editor.on( 'beforeSetMode', function() { | ||
862 | removeFillingCharSequenceNode( editor.editable() ); | ||
863 | }, null, null, -1 ); | ||
864 | |||
865 | // Filter Undo snapshot's HTML to get rid of Filling Char Sequence. | ||
866 | // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's | ||
867 | // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (#13816). | ||
868 | editor.on( 'getSnapshot', function( evt ) { | ||
869 | if ( evt.data ) { | ||
870 | evt.data = removeFillingCharSequenceString( evt.data ); | ||
871 | } | ||
872 | }, editor, null, 20 ); | ||
873 | |||
874 | // Filter data to get rid of Filling Char Sequence. Filter on #toDataFormat | ||
875 | // instead of #getData because once removed, FCSeq may leave an empty element, | ||
876 | // which should be pruned by the dataProcessor (#13816). | ||
877 | // Note: Used low priority to filter when dataProcessor works on strings, | ||
878 | // not pseudo–DOM. | ||
879 | editor.on( 'toDataFormat', function( evt ) { | ||
880 | evt.data.dataValue = removeFillingCharSequenceString( evt.data.dataValue ); | ||
881 | }, null, null, 0 ); | ||
882 | } ); | ||
883 | } | ||
884 | |||
885 | /** | ||
886 | * Check the selection change in editor and potentially fires | ||
887 | * the {@link CKEDITOR.editor#event-selectionChange} event. | ||
888 | * | ||
889 | * @method | ||
890 | * @member CKEDITOR.editor | ||
891 | * @param {Boolean} [checkNow=false] Force the check to happen immediately | ||
892 | * instead of coming with a timeout delay (default). | ||
893 | */ | ||
894 | CKEDITOR.editor.prototype.selectionChange = function( checkNow ) { | ||
895 | ( checkNow ? checkSelectionChange : checkSelectionChangeTimeout ).call( this ); | ||
896 | }; | ||
897 | |||
898 | /** | ||
899 | * Retrieve the editor selection in scope of editable element. | ||
900 | * | ||
901 | * **Note:** Since the native browser selection provides only one single | ||
902 | * selection at a time per document, so if editor's editable element has lost focus, | ||
903 | * this method will return a null value unless the {@link CKEDITOR.editor#lockSelection} | ||
904 | * has been called beforehand so the saved selection is retrieved. | ||
905 | * | ||
906 | * var selection = CKEDITOR.instances.editor1.getSelection(); | ||
907 | * alert( selection.getType() ); | ||
908 | * | ||
909 | * @method | ||
910 | * @member CKEDITOR.editor | ||
911 | * @param {Boolean} forceRealSelection Return real selection, instead of saved or fake one. | ||
912 | * @returns {CKEDITOR.dom.selection} A selection object or null if not available for the moment. | ||
913 | */ | ||
914 | CKEDITOR.editor.prototype.getSelection = function( forceRealSelection ) { | ||
915 | |||
916 | // Check if there exists a locked or fake selection. | ||
917 | if ( ( this._.savedSelection || this._.fakeSelection ) && !forceRealSelection ) | ||
918 | return this._.savedSelection || this._.fakeSelection; | ||
919 | |||
920 | // Editable element might be absent or editor might not be in a wysiwyg mode. | ||
921 | var editable = this.editable(); | ||
922 | return editable && this.mode == 'wysiwyg' ? new CKEDITOR.dom.selection( editable ) : null; | ||
923 | }; | ||
924 | |||
925 | /** | ||
926 | * Locks the selection made in the editor in order to make it possible to | ||
927 | * manipulate it without browser interference. A locked selection is | ||
928 | * cached and remains unchanged until it is released with the | ||
929 | * {@link CKEDITOR.editor#unlockSelection} method. | ||
930 | * | ||
931 | * @method | ||
932 | * @member CKEDITOR.editor | ||
933 | * @param {CKEDITOR.dom.selection} [sel] Specify the selection to be locked. | ||
934 | * @returns {Boolean} `true` if selection was locked. | ||
935 | */ | ||
936 | CKEDITOR.editor.prototype.lockSelection = function( sel ) { | ||
937 | sel = sel || this.getSelection( 1 ); | ||
938 | if ( sel.getType() != CKEDITOR.SELECTION_NONE ) { | ||
939 | !sel.isLocked && sel.lock(); | ||
940 | this._.savedSelection = sel; | ||
941 | return true; | ||
942 | } | ||
943 | return false; | ||
944 | }; | ||
945 | |||
946 | /** | ||
947 | * Unlocks the selection made in the editor and locked with the | ||
948 | * {@link CKEDITOR.editor#unlockSelection} method. An unlocked selection | ||
949 | * is no longer cached and can be changed. | ||
950 | * | ||
951 | * @method | ||
952 | * @member CKEDITOR.editor | ||
953 | * @param {Boolean} [restore] If set to `true`, the selection is | ||
954 | * restored back to the selection saved earlier by using the | ||
955 | * {@link CKEDITOR.dom.selection#lock} method. | ||
956 | */ | ||
957 | CKEDITOR.editor.prototype.unlockSelection = function( restore ) { | ||
958 | var sel = this._.savedSelection; | ||
959 | if ( sel ) { | ||
960 | sel.unlock( restore ); | ||
961 | delete this._.savedSelection; | ||
962 | return true; | ||
963 | } | ||
964 | |||
965 | return false; | ||
966 | }; | ||
967 | |||
968 | /** | ||
969 | * @method | ||
970 | * @member CKEDITOR.editor | ||
971 | * @todo | ||
972 | */ | ||
973 | CKEDITOR.editor.prototype.forceNextSelectionCheck = function() { | ||
974 | delete this._.selectionPreviousPath; | ||
975 | }; | ||
976 | |||
977 | /** | ||
978 | * Gets the current selection in context of the document's body element. | ||
979 | * | ||
980 | * var selection = CKEDITOR.instances.editor1.document.getSelection(); | ||
981 | * alert( selection.getType() ); | ||
982 | * | ||
983 | * @method | ||
984 | * @member CKEDITOR.dom.document | ||
985 | * @returns {CKEDITOR.dom.selection} A selection object. | ||
986 | */ | ||
987 | CKEDITOR.dom.document.prototype.getSelection = function() { | ||
988 | return new CKEDITOR.dom.selection( this ); | ||
989 | }; | ||
990 | |||
991 | /** | ||
992 | * Select this range as the only one with {@link CKEDITOR.dom.selection#selectRanges}. | ||
993 | * | ||
994 | * @method | ||
995 | * @returns {CKEDITOR.dom.selection} | ||
996 | * @member CKEDITOR.dom.range | ||
997 | */ | ||
998 | CKEDITOR.dom.range.prototype.select = function() { | ||
999 | var sel = this.root instanceof CKEDITOR.editable ? this.root.editor.getSelection() : new CKEDITOR.dom.selection( this.root ); | ||
1000 | |||
1001 | sel.selectRanges( [ this ] ); | ||
1002 | |||
1003 | return sel; | ||
1004 | }; | ||
1005 | |||
1006 | /** | ||
1007 | * No selection. | ||
1008 | * | ||
1009 | * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE ) | ||
1010 | * alert( 'Nothing is selected' ); | ||
1011 | * | ||
1012 | * @readonly | ||
1013 | * @property {Number} [=1] | ||
1014 | * @member CKEDITOR | ||
1015 | */ | ||
1016 | CKEDITOR.SELECTION_NONE = 1; | ||
1017 | |||
1018 | /** | ||
1019 | * A text or a collapsed selection. | ||
1020 | * | ||
1021 | * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT ) | ||
1022 | * alert( 'A text is selected' ); | ||
1023 | * | ||
1024 | * @readonly | ||
1025 | * @property {Number} [=2] | ||
1026 | * @member CKEDITOR | ||
1027 | */ | ||
1028 | CKEDITOR.SELECTION_TEXT = 2; | ||
1029 | |||
1030 | /** | ||
1031 | * Element selection. | ||
1032 | * | ||
1033 | * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT ) | ||
1034 | * alert( 'An element is selected' ); | ||
1035 | * | ||
1036 | * @readonly | ||
1037 | * @property {Number} [=3] | ||
1038 | * @member CKEDITOR | ||
1039 | */ | ||
1040 | CKEDITOR.SELECTION_ELEMENT = 3; | ||
1041 | |||
1042 | var isMSSelection = typeof window.getSelection != 'function', | ||
1043 | nextRev = 1; | ||
1044 | |||
1045 | /** | ||
1046 | * Manipulates the selection within a DOM element. If the current browser selection | ||
1047 | * spans outside of the element, an empty selection object is returned. | ||
1048 | * | ||
1049 | * Despite the fact that selection's constructor allows to create selection instances, | ||
1050 | * usually it's better to get selection from the editor instance: | ||
1051 | * | ||
1052 | * var sel = editor.getSelection(); | ||
1053 | * | ||
1054 | * See {@link CKEDITOR.editor#getSelection}. | ||
1055 | * | ||
1056 | * @class | ||
1057 | * @constructor Creates a selection class instance. | ||
1058 | * | ||
1059 | * // Selection scoped in document. | ||
1060 | * var sel = new CKEDITOR.dom.selection( CKEDITOR.document ); | ||
1061 | * | ||
1062 | * // Selection scoped in element with 'editable' id. | ||
1063 | * var sel = new CKEDITOR.dom.selection( CKEDITOR.document.getById( 'editable' ) ); | ||
1064 | * | ||
1065 | * // Cloning selection. | ||
1066 | * var clone = new CKEDITOR.dom.selection( sel ); | ||
1067 | * | ||
1068 | * @param {CKEDITOR.dom.document/CKEDITOR.dom.element/CKEDITOR.dom.selection} target | ||
1069 | * The DOM document/element that the DOM selection is restrained to. Only selection which spans | ||
1070 | * within the target element is considered as valid. | ||
1071 | * | ||
1072 | * If {@link CKEDITOR.dom.selection} is passed, then its clone will be created. | ||
1073 | */ | ||
1074 | CKEDITOR.dom.selection = function( target ) { | ||
1075 | // Target is a selection - clone it. | ||
1076 | if ( target instanceof CKEDITOR.dom.selection ) { | ||
1077 | var selection = target; | ||
1078 | target = target.root; | ||
1079 | } | ||
1080 | |||
1081 | var isElement = target instanceof CKEDITOR.dom.element, | ||
1082 | root; | ||
1083 | |||
1084 | this.rev = selection ? selection.rev : nextRev++; | ||
1085 | this.document = target instanceof CKEDITOR.dom.document ? target : target.getDocument(); | ||
1086 | this.root = root = isElement ? target : this.document.getBody(); | ||
1087 | this.isLocked = 0; | ||
1088 | this._ = { | ||
1089 | cache: {} | ||
1090 | }; | ||
1091 | |||
1092 | // Clone selection. | ||
1093 | if ( selection ) { | ||
1094 | CKEDITOR.tools.extend( this._.cache, selection._.cache ); | ||
1095 | this.isFake = selection.isFake; | ||
1096 | this.isLocked = selection.isLocked; | ||
1097 | return this; | ||
1098 | } | ||
1099 | |||
1100 | // Check whether browser focus is really inside of the editable element. | ||
1101 | |||
1102 | var nativeSel = this.getNative(), | ||
1103 | rangeParent, | ||
1104 | range; | ||
1105 | |||
1106 | if ( nativeSel ) { | ||
1107 | if ( nativeSel.getRangeAt ) { | ||
1108 | range = nativeSel.rangeCount && nativeSel.getRangeAt( 0 ); | ||
1109 | rangeParent = range && new CKEDITOR.dom.node( range.commonAncestorContainer ); | ||
1110 | } | ||
1111 | // For old IEs. | ||
1112 | else { | ||
1113 | // Sometimes, mostly when selection is close to the table or hr, | ||
1114 | // IE throws "Unspecified error". | ||
1115 | try { | ||
1116 | range = nativeSel.createRange(); | ||
1117 | } catch ( err ) {} | ||
1118 | rangeParent = range && CKEDITOR.dom.element.get( range.item && range.item( 0 ) || range.parentElement() ); | ||
1119 | } | ||
1120 | } | ||
1121 | |||
1122 | // Selection out of concerned range, empty the selection. | ||
1123 | // TODO check whether this condition cannot be reverted to its old | ||
1124 | // form (commented out) after we closed #10438. | ||
1125 | //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) { | ||
1126 | if ( !( | ||
1127 | rangeParent && | ||
1128 | ( rangeParent.type == CKEDITOR.NODE_ELEMENT || rangeParent.type == CKEDITOR.NODE_TEXT ) && | ||
1129 | ( this.root.equals( rangeParent ) || this.root.contains( rangeParent ) ) | ||
1130 | ) ) { | ||
1131 | |||
1132 | this._.cache.type = CKEDITOR.SELECTION_NONE; | ||
1133 | this._.cache.startElement = null; | ||
1134 | this._.cache.selectedElement = null; | ||
1135 | this._.cache.selectedText = ''; | ||
1136 | this._.cache.ranges = new CKEDITOR.dom.rangeList(); | ||
1137 | } | ||
1138 | |||
1139 | return this; | ||
1140 | }; | ||
1141 | |||
1142 | var styleObjectElements = { img: 1, hr: 1, li: 1, table: 1, tr: 1, td: 1, th: 1, embed: 1, object: 1, ol: 1, ul: 1, | ||
1143 | a: 1, input: 1, form: 1, select: 1, textarea: 1, button: 1, fieldset: 1, thead: 1, tfoot: 1 }; | ||
1144 | |||
1145 | // #13816 | ||
1146 | var fillingCharSequence = CKEDITOR.tools.repeat( '\u200b', 7 ), | ||
1147 | fillingCharSequenceRegExp = new RegExp( fillingCharSequence + '( )?', 'g' ); | ||
1148 | |||
1149 | CKEDITOR.tools.extend( CKEDITOR.dom.selection, { | ||
1150 | _removeFillingCharSequenceString: removeFillingCharSequenceString, | ||
1151 | _createFillingCharSequenceNode: createFillingCharSequenceNode, | ||
1152 | |||
1153 | /** | ||
1154 | * The sequence used in a WebKit-based browser to create a Filling Character. By default it is | ||
1155 | * a string of 7 zero-width space characters (U+200B). | ||
1156 | * | ||
1157 | * @since 4.5.7 | ||
1158 | * @readonly | ||
1159 | * @property {String} | ||
1160 | */ | ||
1161 | FILLING_CHAR_SEQUENCE: fillingCharSequence | ||
1162 | } ); | ||
1163 | |||
1164 | CKEDITOR.dom.selection.prototype = { | ||
1165 | /** | ||
1166 | * Gets the native selection object from the browser. | ||
1167 | * | ||
1168 | * var selection = editor.getSelection().getNative(); | ||
1169 | * | ||
1170 | * @returns {Object} The native browser selection object. | ||
1171 | */ | ||
1172 | getNative: function() { | ||
1173 | if ( this._.cache.nativeSel !== undefined ) | ||
1174 | return this._.cache.nativeSel; | ||
1175 | |||
1176 | return ( this._.cache.nativeSel = isMSSelection ? this.document.$.selection : this.document.getWindow().$.getSelection() ); | ||
1177 | }, | ||
1178 | |||
1179 | /** | ||
1180 | * Gets the type of the current selection. The following values are | ||
1181 | * available: | ||
1182 | * | ||
1183 | * * {@link CKEDITOR#SELECTION_NONE} (1): No selection. | ||
1184 | * * {@link CKEDITOR#SELECTION_TEXT} (2): A text or a collapsed selection is selected. | ||
1185 | * * {@link CKEDITOR#SELECTION_ELEMENT} (3): An element is selected. | ||
1186 | * | ||
1187 | * Example: | ||
1188 | * | ||
1189 | * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT ) | ||
1190 | * alert( 'A text is selected' ); | ||
1191 | * | ||
1192 | * @method | ||
1193 | * @returns {Number} One of the following constant values: {@link CKEDITOR#SELECTION_NONE}, | ||
1194 | * {@link CKEDITOR#SELECTION_TEXT} or {@link CKEDITOR#SELECTION_ELEMENT}. | ||
1195 | */ | ||
1196 | getType: isMSSelection ? | ||
1197 | function() { | ||
1198 | var cache = this._.cache; | ||
1199 | if ( cache.type ) | ||
1200 | return cache.type; | ||
1201 | |||
1202 | var type = CKEDITOR.SELECTION_NONE; | ||
1203 | |||
1204 | try { | ||
1205 | var sel = this.getNative(), | ||
1206 | ieType = sel.type; | ||
1207 | |||
1208 | if ( ieType == 'Text' ) | ||
1209 | type = CKEDITOR.SELECTION_TEXT; | ||
1210 | |||
1211 | if ( ieType == 'Control' ) | ||
1212 | type = CKEDITOR.SELECTION_ELEMENT; | ||
1213 | |||
1214 | // It is possible that we can still get a text range | ||
1215 | // object even when type == 'None' is returned by IE. | ||
1216 | // So we'd better check the object returned by | ||
1217 | // createRange() rather than by looking at the type. | ||
1218 | if ( sel.createRange().parentElement() ) | ||
1219 | type = CKEDITOR.SELECTION_TEXT; | ||
1220 | } catch ( e ) {} | ||
1221 | |||
1222 | return ( cache.type = type ); | ||
1223 | } : function() { | ||
1224 | var cache = this._.cache; | ||
1225 | if ( cache.type ) | ||
1226 | return cache.type; | ||
1227 | |||
1228 | var type = CKEDITOR.SELECTION_TEXT; | ||
1229 | |||
1230 | var sel = this.getNative(); | ||
1231 | |||
1232 | if ( !( sel && sel.rangeCount ) ) | ||
1233 | type = CKEDITOR.SELECTION_NONE; | ||
1234 | else if ( sel.rangeCount == 1 ) { | ||
1235 | // Check if the actual selection is a control (IMG, | ||
1236 | // TABLE, HR, etc...). | ||
1237 | |||
1238 | var range = sel.getRangeAt( 0 ), | ||
1239 | startContainer = range.startContainer; | ||
1240 | |||
1241 | if ( startContainer == range.endContainer && startContainer.nodeType == 1 && | ||
1242 | ( range.endOffset - range.startOffset ) == 1 && | ||
1243 | styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] ) { | ||
1244 | type = CKEDITOR.SELECTION_ELEMENT; | ||
1245 | } | ||
1246 | |||
1247 | } | ||
1248 | |||
1249 | return ( cache.type = type ); | ||
1250 | }, | ||
1251 | |||
1252 | /** | ||
1253 | * Retrieves the {@link CKEDITOR.dom.range} instances that represent the current selection. | ||
1254 | * | ||
1255 | * Note: Some browsers return multiple ranges even for a continuous selection. Firefox, for example, returns | ||
1256 | * one range for each table cell when one or more table rows are selected. | ||
1257 | * | ||
1258 | * var ranges = selection.getRanges(); | ||
1259 | * alert( ranges.length ); | ||
1260 | * | ||
1261 | * @method | ||
1262 | * @param {Boolean} [onlyEditables] If set to `true`, this function retrives editable ranges only. | ||
1263 | * @returns {Array} Range instances that represent the current selection. | ||
1264 | */ | ||
1265 | getRanges: ( function() { | ||
1266 | var func = isMSSelection ? ( function() { | ||
1267 | function getNodeIndex( node ) { | ||
1268 | return new CKEDITOR.dom.node( node ).getIndex(); | ||
1269 | } | ||
1270 | |||
1271 | // Finds the container and offset for a specific boundary | ||
1272 | // of an IE range. | ||
1273 | var getBoundaryInformation = function( range, start ) { | ||
1274 | // Creates a collapsed range at the requested boundary. | ||
1275 | range = range.duplicate(); | ||
1276 | range.collapse( start ); | ||
1277 | |||
1278 | // Gets the element that encloses the range entirely. | ||
1279 | var parent = range.parentElement(); | ||
1280 | |||
1281 | // Empty parent element, e.g. <i>^</i> | ||
1282 | if ( !parent.hasChildNodes() ) | ||
1283 | return { container: parent, offset: 0 }; | ||
1284 | |||
1285 | var siblings = parent.children, | ||
1286 | child, sibling, | ||
1287 | testRange = range.duplicate(), | ||
1288 | startIndex = 0, | ||
1289 | endIndex = siblings.length - 1, | ||
1290 | index = -1, | ||
1291 | position, distance, container; | ||
1292 | |||
1293 | // Binary search over all element childs to test the range to see whether | ||
1294 | // range is right on the boundary of one element. | ||
1295 | while ( startIndex <= endIndex ) { | ||
1296 | index = Math.floor( ( startIndex + endIndex ) / 2 ); | ||
1297 | child = siblings[ index ]; | ||
1298 | testRange.moveToElementText( child ); | ||
1299 | position = testRange.compareEndPoints( 'StartToStart', range ); | ||
1300 | |||
1301 | if ( position > 0 ) | ||
1302 | endIndex = index - 1; | ||
1303 | else if ( position < 0 ) | ||
1304 | startIndex = index + 1; | ||
1305 | else | ||
1306 | return { container: parent, offset: getNodeIndex( child ) }; | ||
1307 | } | ||
1308 | |||
1309 | // All childs are text nodes, | ||
1310 | // or to the right hand of test range are all text nodes. (#6992) | ||
1311 | if ( index == -1 || index == siblings.length - 1 && position < 0 ) { | ||
1312 | // Adapt test range to embrace the entire parent contents. | ||
1313 | testRange.moveToElementText( parent ); | ||
1314 | testRange.setEndPoint( 'StartToStart', range ); | ||
1315 | |||
1316 | // IE report line break as CRLF with range.text but | ||
1317 | // only LF with textnode.nodeValue, normalize them to avoid | ||
1318 | // breaking character counting logic below. (#3949) | ||
1319 | distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; | ||
1320 | |||
1321 | siblings = parent.childNodes; | ||
1322 | |||
1323 | // Actual range anchor right beside test range at the boundary of text node. | ||
1324 | if ( !distance ) { | ||
1325 | child = siblings[ siblings.length - 1 ]; | ||
1326 | |||
1327 | if ( child.nodeType != CKEDITOR.NODE_TEXT ) | ||
1328 | return { container: parent, offset: siblings.length }; | ||
1329 | else | ||
1330 | return { container: child, offset: child.nodeValue.length }; | ||
1331 | } | ||
1332 | |||
1333 | // Start the measuring until distance overflows, meanwhile count the text nodes. | ||
1334 | var i = siblings.length; | ||
1335 | while ( distance > 0 && i > 0 ) { | ||
1336 | sibling = siblings[ --i ]; | ||
1337 | if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) { | ||
1338 | container = sibling; | ||
1339 | distance -= sibling.nodeValue.length; | ||
1340 | } | ||
1341 | } | ||
1342 | |||
1343 | return { container: container, offset: -distance }; | ||
1344 | } | ||
1345 | // Test range was one offset beyond OR behind the anchored text node. | ||
1346 | else { | ||
1347 | // Adapt one side of test range to the actual range | ||
1348 | // for measuring the offset between them. | ||
1349 | testRange.collapse( position > 0 ? true : false ); | ||
1350 | testRange.setEndPoint( position > 0 ? 'StartToStart' : 'EndToStart', range ); | ||
1351 | |||
1352 | // IE report line break as CRLF with range.text but | ||
1353 | // only LF with textnode.nodeValue, normalize them to avoid | ||
1354 | // breaking character counting logic below. (#3949) | ||
1355 | distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; | ||
1356 | |||
1357 | // Actual range anchor right beside test range at the inner boundary of text node. | ||
1358 | if ( !distance ) | ||
1359 | return { container: parent, offset: getNodeIndex( child ) + ( position > 0 ? 0 : 1 ) }; | ||
1360 | |||
1361 | // Start the measuring until distance overflows, meanwhile count the text nodes. | ||
1362 | while ( distance > 0 ) { | ||
1363 | try { | ||
1364 | sibling = child[ position > 0 ? 'previousSibling' : 'nextSibling' ]; | ||
1365 | if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) { | ||
1366 | distance -= sibling.nodeValue.length; | ||
1367 | container = sibling; | ||
1368 | } | ||
1369 | child = sibling; | ||
1370 | } | ||
1371 | // Measurement in IE could be somtimes wrong because of <select> element. (#4611) | ||
1372 | catch ( e ) { | ||
1373 | return { container: parent, offset: getNodeIndex( child ) }; | ||
1374 | } | ||
1375 | } | ||
1376 | |||
1377 | return { container: container, offset: position > 0 ? -distance : container.nodeValue.length + distance }; | ||
1378 | } | ||
1379 | }; | ||
1380 | |||
1381 | return function() { | ||
1382 | // IE doesn't have range support (in the W3C way), so we | ||
1383 | // need to do some magic to transform selections into | ||
1384 | // CKEDITOR.dom.range instances. | ||
1385 | |||
1386 | var sel = this.getNative(), | ||
1387 | nativeRange = sel && sel.createRange(), | ||
1388 | type = this.getType(), | ||
1389 | range; | ||
1390 | |||
1391 | if ( !sel ) | ||
1392 | return []; | ||
1393 | |||
1394 | if ( type == CKEDITOR.SELECTION_TEXT ) { | ||
1395 | range = new CKEDITOR.dom.range( this.root ); | ||
1396 | |||
1397 | var boundaryInfo = getBoundaryInformation( nativeRange, true ); | ||
1398 | range.setStart( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); | ||
1399 | |||
1400 | boundaryInfo = getBoundaryInformation( nativeRange ); | ||
1401 | range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); | ||
1402 | |||
1403 | // Correct an invalid IE range case on empty list item. (#5850) | ||
1404 | if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING && range.endOffset <= range.startContainer.getIndex() ) | ||
1405 | range.collapse(); | ||
1406 | |||
1407 | return [ range ]; | ||
1408 | } else if ( type == CKEDITOR.SELECTION_ELEMENT ) { | ||
1409 | var retval = []; | ||
1410 | |||
1411 | for ( var i = 0; i < nativeRange.length; i++ ) { | ||
1412 | var element = nativeRange.item( i ), | ||
1413 | parentElement = element.parentNode, | ||
1414 | j = 0; | ||
1415 | |||
1416 | range = new CKEDITOR.dom.range( this.root ); | ||
1417 | |||
1418 | for ( ; j < parentElement.childNodes.length && parentElement.childNodes[ j ] != element; j++ ) { | ||
1419 | |||
1420 | } | ||
1421 | |||
1422 | range.setStart( new CKEDITOR.dom.node( parentElement ), j ); | ||
1423 | range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 ); | ||
1424 | retval.push( range ); | ||
1425 | } | ||
1426 | |||
1427 | return retval; | ||
1428 | } | ||
1429 | |||
1430 | return []; | ||
1431 | }; | ||
1432 | } )() : | ||
1433 | function() { | ||
1434 | // On browsers implementing the W3C range, we simply | ||
1435 | // tranform the native ranges in CKEDITOR.dom.range | ||
1436 | // instances. | ||
1437 | |||
1438 | var ranges = [], | ||
1439 | range, | ||
1440 | sel = this.getNative(); | ||
1441 | |||
1442 | if ( !sel ) | ||
1443 | return ranges; | ||
1444 | |||
1445 | for ( var i = 0; i < sel.rangeCount; i++ ) { | ||
1446 | var nativeRange = sel.getRangeAt( i ); | ||
1447 | |||
1448 | range = new CKEDITOR.dom.range( this.root ); | ||
1449 | |||
1450 | range.setStart( new CKEDITOR.dom.node( nativeRange.startContainer ), nativeRange.startOffset ); | ||
1451 | range.setEnd( new CKEDITOR.dom.node( nativeRange.endContainer ), nativeRange.endOffset ); | ||
1452 | ranges.push( range ); | ||
1453 | } | ||
1454 | return ranges; | ||
1455 | }; | ||
1456 | |||
1457 | return function( onlyEditables ) { | ||
1458 | var cache = this._.cache, | ||
1459 | ranges = cache.ranges; | ||
1460 | |||
1461 | if ( !ranges ) | ||
1462 | cache.ranges = ranges = new CKEDITOR.dom.rangeList( func.call( this ) ); | ||
1463 | |||
1464 | if ( !onlyEditables ) | ||
1465 | return ranges; | ||
1466 | |||
1467 | // Split range into multiple by read-only nodes. | ||
1468 | // Clone ranges array to avoid changing cached ranges (#11493). | ||
1469 | return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) ); | ||
1470 | }; | ||
1471 | } )(), | ||
1472 | |||
1473 | /** | ||
1474 | * Gets the DOM element in which the selection starts. | ||
1475 | * | ||
1476 | * var element = editor.getSelection().getStartElement(); | ||
1477 | * alert( element.getName() ); | ||
1478 | * | ||
1479 | * @returns {CKEDITOR.dom.element} The element at the beginning of the selection. | ||
1480 | */ | ||
1481 | getStartElement: function() { | ||
1482 | var cache = this._.cache; | ||
1483 | if ( cache.startElement !== undefined ) | ||
1484 | return cache.startElement; | ||
1485 | |||
1486 | var node; | ||
1487 | |||
1488 | switch ( this.getType() ) { | ||
1489 | case CKEDITOR.SELECTION_ELEMENT: | ||
1490 | return this.getSelectedElement(); | ||
1491 | |||
1492 | case CKEDITOR.SELECTION_TEXT: | ||
1493 | |||
1494 | var range = this.getRanges()[ 0 ]; | ||
1495 | |||
1496 | if ( range ) { | ||
1497 | if ( !range.collapsed ) { | ||
1498 | range.optimize(); | ||
1499 | |||
1500 | // Decrease the range content to exclude particial | ||
1501 | // selected node on the start which doesn't have | ||
1502 | // visual impact. ( #3231 ) | ||
1503 | while ( 1 ) { | ||
1504 | var startContainer = range.startContainer, | ||
1505 | startOffset = range.startOffset; | ||
1506 | // Limit the fix only to non-block elements.(#3950) | ||
1507 | if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() ) | ||
1508 | range.setStartAfter( startContainer ); | ||
1509 | else | ||
1510 | break; | ||
1511 | } | ||
1512 | |||
1513 | node = range.startContainer; | ||
1514 | |||
1515 | if ( node.type != CKEDITOR.NODE_ELEMENT ) | ||
1516 | return node.getParent(); | ||
1517 | |||
1518 | node = node.getChild( range.startOffset ); | ||
1519 | |||
1520 | if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) | ||
1521 | node = range.startContainer; | ||
1522 | else { | ||
1523 | var child = node.getFirst(); | ||
1524 | while ( child && child.type == CKEDITOR.NODE_ELEMENT ) { | ||
1525 | node = child; | ||
1526 | child = child.getFirst(); | ||
1527 | } | ||
1528 | } | ||
1529 | } else { | ||
1530 | node = range.startContainer; | ||
1531 | if ( node.type != CKEDITOR.NODE_ELEMENT ) | ||
1532 | node = node.getParent(); | ||
1533 | } | ||
1534 | |||
1535 | node = node.$; | ||
1536 | } | ||
1537 | } | ||
1538 | |||
1539 | return cache.startElement = ( node ? new CKEDITOR.dom.element( node ) : null ); | ||
1540 | }, | ||
1541 | |||
1542 | /** | ||
1543 | * Gets the currently selected element. | ||
1544 | * | ||
1545 | * var element = editor.getSelection().getSelectedElement(); | ||
1546 | * alert( element.getName() ); | ||
1547 | * | ||
1548 | * @returns {CKEDITOR.dom.element} The selected element. Null if no | ||
1549 | * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}. | ||
1550 | */ | ||
1551 | getSelectedElement: function() { | ||
1552 | var cache = this._.cache; | ||
1553 | if ( cache.selectedElement !== undefined ) | ||
1554 | return cache.selectedElement; | ||
1555 | |||
1556 | var self = this; | ||
1557 | |||
1558 | var node = CKEDITOR.tools.tryThese( | ||
1559 | // Is it native IE control type selection? | ||
1560 | function() { | ||
1561 | return self.getNative().createRange().item( 0 ); | ||
1562 | }, | ||
1563 | // Figure it out by checking if there's a single enclosed | ||
1564 | // node of the range. | ||
1565 | function() { | ||
1566 | var range = self.getRanges()[ 0 ].clone(), | ||
1567 | enclosed, selected; | ||
1568 | |||
1569 | // Check first any enclosed element, e.g. <ul>[<li><a href="#">item</a></li>]</ul> | ||
1570 | for ( var i = 2; i && !( ( enclosed = range.getEnclosedNode() ) && ( enclosed.type == CKEDITOR.NODE_ELEMENT ) && styleObjectElements[ enclosed.getName() ] && ( selected = enclosed ) ); i-- ) { | ||
1571 | // Then check any deep wrapped element, e.g. [<b><i><img /></i></b>] | ||
1572 | range.shrink( CKEDITOR.SHRINK_ELEMENT ); | ||
1573 | } | ||
1574 | |||
1575 | return selected && selected.$; | ||
1576 | } | ||
1577 | ); | ||
1578 | |||
1579 | return cache.selectedElement = ( node ? new CKEDITOR.dom.element( node ) : null ); | ||
1580 | }, | ||
1581 | |||
1582 | /** | ||
1583 | * Retrieves the text contained within the range. An empty string is returned for non-text selection. | ||
1584 | * | ||
1585 | * var text = editor.getSelection().getSelectedText(); | ||
1586 | * alert( text ); | ||
1587 | * | ||
1588 | * @since 3.6.1 | ||
1589 | * @returns {String} A string of text within the current selection. | ||
1590 | */ | ||
1591 | getSelectedText: function() { | ||
1592 | var cache = this._.cache; | ||
1593 | if ( cache.selectedText !== undefined ) | ||
1594 | return cache.selectedText; | ||
1595 | |||
1596 | var nativeSel = this.getNative(), | ||
1597 | text = isMSSelection ? nativeSel.type == 'Control' ? '' : nativeSel.createRange().text : nativeSel.toString(); | ||
1598 | |||
1599 | return ( cache.selectedText = text ); | ||
1600 | }, | ||
1601 | |||
1602 | /** | ||
1603 | * Locks the selection made in the editor in order to make it possible to | ||
1604 | * manipulate it without browser interference. A locked selection is | ||
1605 | * cached and remains unchanged until it is released with the {@link #unlock} method. | ||
1606 | * | ||
1607 | * editor.getSelection().lock(); | ||
1608 | */ | ||
1609 | lock: function() { | ||
1610 | // Call all cacheable function. | ||
1611 | this.getRanges(); | ||
1612 | this.getStartElement(); | ||
1613 | this.getSelectedElement(); | ||
1614 | this.getSelectedText(); | ||
1615 | |||
1616 | // The native selection is not available when locked. | ||
1617 | this._.cache.nativeSel = null; | ||
1618 | |||
1619 | this.isLocked = 1; | ||
1620 | }, | ||
1621 | |||
1622 | /** | ||
1623 | * @todo | ||
1624 | */ | ||
1625 | unlock: function( restore ) { | ||
1626 | if ( !this.isLocked ) | ||
1627 | return; | ||
1628 | |||
1629 | if ( restore ) { | ||
1630 | var selectedElement = this.getSelectedElement(), | ||
1631 | ranges = !selectedElement && this.getRanges(), | ||
1632 | faked = this.isFake; | ||
1633 | } | ||
1634 | |||
1635 | this.isLocked = 0; | ||
1636 | this.reset(); | ||
1637 | |||
1638 | if ( restore ) { | ||
1639 | // Saved selection may be outdated (e.g. anchored in offline nodes). | ||
1640 | // Avoid getting broken by such. | ||
1641 | var common = selectedElement || ranges[ 0 ] && ranges[ 0 ].getCommonAncestor(); | ||
1642 | if ( !( common && common.getAscendant( 'body', 1 ) ) ) | ||
1643 | return; | ||
1644 | |||
1645 | if ( faked ) | ||
1646 | this.fake( selectedElement ); | ||
1647 | else if ( selectedElement ) | ||
1648 | this.selectElement( selectedElement ); | ||
1649 | else | ||
1650 | this.selectRanges( ranges ); | ||
1651 | } | ||
1652 | }, | ||
1653 | |||
1654 | /** | ||
1655 | * Clears the selection cache. | ||
1656 | * | ||
1657 | * editor.getSelection().reset(); | ||
1658 | */ | ||
1659 | reset: function() { | ||
1660 | this._.cache = {}; | ||
1661 | this.isFake = 0; | ||
1662 | |||
1663 | var editor = this.root.editor; | ||
1664 | |||
1665 | // Invalidate any fake selection available in the editor. | ||
1666 | if ( editor && editor._.fakeSelection ) { | ||
1667 | // Test whether this selection is the one that was | ||
1668 | // faked or its clone. | ||
1669 | if ( this.rev == editor._.fakeSelection.rev ) { | ||
1670 | delete editor._.fakeSelection; | ||
1671 | |||
1672 | removeHiddenSelectionContainer( editor ); | ||
1673 | } | ||
1674 | else { | ||
1675 | CKEDITOR.warn( 'selection-fake-reset' ); | ||
1676 | } | ||
1677 | } | ||
1678 | |||
1679 | this.rev = nextRev++; | ||
1680 | }, | ||
1681 | |||
1682 | /** | ||
1683 | * Makes the current selection of type {@link CKEDITOR#SELECTION_ELEMENT} by enclosing the specified element. | ||
1684 | * | ||
1685 | * var element = editor.document.getById( 'sampleElement' ); | ||
1686 | * editor.getSelection().selectElement( element ); | ||
1687 | * | ||
1688 | * @param {CKEDITOR.dom.element} element The element to enclose in the selection. | ||
1689 | */ | ||
1690 | selectElement: function( element ) { | ||
1691 | var range = new CKEDITOR.dom.range( this.root ); | ||
1692 | range.setStartBefore( element ); | ||
1693 | range.setEndAfter( element ); | ||
1694 | this.selectRanges( [ range ] ); | ||
1695 | }, | ||
1696 | |||
1697 | /** | ||
1698 | * Clears the original selection and adds the specified ranges to the document selection. | ||
1699 | * | ||
1700 | * // Move selection to the end of the editable element. | ||
1701 | * var range = editor.createRange(); | ||
1702 | * range.moveToPosition( range.root, CKEDITOR.POSITION_BEFORE_END ); | ||
1703 | * editor.getSelection().selectRanges( [ ranges ] ); | ||
1704 | * | ||
1705 | * @param {Array} ranges An array of {@link CKEDITOR.dom.range} instances | ||
1706 | * representing ranges to be added to the document. | ||
1707 | */ | ||
1708 | selectRanges: function( ranges ) { | ||
1709 | var editor = this.root.editor, | ||
1710 | hadHiddenSelectionContainer = editor && editor._.hiddenSelectionContainer; | ||
1711 | |||
1712 | this.reset(); | ||
1713 | |||
1714 | // Check if there's a hiddenSelectionContainer in editable at some index. | ||
1715 | // Some ranges may be anchored after the hiddenSelectionContainer and, | ||
1716 | // once the container is removed while resetting the selection, they | ||
1717 | // may need new endOffset (one element less within the range) (#11021 #11393). | ||
1718 | if ( hadHiddenSelectionContainer ) | ||
1719 | fixRangesAfterHiddenSelectionContainer( ranges, this.root ); | ||
1720 | |||
1721 | if ( !ranges.length ) | ||
1722 | return; | ||
1723 | |||
1724 | // Refresh the locked selection. | ||
1725 | if ( this.isLocked ) { | ||
1726 | // making a new DOM selection will force the focus on editable in certain situation, | ||
1727 | // we have to save the currently focused element for later recovery. | ||
1728 | var focused = CKEDITOR.document.getActive(); | ||
1729 | this.unlock(); | ||
1730 | this.selectRanges( ranges ); | ||
1731 | this.lock(); | ||
1732 | // Return to the previously focused element. | ||
1733 | focused && !focused.equals( this.root ) && focused.focus(); | ||
1734 | return; | ||
1735 | } | ||
1736 | |||
1737 | // Handle special case - automatic fake selection on non-editable elements. | ||
1738 | var receiver = getNonEditableFakeSelectionReceiver( ranges ); | ||
1739 | |||
1740 | if ( receiver ) { | ||
1741 | this.fake( receiver ); | ||
1742 | return; | ||
1743 | } | ||
1744 | |||
1745 | if ( isMSSelection ) { | ||
1746 | var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), | ||
1747 | fillerTextRegex = /\ufeff|\u00a0/, | ||
1748 | nonCells = { table: 1, tbody: 1, tr: 1 }; | ||
1749 | |||
1750 | if ( ranges.length > 1 ) { | ||
1751 | // IE doesn't accept multiple ranges selection, so we join all into one. | ||
1752 | var last = ranges[ ranges.length - 1 ]; | ||
1753 | ranges[ 0 ].setEnd( last.endContainer, last.endOffset ); | ||
1754 | } | ||
1755 | |||
1756 | var range = ranges[ 0 ]; | ||
1757 | var collapsed = range.collapsed, | ||
1758 | isStartMarkerAlone, dummySpan, ieRange; | ||
1759 | |||
1760 | // Try to make a object selection, be careful with selecting phase element in IE | ||
1761 | // will breaks the selection in non-framed environment. | ||
1762 | var selected = range.getEnclosedNode(); | ||
1763 | if ( selected && selected.type == CKEDITOR.NODE_ELEMENT && selected.getName() in styleObjectElements && | ||
1764 | !( selected.is( 'a' ) && selected.getText() ) ) { | ||
1765 | try { | ||
1766 | ieRange = selected.$.createControlRange(); | ||
1767 | ieRange.addElement( selected.$ ); | ||
1768 | ieRange.select(); | ||
1769 | return; | ||
1770 | } catch ( er ) {} | ||
1771 | } | ||
1772 | |||
1773 | // IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g. | ||
1774 | // <table><tbody><tr>[<td>cell</b></td>... => <table><tbody><tr><td>[cell</td>... | ||
1775 | if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells || | ||
1776 | range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) { | ||
1777 | range.shrink( CKEDITOR.NODE_ELEMENT, true ); | ||
1778 | // The range might get collapsed (#7975). Update cached variable. | ||
1779 | collapsed = range.collapsed; | ||
1780 | } | ||
1781 | |||
1782 | var bookmark = range.createBookmark(); | ||
1783 | |||
1784 | // Create marker tags for the start and end boundaries. | ||
1785 | var startNode = bookmark.startNode; | ||
1786 | |||
1787 | var endNode; | ||
1788 | if ( !collapsed ) | ||
1789 | endNode = bookmark.endNode; | ||
1790 | |||
1791 | // Create the main range which will be used for the selection. | ||
1792 | ieRange = range.document.$.body.createTextRange(); | ||
1793 | |||
1794 | // Position the range at the start boundary. | ||
1795 | ieRange.moveToElementText( startNode.$ ); | ||
1796 | ieRange.moveStart( 'character', 1 ); | ||
1797 | |||
1798 | if ( endNode ) { | ||
1799 | // Create a tool range for the end. | ||
1800 | var ieRangeEnd = range.document.$.body.createTextRange(); | ||
1801 | |||
1802 | // Position the tool range at the end. | ||
1803 | ieRangeEnd.moveToElementText( endNode.$ ); | ||
1804 | |||
1805 | // Move the end boundary of the main range to match the tool range. | ||
1806 | ieRange.setEndPoint( 'EndToEnd', ieRangeEnd ); | ||
1807 | ieRange.moveEnd( 'character', -1 ); | ||
1808 | } else { | ||
1809 | // The isStartMarkerAlone logic comes from V2. It guarantees that the lines | ||
1810 | // will expand and that the cursor will be blinking on the right place. | ||
1811 | // Actually, we are using this flag just to avoid using this hack in all | ||
1812 | // situations, but just on those needed. | ||
1813 | var next = startNode.getNext( notWhitespaces ); | ||
1814 | var inPre = startNode.hasAscendant( 'pre' ); | ||
1815 | isStartMarkerAlone = ( !( next && next.getText && next.getText().match( fillerTextRegex ) ) && // already a filler there? | ||
1816 | ( inPre || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) ); | ||
1817 | |||
1818 | // Append a temporary <span></span> before the selection. | ||
1819 | // This is needed to avoid IE destroying selections inside empty | ||
1820 | // inline elements, like <b></b> (#253). | ||
1821 | // It is also needed when placing the selection right after an inline | ||
1822 | // element to avoid the selection moving inside of it. | ||
1823 | dummySpan = range.document.createElement( 'span' ); | ||
1824 | dummySpan.setHtml( '' ); // Zero Width No-Break Space (U+FEFF). See #1359. | ||
1825 | dummySpan.insertBefore( startNode ); | ||
1826 | |||
1827 | if ( isStartMarkerAlone ) { | ||
1828 | // To expand empty blocks or line spaces after <br>, we need | ||
1829 | // instead to have any char, which will be later deleted using the | ||
1830 | // selection. | ||
1831 | // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359) | ||
1832 | range.document.createText( '\ufeff' ).insertBefore( startNode ); | ||
1833 | } | ||
1834 | } | ||
1835 | |||
1836 | // Remove the markers (reset the position, because of the changes in the DOM tree). | ||
1837 | range.setStartBefore( startNode ); | ||
1838 | startNode.remove(); | ||
1839 | |||
1840 | if ( collapsed ) { | ||
1841 | if ( isStartMarkerAlone ) { | ||
1842 | // Move the selection start to include the temporary \ufeff. | ||
1843 | ieRange.moveStart( 'character', -1 ); | ||
1844 | |||
1845 | ieRange.select(); | ||
1846 | |||
1847 | // Remove our temporary stuff. | ||
1848 | range.document.$.selection.clear(); | ||
1849 | } else { | ||
1850 | ieRange.select(); | ||
1851 | } | ||
1852 | |||
1853 | range.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START ); | ||
1854 | dummySpan.remove(); | ||
1855 | } else { | ||
1856 | range.setEndBefore( endNode ); | ||
1857 | endNode.remove(); | ||
1858 | ieRange.select(); | ||
1859 | } | ||
1860 | } else { | ||
1861 | var sel = this.getNative(); | ||
1862 | |||
1863 | // getNative() returns null if iframe is "display:none" in FF. (#6577) | ||
1864 | if ( !sel ) | ||
1865 | return; | ||
1866 | |||
1867 | this.removeAllRanges(); | ||
1868 | |||
1869 | for ( var i = 0; i < ranges.length; i++ ) { | ||
1870 | // Joining sequential ranges introduced by | ||
1871 | // readonly elements protection. | ||
1872 | if ( i < ranges.length - 1 ) { | ||
1873 | var left = ranges[ i ], | ||
1874 | right = ranges[ i + 1 ], | ||
1875 | between = left.clone(); | ||
1876 | between.setStart( left.endContainer, left.endOffset ); | ||
1877 | between.setEnd( right.startContainer, right.startOffset ); | ||
1878 | |||
1879 | // Don't confused by Firefox adjancent multi-ranges | ||
1880 | // introduced by table cells selection. | ||
1881 | if ( !between.collapsed ) { | ||
1882 | between.shrink( CKEDITOR.NODE_ELEMENT, true ); | ||
1883 | var ancestor = between.getCommonAncestor(), | ||
1884 | enclosed = between.getEnclosedNode(); | ||
1885 | |||
1886 | // The following cases has to be considered: | ||
1887 | // 1. <span contenteditable="false">[placeholder]</span> | ||
1888 | // 2. <input contenteditable="false" type="radio"/> (#6621) | ||
1889 | if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) { | ||
1890 | right.setStart( left.startContainer, left.startOffset ); | ||
1891 | ranges.splice( i--, 1 ); | ||
1892 | continue; | ||
1893 | } | ||
1894 | } | ||
1895 | } | ||
1896 | |||
1897 | range = ranges[ i ]; | ||
1898 | |||
1899 | var nativeRange = this.document.$.createRange(); | ||
1900 | |||
1901 | if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) { | ||
1902 | // Append a zero-width space so WebKit will not try to | ||
1903 | // move the selection by itself (#1272). | ||
1904 | var fillingChar = createFillingCharSequenceNode( this.root ); | ||
1905 | range.insertNode( fillingChar ); | ||
1906 | |||
1907 | next = fillingChar.getNext(); | ||
1908 | |||
1909 | // If the filling char is followed by a <br>, whithout | ||
1910 | // having something before it, it'll not blink. | ||
1911 | // Let's remove it in this case. | ||
1912 | if ( next && !fillingChar.getPrevious() && next.type == CKEDITOR.NODE_ELEMENT && next.getName() == 'br' ) { | ||
1913 | removeFillingCharSequenceNode( this.root ); | ||
1914 | range.moveToPosition( next, CKEDITOR.POSITION_BEFORE_START ); | ||
1915 | } else { | ||
1916 | range.moveToPosition( fillingChar, CKEDITOR.POSITION_AFTER_END ); | ||
1917 | } | ||
1918 | } | ||
1919 | |||
1920 | nativeRange.setStart( range.startContainer.$, range.startOffset ); | ||
1921 | |||
1922 | try { | ||
1923 | nativeRange.setEnd( range.endContainer.$, range.endOffset ); | ||
1924 | } catch ( e ) { | ||
1925 | // There is a bug in Firefox implementation (it would be too easy | ||
1926 | // otherwise). The new start can't be after the end (W3C says it can). | ||
1927 | // So, let's create a new range and collapse it to the desired point. | ||
1928 | if ( e.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 ) { | ||
1929 | range.collapse( 1 ); | ||
1930 | nativeRange.setEnd( range.endContainer.$, range.endOffset ); | ||
1931 | } else { | ||
1932 | throw e; | ||
1933 | } | ||
1934 | } | ||
1935 | |||
1936 | // Select the range. | ||
1937 | sel.addRange( nativeRange ); | ||
1938 | } | ||
1939 | } | ||
1940 | |||
1941 | this.reset(); | ||
1942 | |||
1943 | // Fakes the IE DOM event "selectionchange" on editable. | ||
1944 | this.root.fire( 'selectionchange' ); | ||
1945 | }, | ||
1946 | |||
1947 | /** | ||
1948 | * Makes a "fake selection" of an element. | ||
1949 | * | ||
1950 | * A fake selection does not render UI artifacts over the selected | ||
1951 | * element. Additionally, the browser native selection system is not | ||
1952 | * aware of the fake selection. In practice, the native selection is | ||
1953 | * moved to a hidden place where no native selection UI artifacts are | ||
1954 | * displayed to the user. | ||
1955 | * | ||
1956 | * @param {CKEDITOR.dom.element} element The element to be "selected". | ||
1957 | */ | ||
1958 | fake: function( element ) { | ||
1959 | var editor = this.root.editor; | ||
1960 | |||
1961 | // Cleanup after previous selection - e.g. remove hidden sel container. | ||
1962 | this.reset(); | ||
1963 | |||
1964 | hideSelection( editor ); | ||
1965 | |||
1966 | // Set this value after executing hiseSelection, because it may | ||
1967 | // cause reset() which overwrites cache. | ||
1968 | var cache = this._.cache; | ||
1969 | |||
1970 | // Caches a range than holds the element. | ||
1971 | var range = new CKEDITOR.dom.range( this.root ); | ||
1972 | range.setStartBefore( element ); | ||
1973 | range.setEndAfter( element ); | ||
1974 | cache.ranges = new CKEDITOR.dom.rangeList( range ); | ||
1975 | |||
1976 | // Put this element in the cache. | ||
1977 | cache.selectedElement = cache.startElement = element; | ||
1978 | cache.type = CKEDITOR.SELECTION_ELEMENT; | ||
1979 | |||
1980 | // Properties that will not be available when isFake. | ||
1981 | cache.selectedText = cache.nativeSel = null; | ||
1982 | |||
1983 | this.isFake = 1; | ||
1984 | this.rev = nextRev++; | ||
1985 | |||
1986 | // Save this selection, so it can be returned by editor.getSelection(). | ||
1987 | editor._.fakeSelection = this; | ||
1988 | |||
1989 | // Fire selectionchange, just like a normal selection. | ||
1990 | this.root.fire( 'selectionchange' ); | ||
1991 | }, | ||
1992 | |||
1993 | /** | ||
1994 | * Checks whether selection is placed in hidden element. | ||
1995 | * | ||
1996 | * This method is to be used to verify whether fake selection | ||
1997 | * (see {@link #fake}) is still hidden. | ||
1998 | * | ||
1999 | * **Note:** this method should be executed on real selection - e.g.: | ||
2000 | * | ||
2001 | * editor.getSelection( true ).isHidden(); | ||
2002 | * | ||
2003 | * @returns {Boolean} | ||
2004 | */ | ||
2005 | isHidden: function() { | ||
2006 | var el = this.getCommonAncestor(); | ||
2007 | |||
2008 | if ( el && el.type == CKEDITOR.NODE_TEXT ) | ||
2009 | el = el.getParent(); | ||
2010 | |||
2011 | return !!( el && el.data( 'cke-hidden-sel' ) ); | ||
2012 | }, | ||
2013 | |||
2014 | /** | ||
2015 | * Creates a bookmark for each range of this selection (from {@link #getRanges}) | ||
2016 | * by calling the {@link CKEDITOR.dom.range#createBookmark} method, | ||
2017 | * with extra care taken to avoid interference among those ranges. The arguments | ||
2018 | * received are the same as with the underlying range method. | ||
2019 | * | ||
2020 | * var bookmarks = editor.getSelection().createBookmarks(); | ||
2021 | * | ||
2022 | * @returns {Array} Array of bookmarks for each range. | ||
2023 | */ | ||
2024 | createBookmarks: function( serializable ) { | ||
2025 | var bookmark = this.getRanges().createBookmarks( serializable ); | ||
2026 | this.isFake && ( bookmark.isFake = 1 ); | ||
2027 | return bookmark; | ||
2028 | }, | ||
2029 | |||
2030 | /** | ||
2031 | * Creates a bookmark for each range of this selection (from {@link #getRanges}) | ||
2032 | * by calling the {@link CKEDITOR.dom.range#createBookmark2} method, | ||
2033 | * with extra care taken to avoid interference among those ranges. The arguments | ||
2034 | * received are the same as with the underlying range method. | ||
2035 | * | ||
2036 | * var bookmarks = editor.getSelection().createBookmarks2(); | ||
2037 | * | ||
2038 | * @returns {Array} Array of bookmarks for each range. | ||
2039 | */ | ||
2040 | createBookmarks2: function( normalized ) { | ||
2041 | var bookmark = this.getRanges().createBookmarks2( normalized ); | ||
2042 | this.isFake && ( bookmark.isFake = 1 ); | ||
2043 | return bookmark; | ||
2044 | }, | ||
2045 | |||
2046 | /** | ||
2047 | * Selects the virtual ranges denoted by the bookmarks by calling {@link #selectRanges}. | ||
2048 | * | ||
2049 | * var bookmarks = editor.getSelection().createBookmarks(); | ||
2050 | * editor.getSelection().selectBookmarks( bookmarks ); | ||
2051 | * | ||
2052 | * @param {Array} bookmarks The bookmarks representing ranges to be selected. | ||
2053 | * @returns {CKEDITOR.dom.selection} This selection object, after the ranges were selected. | ||
2054 | */ | ||
2055 | selectBookmarks: function( bookmarks ) { | ||
2056 | var ranges = [], | ||
2057 | node; | ||
2058 | |||
2059 | for ( var i = 0; i < bookmarks.length; i++ ) { | ||
2060 | var range = new CKEDITOR.dom.range( this.root ); | ||
2061 | range.moveToBookmark( bookmarks[ i ] ); | ||
2062 | ranges.push( range ); | ||
2063 | } | ||
2064 | |||
2065 | // It may happen that the content change during loading, before selection is set so bookmark leads to text node. | ||
2066 | if ( bookmarks.isFake ) { | ||
2067 | node = ranges[ 0 ].getEnclosedNode(); | ||
2068 | if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) { | ||
2069 | CKEDITOR.warn( 'selection-not-fake' ); | ||
2070 | bookmarks.isFake = 0; | ||
2071 | } | ||
2072 | } | ||
2073 | |||
2074 | if ( bookmarks.isFake ) | ||
2075 | this.fake( node ); | ||
2076 | else | ||
2077 | this.selectRanges( ranges ); | ||
2078 | |||
2079 | return this; | ||
2080 | }, | ||
2081 | |||
2082 | /** | ||
2083 | * Retrieves the common ancestor node of the first range and the last range. | ||
2084 | * | ||
2085 | * var ancestor = editor.getSelection().getCommonAncestor(); | ||
2086 | * | ||
2087 | * @returns {CKEDITOR.dom.element} The common ancestor of the selection or `null` if selection is empty. | ||
2088 | */ | ||
2089 | getCommonAncestor: function() { | ||
2090 | var ranges = this.getRanges(); | ||
2091 | if ( !ranges.length ) | ||
2092 | return null; | ||
2093 | |||
2094 | var startNode = ranges[ 0 ].startContainer, | ||
2095 | endNode = ranges[ ranges.length - 1 ].endContainer; | ||
2096 | return startNode.getCommonAncestor( endNode ); | ||
2097 | }, | ||
2098 | |||
2099 | /** | ||
2100 | * Moves the scrollbar to the starting position of the current selection. | ||
2101 | * | ||
2102 | * editor.getSelection().scrollIntoView(); | ||
2103 | */ | ||
2104 | scrollIntoView: function() { | ||
2105 | // Scrolls the first range into view. | ||
2106 | if ( this.type != CKEDITOR.SELECTION_NONE ) | ||
2107 | this.getRanges()[ 0 ].scrollIntoView(); | ||
2108 | }, | ||
2109 | |||
2110 | /** | ||
2111 | * Remove all the selection ranges from the document. | ||
2112 | */ | ||
2113 | removeAllRanges: function() { | ||
2114 | // Don't clear selection outside this selection's root (#11500). | ||
2115 | if ( this.getType() == CKEDITOR.SELECTION_NONE ) | ||
2116 | return; | ||
2117 | |||
2118 | var nativ = this.getNative(); | ||
2119 | |||
2120 | try { | ||
2121 | nativ && nativ[ isMSSelection ? 'empty' : 'removeAllRanges' ](); | ||
2122 | } catch ( er ) {} | ||
2123 | |||
2124 | this.reset(); | ||
2125 | } | ||
2126 | }; | ||
2127 | |||
2128 | } )(); | ||
2129 | |||
2130 | |||
2131 | /** | ||
2132 | * Fired when selection inside editor has been changed. Note that this event | ||
2133 | * is fired only when selection's start element (container of a selecion start) | ||
2134 | * changes, not on every possible selection change. Thanks to that `selectionChange` | ||
2135 | * is fired less frequently, but on every context | ||
2136 | * (the {@link CKEDITOR.editor#elementPath elements path} holding selection's start) change. | ||
2137 | * | ||
2138 | * @event selectionChange | ||
2139 | * @member CKEDITOR.editor | ||
2140 | * @param {CKEDITOR.editor} editor This editor instance. | ||
2141 | * @param data | ||
2142 | * @param {CKEDITOR.dom.selection} data.selection | ||
2143 | * @param {CKEDITOR.dom.elementPath} data.path | ||
2144 | */ | ||
2145 | |||
2146 | /** | ||
2147 | * Selection's revision. This value is incremented every time new | ||
2148 | * selection is created or existing one is modified. | ||
2149 | * | ||
2150 | * @since 4.3 | ||
2151 | * @readonly | ||
2152 | * @property {Number} rev | ||
2153 | */ | ||
2154 | |||
2155 | /** | ||
2156 | * Document in which selection is anchored. | ||
2157 | * | ||
2158 | * @readonly | ||
2159 | * @property {CKEDITOR.dom.document} document | ||
2160 | */ | ||
2161 | |||
2162 | /** | ||
2163 | * Selection's root element. | ||
2164 | * | ||
2165 | * @readonly | ||
2166 | * @property {CKEDITOR.dom.element} root | ||
2167 | */ | ||
2168 | |||
2169 | /** | ||
2170 | * Whether selection is locked (cannot be modified). | ||
2171 | * | ||
2172 | * See {@link #lock} and {@link #unlock} methods. | ||
2173 | * | ||
2174 | * @readonly | ||
2175 | * @property {Boolean} isLocked | ||
2176 | */ | ||
2177 | |||
2178 | /** | ||
2179 | * Whether selection is a fake selection. | ||
2180 | * | ||
2181 | * See {@link #fake} method. | ||
2182 | * | ||
2183 | * @readonly | ||
2184 | * @property {Boolean} isFake | ||
2185 | */ | ||
diff --git a/sources/core/skin.js b/sources/core/skin.js new file mode 100644 index 0000000..98b8536 --- /dev/null +++ b/sources/core/skin.js | |||
@@ -0,0 +1,350 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.skin} class that is used to manage skin parts. | ||
8 | */ | ||
9 | |||
10 | ( function() { | ||
11 | var cssLoaded = {}; | ||
12 | |||
13 | function getName() { | ||
14 | return CKEDITOR.skinName.split( ',' )[ 0 ]; | ||
15 | } | ||
16 | |||
17 | function getConfigPath() { | ||
18 | return CKEDITOR.getUrl( CKEDITOR.skinName.split( ',' )[ 1 ] || ( 'skins/' + getName() + '/' ) ); | ||
19 | } | ||
20 | |||
21 | /** | ||
22 | * Manages the loading of skin parts among all editor instances. | ||
23 | * | ||
24 | * @class | ||
25 | * @singleton | ||
26 | */ | ||
27 | CKEDITOR.skin = { | ||
28 | /** | ||
29 | * Returns the root path to the skin directory. | ||
30 | * | ||
31 | * @method | ||
32 | * @todo | ||
33 | */ | ||
34 | path: getConfigPath, | ||
35 | |||
36 | /** | ||
37 | * Loads a skin part into the page. Does nothing if the part has already been loaded. | ||
38 | * | ||
39 | * **Note:** The "editor" part is always auto loaded upon instance creation, | ||
40 | * thus this function is mainly used to **lazy load** other parts of the skin | ||
41 | * that do not have to be displayed until requested. | ||
42 | * | ||
43 | * // Load the dialog part. | ||
44 | * editor.skin.loadPart( 'dialog' ); | ||
45 | * | ||
46 | * @param {String} part The name of the skin part CSS file that resides in the skin directory. | ||
47 | * @param {Function} fn The provided callback function which is invoked after the part is loaded. | ||
48 | */ | ||
49 | loadPart: function( part, fn ) { | ||
50 | if ( CKEDITOR.skin.name != getName() ) { | ||
51 | CKEDITOR.scriptLoader.load( CKEDITOR.getUrl( getConfigPath() + 'skin.js' ), function() { | ||
52 | loadCss( part, fn ); | ||
53 | } ); | ||
54 | } else { | ||
55 | loadCss( part, fn ); | ||
56 | } | ||
57 | }, | ||
58 | |||
59 | /** | ||
60 | * Retrieves the real URL of a (CSS) skin part. | ||
61 | * | ||
62 | * @param {String} part | ||
63 | */ | ||
64 | getPath: function( part ) { | ||
65 | return CKEDITOR.getUrl( getCssPath( part ) ); | ||
66 | }, | ||
67 | |||
68 | /** | ||
69 | * The list of registered icons. To add new icons to this list, use {@link #addIcon}. | ||
70 | */ | ||
71 | icons: {}, | ||
72 | |||
73 | /** | ||
74 | * Registers an icon. | ||
75 | * | ||
76 | * @param {String} name The icon name. | ||
77 | * @param {String} path The path to the icon image file. | ||
78 | * @param {Number} [offset] The vertical offset position of the icon, if | ||
79 | * available inside a strip image. | ||
80 | * @param {String} [bgsize] The value of the CSS "background-size" property to | ||
81 | * use for this icon | ||
82 | */ | ||
83 | addIcon: function( name, path, offset, bgsize ) { | ||
84 | name = name.toLowerCase(); | ||
85 | if ( !this.icons[ name ] ) { | ||
86 | this.icons[ name ] = { | ||
87 | path: path, | ||
88 | offset: offset || 0, | ||
89 | bgsize: bgsize || '16px' | ||
90 | }; | ||
91 | } | ||
92 | }, | ||
93 | |||
94 | /** | ||
95 | * Gets the CSS background styles to be used to render a specific icon. | ||
96 | * | ||
97 | * @param {String} name The icon name, as registered with {@link #addIcon}. | ||
98 | * @param {Boolean} [rtl] Indicates that the RTL version of the icon is | ||
99 | * to be used, if available. | ||
100 | * @param {String} [overridePath] The path to the icon image file. It | ||
101 | * overrides the path defined by the named icon, if available, and is | ||
102 | * used if the named icon was not registered. | ||
103 | * @param {Number} [overrideOffset] The vertical offset position of the | ||
104 | * icon. It overrides the offset defined by the named icon, if | ||
105 | * available, and is used if the named icon was not registered. | ||
106 | * @param {String} [overrideBgsize] The value of the CSS "background-size" property | ||
107 | * to use for the icon. It overrides the value defined by the named icon, | ||
108 | * if available, and is used if the named icon was not registered. | ||
109 | */ | ||
110 | getIconStyle: function( name, rtl, overridePath, overrideOffset, overrideBgsize ) { | ||
111 | var icon, path, offset, bgsize; | ||
112 | |||
113 | if ( name ) { | ||
114 | name = name.toLowerCase(); | ||
115 | // If we're in RTL, try to get the RTL version of the icon. | ||
116 | if ( rtl ) | ||
117 | icon = this.icons[ name + '-rtl' ]; | ||
118 | |||
119 | // If not in LTR or no RTL version available, get the generic one. | ||
120 | if ( !icon ) | ||
121 | icon = this.icons[ name ]; | ||
122 | } | ||
123 | |||
124 | path = overridePath || ( icon && icon.path ) || ''; | ||
125 | offset = overrideOffset || ( icon && icon.offset ); | ||
126 | bgsize = overrideBgsize || ( icon && icon.bgsize ) || '16px'; | ||
127 | |||
128 | // If we use apostrophes in background-image, we must escape apostrophes in path (just to be sure). (#13361) | ||
129 | if ( path ) | ||
130 | path = path.replace( /'/g, '\\\'' ); | ||
131 | |||
132 | return path && | ||
133 | ( 'background-image:url(\'' + CKEDITOR.getUrl( path ) + '\');background-position:0 ' + offset + 'px;background-size:' + bgsize + ';' ); | ||
134 | } | ||
135 | }; | ||
136 | |||
137 | function getCssPath( part ) { | ||
138 | // Check for ua-specific version of skin part. | ||
139 | var uas = CKEDITOR.skin[ 'ua_' + part ], env = CKEDITOR.env; | ||
140 | if ( uas ) { | ||
141 | |||
142 | // Having versioned UA checked first. | ||
143 | uas = uas.split( ',' ).sort( function( a, b ) { | ||
144 | return a > b ? -1 : 1; | ||
145 | } ); | ||
146 | |||
147 | // Loop through all ua entries, checking is any of them match the current ua. | ||
148 | for ( var i = 0, ua; i < uas.length; i++ ) { | ||
149 | ua = uas[ i ]; | ||
150 | |||
151 | if ( env.ie ) { | ||
152 | if ( ( ua.replace( /^ie/, '' ) == env.version ) || ( env.quirks && ua == 'iequirks' ) ) | ||
153 | ua = 'ie'; | ||
154 | } | ||
155 | |||
156 | if ( env[ ua ] ) { | ||
157 | part += '_' + uas[ i ]; | ||
158 | break; | ||
159 | } | ||
160 | } | ||
161 | } | ||
162 | return CKEDITOR.getUrl( getConfigPath() + part + '.css' ); | ||
163 | } | ||
164 | |||
165 | function loadCss( part, callback ) { | ||
166 | // Avoid reload. | ||
167 | if ( !cssLoaded[ part ] ) { | ||
168 | CKEDITOR.document.appendStyleSheet( getCssPath( part ) ); | ||
169 | cssLoaded[ part ] = 1; | ||
170 | } | ||
171 | |||
172 | // CSS loading should not be blocking. | ||
173 | callback && callback(); | ||
174 | } | ||
175 | |||
176 | CKEDITOR.tools.extend( CKEDITOR.editor.prototype, { | ||
177 | /** Gets the color of the editor user interface. | ||
178 | * | ||
179 | * CKEDITOR.instances.editor1.getUiColor(); | ||
180 | * | ||
181 | * @method | ||
182 | * @member CKEDITOR.editor | ||
183 | * @returns {String} uiColor The editor UI color or `undefined` if the UI color is not set. | ||
184 | */ | ||
185 | getUiColor: function() { | ||
186 | return this.uiColor; | ||
187 | }, | ||
188 | |||
189 | /** Sets the color of the editor user interface. This method accepts a color value in | ||
190 | * hexadecimal notation, with a `#` character (e.g. #ffffff). | ||
191 | * | ||
192 | * CKEDITOR.instances.editor1.setUiColor( '#ff00ff' ); | ||
193 | * | ||
194 | * @method | ||
195 | * @member CKEDITOR.editor | ||
196 | * @param {String} color The desired editor UI color in hexadecimal notation. | ||
197 | */ | ||
198 | setUiColor: function( color ) { | ||
199 | var uiStyle = getStylesheet( CKEDITOR.document ); | ||
200 | |||
201 | return ( this.setUiColor = function( color ) { | ||
202 | this.uiColor = color; | ||
203 | |||
204 | var chameleon = CKEDITOR.skin.chameleon, | ||
205 | editorStyleContent = '', | ||
206 | panelStyleContent = ''; | ||
207 | |||
208 | if ( typeof chameleon == 'function' ) { | ||
209 | editorStyleContent = chameleon( this, 'editor' ); | ||
210 | panelStyleContent = chameleon( this, 'panel' ); | ||
211 | } | ||
212 | |||
213 | var replace = [ [ uiColorRegexp, color ] ]; | ||
214 | |||
215 | // Update general style. | ||
216 | updateStylesheets( [ uiStyle ], editorStyleContent, replace ); | ||
217 | |||
218 | // Update panel styles. | ||
219 | updateStylesheets( uiColorMenus, panelStyleContent, replace ); | ||
220 | } ).call( this, color ); | ||
221 | } | ||
222 | } ); | ||
223 | |||
224 | var uiColorStylesheetId = 'cke_ui_color', | ||
225 | uiColorMenus = [], | ||
226 | uiColorRegexp = /\$color/g; | ||
227 | |||
228 | function getStylesheet( document ) { | ||
229 | var node = document.getById( uiColorStylesheetId ); | ||
230 | if ( !node ) { | ||
231 | node = document.getHead().append( 'style' ); | ||
232 | node.setAttribute( 'id', uiColorStylesheetId ); | ||
233 | node.setAttribute( 'type', 'text/css' ); | ||
234 | } | ||
235 | return node; | ||
236 | } | ||
237 | |||
238 | function updateStylesheets( styleNodes, styleContent, replace ) { | ||
239 | var r, i, content; | ||
240 | |||
241 | // We have to split CSS declarations for webkit. | ||
242 | if ( CKEDITOR.env.webkit ) { | ||
243 | styleContent = styleContent.split( '}' ).slice( 0, -1 ); | ||
244 | for ( i = 0; i < styleContent.length; i++ ) | ||
245 | styleContent[ i ] = styleContent[ i ].split( '{' ); | ||
246 | } | ||
247 | |||
248 | for ( var id = 0; id < styleNodes.length; id++ ) { | ||
249 | if ( CKEDITOR.env.webkit ) { | ||
250 | for ( i = 0; i < styleContent.length; i++ ) { | ||
251 | content = styleContent[ i ][ 1 ]; | ||
252 | for ( r = 0; r < replace.length; r++ ) | ||
253 | content = content.replace( replace[ r ][ 0 ], replace[ r ][ 1 ] ); | ||
254 | |||
255 | styleNodes[ id ].$.sheet.addRule( styleContent[ i ][ 0 ], content ); | ||
256 | } | ||
257 | } else { | ||
258 | content = styleContent; | ||
259 | for ( r = 0; r < replace.length; r++ ) | ||
260 | content = content.replace( replace[ r ][ 0 ], replace[ r ][ 1 ] ); | ||
261 | |||
262 | if ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 ) | ||
263 | styleNodes[ id ].$.styleSheet.cssText += content; | ||
264 | else | ||
265 | styleNodes[ id ].$.innerHTML += content; | ||
266 | } | ||
267 | } | ||
268 | } | ||
269 | |||
270 | CKEDITOR.on( 'instanceLoaded', function( evt ) { | ||
271 | // The chameleon feature is not for IE quirks. | ||
272 | if ( CKEDITOR.env.ie && CKEDITOR.env.quirks ) | ||
273 | return; | ||
274 | |||
275 | var editor = evt.editor, | ||
276 | showCallback = function( event ) { | ||
277 | var panel = event.data[ 0 ] || event.data; | ||
278 | var iframe = panel.element.getElementsByTag( 'iframe' ).getItem( 0 ).getFrameDocument(); | ||
279 | |||
280 | // Add stylesheet if missing. | ||
281 | if ( !iframe.getById( 'cke_ui_color' ) ) { | ||
282 | var node = getStylesheet( iframe ); | ||
283 | uiColorMenus.push( node ); | ||
284 | |||
285 | var color = editor.getUiColor(); | ||
286 | // Set uiColor for new panel. | ||
287 | if ( color ) | ||
288 | updateStylesheets( [ node ], CKEDITOR.skin.chameleon( editor, 'panel' ), [ [ uiColorRegexp, color ] ] ); | ||
289 | |||
290 | } | ||
291 | }; | ||
292 | |||
293 | editor.on( 'panelShow', showCallback ); | ||
294 | editor.on( 'menuShow', showCallback ); | ||
295 | |||
296 | // Apply UI color if specified in config. | ||
297 | if ( editor.config.uiColor ) | ||
298 | editor.setUiColor( editor.config.uiColor ); | ||
299 | } ); | ||
300 | } )(); | ||
301 | |||
302 | /** | ||
303 | * The list of file names matching the browser user agent string from | ||
304 | * {@link CKEDITOR.env}. This is used to load the skin part file in addition | ||
305 | * to the "main" skin file for a particular browser. | ||
306 | * | ||
307 | * **Note:** For each of the defined skin parts the corresponding | ||
308 | * CSS file with the same name as the user agent must exist inside | ||
309 | * the skin directory. | ||
310 | * | ||
311 | * @property ua | ||
312 | * @todo type? | ||
313 | */ | ||
314 | |||
315 | /** | ||
316 | * The name of the skin that is currently used. | ||
317 | * | ||
318 | * @property {String} name | ||
319 | * @todo | ||
320 | */ | ||
321 | |||
322 | /** | ||
323 | * The editor skin name. Note that it is not possible to have editors with | ||
324 | * different skin settings in the same page. In such case just one of the | ||
325 | * skins will be used for all editors. | ||
326 | * | ||
327 | * This is a shortcut to {@link CKEDITOR#skinName}. | ||
328 | * | ||
329 | * It is possible to install skins outside the default `skin` folder in the | ||
330 | * editor installation. In that case, the absolute URL path to that folder | ||
331 | * should be provided, separated by a comma (`'skin_name,skin_path'`). | ||
332 | * | ||
333 | * config.skin = 'moono'; | ||
334 | * | ||
335 | * config.skin = 'myskin,/customstuff/myskin/'; | ||
336 | * | ||
337 | * @cfg {String} skin | ||
338 | * @member CKEDITOR.config | ||
339 | */ | ||
340 | |||
341 | /** | ||
342 | * A function that supports the chameleon (skin color switch) feature, providing | ||
343 | * the skin color style updates to be applied in runtime. | ||
344 | * | ||
345 | * **Note:** The embedded `$color` variable is to be substituted with a specific UI color. | ||
346 | * | ||
347 | * @method chameleon | ||
348 | * @param {String} editor The editor instance that the color changes apply to. | ||
349 | * @param {String} part The name of the skin part where the color changes take place. | ||
350 | */ | ||
diff --git a/sources/core/style.js b/sources/core/style.js new file mode 100644 index 0000000..09b117b --- /dev/null +++ b/sources/core/style.js | |||
@@ -0,0 +1,2089 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | 'use strict'; | ||
7 | |||
8 | /** | ||
9 | * Block style type. | ||
10 | * | ||
11 | * Read more in the {@link CKEDITOR.style} class documentation. | ||
12 | * | ||
13 | * @readonly | ||
14 | * @property {Number} [=1] | ||
15 | * @member CKEDITOR | ||
16 | */ | ||
17 | CKEDITOR.STYLE_BLOCK = 1; | ||
18 | |||
19 | /** | ||
20 | * Inline style type. | ||
21 | * | ||
22 | * Read more in the {@link CKEDITOR.style} class documentation. | ||
23 | * | ||
24 | * @readonly | ||
25 | * @property {Number} [=2] | ||
26 | * @member CKEDITOR | ||
27 | */ | ||
28 | CKEDITOR.STYLE_INLINE = 2; | ||
29 | |||
30 | /** | ||
31 | * Object style type. | ||
32 | * | ||
33 | * Read more in the {@link CKEDITOR.style} class documentation. | ||
34 | * | ||
35 | * @readonly | ||
36 | * @property {Number} [=3] | ||
37 | * @member CKEDITOR | ||
38 | */ | ||
39 | CKEDITOR.STYLE_OBJECT = 3; | ||
40 | |||
41 | ( function() { | ||
42 | var blockElements = { | ||
43 | address: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, p: 1, | ||
44 | pre: 1, section: 1, header: 1, footer: 1, nav: 1, article: 1, aside: 1, figure: 1, | ||
45 | dialog: 1, hgroup: 1, time: 1, meter: 1, menu: 1, command: 1, keygen: 1, output: 1, | ||
46 | progress: 1, details: 1, datagrid: 1, datalist: 1 | ||
47 | }, | ||
48 | |||
49 | objectElements = { | ||
50 | a: 1, blockquote: 1, embed: 1, hr: 1, img: 1, li: 1, object: 1, ol: 1, table: 1, td: 1, | ||
51 | tr: 1, th: 1, ul: 1, dl: 1, dt: 1, dd: 1, form: 1, audio: 1, video: 1 | ||
52 | }; | ||
53 | |||
54 | var semicolonFixRegex = /\s*(?:;\s*|$)/, | ||
55 | varRegex = /#\((.+?)\)/g; | ||
56 | |||
57 | var notBookmark = CKEDITOR.dom.walker.bookmark( 0, 1 ), | ||
58 | nonWhitespaces = CKEDITOR.dom.walker.whitespaces( 1 ); | ||
59 | |||
60 | /** | ||
61 | * A class representing a style instance for the specific style definition. | ||
62 | * In this approach, a style is a set of properties, like attributes and styles, | ||
63 | * which can be applied to and removed from a {@link CKEDITOR.dom.selection selection} through | ||
64 | * {@link CKEDITOR.editor editor} methods: {@link CKEDITOR.editor#applyStyle} and {@link CKEDITOR.editor#removeStyle}, | ||
65 | * respectively. | ||
66 | * | ||
67 | * Three default style types are available: {@link CKEDITOR#STYLE_BLOCK STYLE_BLOCK}, {@link CKEDITOR#STYLE_INLINE STYLE_INLINE}, | ||
68 | * and {@link CKEDITOR#STYLE_OBJECT STYLE_OBJECT}. Based on its type, a style heavily changes its behavior. | ||
69 | * You can read more about style types in the [Style Types section of the Styles guide](#!/guide/dev_styles-section-style-types). | ||
70 | * | ||
71 | * It is possible to define a custom style type by subclassing this class by using the {@link #addCustomHandler} method. | ||
72 | * However, because of great complexity of the styles handling job, it is only possible in very specific cases. | ||
73 | * | ||
74 | * ### Usage | ||
75 | * | ||
76 | * Basic usage: | ||
77 | * | ||
78 | * // Define a block style. | ||
79 | * var style = new CKEDITOR.style( { element: 'h1' } ); | ||
80 | * | ||
81 | * // Considering the following selection: | ||
82 | * // <p>Foo</p><p>Bar^</p> | ||
83 | * // Executing: | ||
84 | * editor.applyStyle( style ); | ||
85 | * // Will give: | ||
86 | * // <p>Foo</p><h1>Bar^</h1> | ||
87 | * style.checkActive( editor.elementPath(), editor ); // -> true | ||
88 | * | ||
89 | * editor.removeStyle( style ); | ||
90 | * // Will give: | ||
91 | * // <p>Foo</p><p>Bar^</p> | ||
92 | * | ||
93 | * style.checkActive( editor.elementPath(), editor ); // -> false | ||
94 | * | ||
95 | * Object style: | ||
96 | * | ||
97 | * // Define an object style. | ||
98 | * var style = new CKEDITOR.style( { element: 'img', attributes: { 'class': 'foo' } } ); | ||
99 | * | ||
100 | * // Considering the following selection: | ||
101 | * // <p><img src="bar.png" alt="" />Foo^</p> | ||
102 | * // Executing: | ||
103 | * editor.applyStyle( style ); | ||
104 | * // Will not apply the style, because the image is not selected. | ||
105 | * // You can check if a style can be applied on the current selection with: | ||
106 | * style.checkApplicable( editor.elementPath(), editor ); // -> false | ||
107 | * | ||
108 | * // Considering the following selection: | ||
109 | * // <p>[<img src="bar.png" alt="" />]Foo</p> | ||
110 | * // Executing | ||
111 | * editor.applyStyle( style ); | ||
112 | * // Will give: | ||
113 | * // <p>[<img src="bar.png" alt="" class="foo" />]Foo</p> | ||
114 | * | ||
115 | * ### API changes introduced in CKEditor 4.4 | ||
116 | * | ||
117 | * Before CKEditor 4.4 all style instances had no access at all to the {@link CKEDITOR.editor editor instance} | ||
118 | * within which the style is used. Neither the style constructor, nor style methods were requiring | ||
119 | * passing the editor instance which made styles independent of the editor and hence its settings and state. | ||
120 | * This design decision came from CKEditor 3; it started causing problems and became an unsolvable obstacle for | ||
121 | * the {@link CKEDITOR.style.customHandlers.widget widget style handler} which we introduced in CKEditor 4.4. | ||
122 | * | ||
123 | * There were two possible solutions. Passing an editor instance to the style constructor or to every method. | ||
124 | * The first approach would be clean, however, having in mind the backward compatibility, we did not decide | ||
125 | * to go for it. It would bind the style to one editor instance, making it unusable with other editor instances. | ||
126 | * That could break many implementations reusing styles between editors. Therefore, we decided to take the longer | ||
127 | * but safer path — the editor instance became an argument for nearly all style methods, however, | ||
128 | * for backward compatibility reasons, all these methods will work without it. Even the newly | ||
129 | * implemented {@link CKEDITOR.style.customHandlers.widget widget style handler}'s methods will not fail, | ||
130 | * although they will also not work by aborting at an early stage. | ||
131 | * | ||
132 | * Therefore, you can safely upgrade to CKEditor 4.4 even if you use style methods without providing | ||
133 | * the editor instance. You must only align your code if your implementation should handle widget styles | ||
134 | * or any other custom style handler. Of course, we recommend doing this in any case to avoid potential | ||
135 | * problems in the future. | ||
136 | * | ||
137 | * @class | ||
138 | * @constructor Creates a style class instance. | ||
139 | * @param styleDefinition | ||
140 | * @param variablesValues | ||
141 | */ | ||
142 | CKEDITOR.style = function( styleDefinition, variablesValues ) { | ||
143 | if ( typeof styleDefinition.type == 'string' ) | ||
144 | return new CKEDITOR.style.customHandlers[ styleDefinition.type ]( styleDefinition ); | ||
145 | |||
146 | // Inline style text as attribute should be converted | ||
147 | // to styles object. | ||
148 | var attrs = styleDefinition.attributes; | ||
149 | if ( attrs && attrs.style ) { | ||
150 | styleDefinition.styles = CKEDITOR.tools.extend( {}, | ||
151 | styleDefinition.styles, CKEDITOR.tools.parseCssText( attrs.style ) ); | ||
152 | delete attrs.style; | ||
153 | } | ||
154 | |||
155 | if ( variablesValues ) { | ||
156 | styleDefinition = CKEDITOR.tools.clone( styleDefinition ); | ||
157 | |||
158 | replaceVariables( styleDefinition.attributes, variablesValues ); | ||
159 | replaceVariables( styleDefinition.styles, variablesValues ); | ||
160 | } | ||
161 | |||
162 | var element = this.element = styleDefinition.element ? | ||
163 | ( | ||
164 | typeof styleDefinition.element == 'string' ? | ||
165 | styleDefinition.element.toLowerCase() : styleDefinition.element | ||
166 | ) : '*'; | ||
167 | |||
168 | this.type = styleDefinition.type || | ||
169 | ( | ||
170 | blockElements[ element ] ? CKEDITOR.STYLE_BLOCK : | ||
171 | objectElements[ element ] ? CKEDITOR.STYLE_OBJECT : | ||
172 | CKEDITOR.STYLE_INLINE | ||
173 | ); | ||
174 | |||
175 | // If the 'element' property is an object with a set of possible element, it will be applied like an object style: only to existing elements | ||
176 | if ( typeof this.element == 'object' ) | ||
177 | this.type = CKEDITOR.STYLE_OBJECT; | ||
178 | |||
179 | this._ = { | ||
180 | definition: styleDefinition | ||
181 | }; | ||
182 | }; | ||
183 | |||
184 | CKEDITOR.style.prototype = { | ||
185 | /** | ||
186 | * Applies the style on the editor's current selection. | ||
187 | * | ||
188 | * Before the style is applied, the method checks if the {@link #checkApplicable style is applicable}. | ||
189 | * | ||
190 | * **Note:** The recommended way of applying the style is by using the | ||
191 | * {@link CKEDITOR.editor#applyStyle} method, which is a shorthand for this method. | ||
192 | * | ||
193 | * @param {CKEDITOR.editor/CKEDITOR.dom.document} editor The editor instance in which | ||
194 | * the style will be applied. | ||
195 | * A {@link CKEDITOR.dom.document} instance is accepted for backward compatibility | ||
196 | * reasons, although since CKEditor 4.4 this type of argument is deprecated. Read more about | ||
197 | * the signature change in the {@link CKEDITOR.style} documentation. | ||
198 | */ | ||
199 | apply: function( editor ) { | ||
200 | // Backward compatibility. | ||
201 | if ( editor instanceof CKEDITOR.dom.document ) | ||
202 | return applyStyleOnSelection.call( this, editor.getSelection() ); | ||
203 | |||
204 | if ( this.checkApplicable( editor.elementPath(), editor ) ) { | ||
205 | var initialEnterMode = this._.enterMode; | ||
206 | |||
207 | // See comment in removeStyle. | ||
208 | if ( !initialEnterMode ) | ||
209 | this._.enterMode = editor.activeEnterMode; | ||
210 | applyStyleOnSelection.call( this, editor.getSelection(), 0, editor ); | ||
211 | this._.enterMode = initialEnterMode; | ||
212 | } | ||
213 | }, | ||
214 | |||
215 | /** | ||
216 | * Removes the style from the editor's current selection. | ||
217 | * | ||
218 | * Before the style is applied, the method checks if {@link #checkApplicable style could be applied}. | ||
219 | * | ||
220 | * **Note:** The recommended way of removing the style is by using the | ||
221 | * {@link CKEDITOR.editor#removeStyle} method, which is a shorthand for this method. | ||
222 | * | ||
223 | * @param {CKEDITOR.editor/CKEDITOR.dom.document} editor The editor instance in which | ||
224 | * the style will be removed. | ||
225 | * A {@link CKEDITOR.dom.document} instance is accepted for backward compatibility | ||
226 | * reasons, although since CKEditor 4.4 this type of argument is deprecated. Read more about | ||
227 | * the signature change in the {@link CKEDITOR.style} documentation. | ||
228 | */ | ||
229 | remove: function( editor ) { | ||
230 | // Backward compatibility. | ||
231 | if ( editor instanceof CKEDITOR.dom.document ) | ||
232 | return applyStyleOnSelection.call( this, editor.getSelection(), 1 ); | ||
233 | |||
234 | if ( this.checkApplicable( editor.elementPath(), editor ) ) { | ||
235 | var initialEnterMode = this._.enterMode; | ||
236 | |||
237 | // Before CKEditor 4.4 style knew nothing about editor, so in order to provide enterMode | ||
238 | // which should be used developers were forced to hack the style object (see #10190). | ||
239 | // Since CKEditor 4.4 style knows about editor (at least when it's being applied/removed), but we | ||
240 | // use _.enterMode for backward compatibility with those hacks. | ||
241 | // Note: we should not change style's enter mode if it was already set. | ||
242 | if ( !initialEnterMode ) | ||
243 | this._.enterMode = editor.activeEnterMode; | ||
244 | applyStyleOnSelection.call( this, editor.getSelection(), 1, editor ); | ||
245 | this._.enterMode = initialEnterMode; | ||
246 | } | ||
247 | }, | ||
248 | |||
249 | /** | ||
250 | * Applies the style on the provided range. Unlike {@link #apply} this | ||
251 | * method does not take care of setting the selection, however, the range | ||
252 | * is updated to the correct place. | ||
253 | * | ||
254 | * **Note:** If you want to apply the style on the editor selection, | ||
255 | * you probably want to use {@link CKEDITOR.editor#applyStyle}. | ||
256 | * | ||
257 | * @param {CKEDITOR.dom.range} range | ||
258 | * @param {CKEDITOR.editor} editor The editor instance. Required argument since | ||
259 | * CKEditor 4.4. The style system will work without it, but it is highly | ||
260 | * recommended to provide it for integration with all features. Read more about | ||
261 | * the signature change in the {@link CKEDITOR.style} documentation. | ||
262 | */ | ||
263 | applyToRange: function( range ) { | ||
264 | this.applyToRange = | ||
265 | this.type == CKEDITOR.STYLE_INLINE ? applyInlineStyle : | ||
266 | this.type == CKEDITOR.STYLE_BLOCK ? applyBlockStyle : | ||
267 | this.type == CKEDITOR.STYLE_OBJECT ? applyObjectStyle : | ||
268 | null; | ||
269 | |||
270 | return this.applyToRange( range ); | ||
271 | }, | ||
272 | |||
273 | /** | ||
274 | * Removes the style from the provided range. Unlike {@link #remove} this | ||
275 | * method does not take care of setting the selection, however, the range | ||
276 | * is updated to the correct place. | ||
277 | * | ||
278 | * **Note:** If you want to remove the style from the editor selection, | ||
279 | * you probably want to use {@link CKEDITOR.editor#removeStyle}. | ||
280 | * | ||
281 | * @param {CKEDITOR.dom.range} range | ||
282 | * @param {CKEDITOR.editor} editor The editor instance. Required argument since | ||
283 | * CKEditor 4.4. The style system will work without it, but it is highly | ||
284 | * recommended to provide it for integration with all features. Read more about | ||
285 | * the signature change in the {@link CKEDITOR.style} documentation. | ||
286 | */ | ||
287 | removeFromRange: function( range ) { | ||
288 | this.removeFromRange = | ||
289 | this.type == CKEDITOR.STYLE_INLINE ? removeInlineStyle : | ||
290 | this.type == CKEDITOR.STYLE_BLOCK ? removeBlockStyle : | ||
291 | this.type == CKEDITOR.STYLE_OBJECT ? removeObjectStyle : | ||
292 | null; | ||
293 | |||
294 | return this.removeFromRange( range ); | ||
295 | }, | ||
296 | |||
297 | /** | ||
298 | * Applies the style to the element. This method bypasses all checks | ||
299 | * and applies the style attributes directly on the provided element. Use with caution. | ||
300 | * | ||
301 | * See {@link CKEDITOR.editor#applyStyle}. | ||
302 | * | ||
303 | * @param {CKEDITOR.dom.element} element | ||
304 | * @param {CKEDITOR.editor} editor The editor instance. Required argument since | ||
305 | * CKEditor 4.4. The style system will work without it, but it is highly | ||
306 | * recommended to provide it for integration with all features. Read more about | ||
307 | * the signature change in the {@link CKEDITOR.style} documentation. | ||
308 | */ | ||
309 | applyToObject: function( element ) { | ||
310 | setupElement( element, this ); | ||
311 | }, | ||
312 | |||
313 | /** | ||
314 | * Gets the style state inside the elements path. | ||
315 | * | ||
316 | * @param {CKEDITOR.dom.elementPath} elementPath | ||
317 | * @param {CKEDITOR.editor} editor The editor instance. Required argument since | ||
318 | * CKEditor 4.4. The style system will work without it, but it is highly | ||
319 | * recommended to provide it for integration with all features. Read more about | ||
320 | * the signature change in the {@link CKEDITOR.style} documentation. | ||
321 | * @returns {Boolean} `true` if the element is active in the elements path. | ||
322 | */ | ||
323 | checkActive: function( elementPath, editor ) { | ||
324 | switch ( this.type ) { | ||
325 | case CKEDITOR.STYLE_BLOCK: | ||
326 | return this.checkElementRemovable( elementPath.block || elementPath.blockLimit, true, editor ); | ||
327 | |||
328 | case CKEDITOR.STYLE_OBJECT: | ||
329 | case CKEDITOR.STYLE_INLINE: | ||
330 | |||
331 | var elements = elementPath.elements; | ||
332 | |||
333 | for ( var i = 0, element; i < elements.length; i++ ) { | ||
334 | element = elements[ i ]; | ||
335 | |||
336 | if ( this.type == CKEDITOR.STYLE_INLINE && ( element == elementPath.block || element == elementPath.blockLimit ) ) | ||
337 | continue; | ||
338 | |||
339 | if ( this.type == CKEDITOR.STYLE_OBJECT ) { | ||
340 | var name = element.getName(); | ||
341 | if ( !( typeof this.element == 'string' ? name == this.element : name in this.element ) ) | ||
342 | continue; | ||
343 | } | ||
344 | |||
345 | if ( this.checkElementRemovable( element, true, editor ) ) | ||
346 | return true; | ||
347 | } | ||
348 | } | ||
349 | return false; | ||
350 | }, | ||
351 | |||
352 | /** | ||
353 | * Whether this style can be applied at the specified elements path. | ||
354 | * | ||
355 | * @param {CKEDITOR.dom.elementPath} elementPath The elements path to | ||
356 | * check the style against. | ||
357 | * @param {CKEDITOR.editor} editor The editor instance. Required argument since | ||
358 | * CKEditor 4.4. The style system will work without it, but it is highly | ||
359 | * recommended to provide it for integration with all features. Read more about | ||
360 | * the signature change in the {@link CKEDITOR.style} documentation. | ||
361 | * @param {CKEDITOR.filter} [filter] If defined, the style will be | ||
362 | * checked against this filter as well. | ||
363 | * @returns {Boolean} `true` if this style can be applied at the elements path. | ||
364 | */ | ||
365 | checkApplicable: function( elementPath, editor, filter ) { | ||
366 | // Backward compatibility. | ||
367 | if ( editor && editor instanceof CKEDITOR.filter ) | ||
368 | filter = editor; | ||
369 | |||
370 | if ( filter && !filter.check( this ) ) | ||
371 | return false; | ||
372 | |||
373 | switch ( this.type ) { | ||
374 | case CKEDITOR.STYLE_OBJECT: | ||
375 | return !!elementPath.contains( this.element ); | ||
376 | case CKEDITOR.STYLE_BLOCK: | ||
377 | return !!elementPath.blockLimit.getDtd()[ this.element ]; | ||
378 | } | ||
379 | |||
380 | return true; | ||
381 | }, | ||
382 | |||
383 | /** | ||
384 | * Checks if the element matches the current style definition. | ||
385 | * | ||
386 | * @param {CKEDITOR.dom.element} element | ||
387 | * @param {Boolean} fullMatch | ||
388 | * @param {CKEDITOR.editor} editor The editor instance. Required argument since | ||
389 | * CKEditor 4.4. The style system will work without it, but it is highly | ||
390 | * recommended to provide it for integration with all features. Read more about | ||
391 | * the signature change in the {@link CKEDITOR.style} documentation. | ||
392 | * @returns {Boolean} | ||
393 | */ | ||
394 | checkElementMatch: function( element, fullMatch ) { | ||
395 | var def = this._.definition; | ||
396 | |||
397 | if ( !element || !def.ignoreReadonly && element.isReadOnly() ) | ||
398 | return false; | ||
399 | |||
400 | var attribs, | ||
401 | name = element.getName(); | ||
402 | |||
403 | // If the element name is the same as the style name. | ||
404 | if ( typeof this.element == 'string' ? name == this.element : name in this.element ) { | ||
405 | // If no attributes are defined in the element. | ||
406 | if ( !fullMatch && !element.hasAttributes() ) | ||
407 | return true; | ||
408 | |||
409 | attribs = getAttributesForComparison( def ); | ||
410 | |||
411 | if ( attribs._length ) { | ||
412 | for ( var attName in attribs ) { | ||
413 | if ( attName == '_length' ) | ||
414 | continue; | ||
415 | |||
416 | var elementAttr = element.getAttribute( attName ) || ''; | ||
417 | |||
418 | // Special treatment for 'style' attribute is required. | ||
419 | if ( attName == 'style' ? compareCssText( attribs[ attName ], elementAttr ) : attribs[ attName ] == elementAttr ) { | ||
420 | if ( !fullMatch ) | ||
421 | return true; | ||
422 | } else if ( fullMatch ) { | ||
423 | return false; | ||
424 | } | ||
425 | } | ||
426 | if ( fullMatch ) | ||
427 | return true; | ||
428 | } else { | ||
429 | return true; | ||
430 | } | ||
431 | } | ||
432 | |||
433 | return false; | ||
434 | }, | ||
435 | |||
436 | /** | ||
437 | * Checks if an element, or any of its attributes, is removable by the | ||
438 | * current style definition. | ||
439 | * | ||
440 | * @param {CKEDITOR.dom.element} element | ||
441 | * @param {Boolean} fullMatch | ||
442 | * @param {CKEDITOR.editor} editor The editor instance. Required argument since | ||
443 | * CKEditor 4.4. The style system will work without it, but it is highly | ||
444 | * recommended to provide it for integration with all features. Read more about | ||
445 | * the signature change in the {@link CKEDITOR.style} documentation. | ||
446 | * @returns {Boolean} | ||
447 | */ | ||
448 | checkElementRemovable: function( element, fullMatch, editor ) { | ||
449 | // Check element matches the style itself. | ||
450 | if ( this.checkElementMatch( element, fullMatch, editor ) ) | ||
451 | return true; | ||
452 | |||
453 | // Check if the element matches the style overrides. | ||
454 | var override = getOverrides( this )[ element.getName() ]; | ||
455 | if ( override ) { | ||
456 | var attribs, attName; | ||
457 | |||
458 | // If no attributes have been defined, remove the element. | ||
459 | if ( !( attribs = override.attributes ) ) | ||
460 | return true; | ||
461 | |||
462 | for ( var i = 0; i < attribs.length; i++ ) { | ||
463 | attName = attribs[ i ][ 0 ]; | ||
464 | var actualAttrValue = element.getAttribute( attName ); | ||
465 | if ( actualAttrValue ) { | ||
466 | var attValue = attribs[ i ][ 1 ]; | ||
467 | |||
468 | // Remove the attribute if: | ||
469 | // - The override definition value is null; | ||
470 | // - The override definition value is a string that | ||
471 | // matches the attribute value exactly. | ||
472 | // - The override definition value is a regex that | ||
473 | // has matches in the attribute value. | ||
474 | if ( attValue === null ) | ||
475 | return true; | ||
476 | if ( typeof attValue == 'string' ) { | ||
477 | if ( actualAttrValue == attValue ) | ||
478 | return true; | ||
479 | } else if ( attValue.test( actualAttrValue ) ) { | ||
480 | return true; | ||
481 | } | ||
482 | } | ||
483 | } | ||
484 | } | ||
485 | return false; | ||
486 | }, | ||
487 | |||
488 | /** | ||
489 | * Builds the preview HTML based on the styles definition. | ||
490 | * | ||
491 | * @param {String} [label] The label used in the style preview. | ||
492 | * @return {String} The HTML of preview. | ||
493 | */ | ||
494 | buildPreview: function( label ) { | ||
495 | var styleDefinition = this._.definition, | ||
496 | html = [], | ||
497 | elementName = styleDefinition.element; | ||
498 | |||
499 | // Avoid <bdo> in the preview. | ||
500 | if ( elementName == 'bdo' ) | ||
501 | elementName = 'span'; | ||
502 | |||
503 | html = [ '<', elementName ]; | ||
504 | |||
505 | // Assign all defined attributes. | ||
506 | var attribs = styleDefinition.attributes; | ||
507 | if ( attribs ) { | ||
508 | for ( var att in attribs ) | ||
509 | html.push( ' ', att, '="', attribs[ att ], '"' ); | ||
510 | } | ||
511 | |||
512 | // Assign the style attribute. | ||
513 | var cssStyle = CKEDITOR.style.getStyleText( styleDefinition ); | ||
514 | if ( cssStyle ) | ||
515 | html.push( ' style="', cssStyle, '"' ); | ||
516 | |||
517 | html.push( '>', ( label || styleDefinition.name ), '</', elementName, '>' ); | ||
518 | |||
519 | return html.join( '' ); | ||
520 | }, | ||
521 | |||
522 | /** | ||
523 | * Returns the style definition. | ||
524 | * | ||
525 | * @since 4.1 | ||
526 | * @returns {Object} | ||
527 | */ | ||
528 | getDefinition: function() { | ||
529 | return this._.definition; | ||
530 | } | ||
531 | |||
532 | /** | ||
533 | * If defined (for example by {@link CKEDITOR.style#addCustomHandler custom style handler}), it returns | ||
534 | * the {@link CKEDITOR.filter.allowedContentRules allowed content rules} which should be added to the | ||
535 | * {@link CKEDITOR.filter} when enabling this style. | ||
536 | * | ||
537 | * **Note:** This method is not defined in the {@link CKEDITOR.style} class. | ||
538 | * | ||
539 | * @since 4.4 | ||
540 | * @method toAllowedContentRules | ||
541 | * @param {CKEDITOR.editor} [editor] The editor instance. | ||
542 | * @returns {CKEDITOR.filter.allowedContentRules} The rules that should represent this style in the {@link CKEDITOR.filter}. | ||
543 | */ | ||
544 | }; | ||
545 | |||
546 | /** | ||
547 | * Builds the inline style text based on the style definition. | ||
548 | * | ||
549 | * @static | ||
550 | * @param styleDefinition | ||
551 | * @returns {String} Inline style text. | ||
552 | */ | ||
553 | CKEDITOR.style.getStyleText = function( styleDefinition ) { | ||
554 | // If we have already computed it, just return it. | ||
555 | var stylesDef = styleDefinition._ST; | ||
556 | if ( stylesDef ) | ||
557 | return stylesDef; | ||
558 | |||
559 | stylesDef = styleDefinition.styles; | ||
560 | |||
561 | // Builds the StyleText. | ||
562 | var stylesText = ( styleDefinition.attributes && styleDefinition.attributes.style ) || '', | ||
563 | specialStylesText = ''; | ||
564 | |||
565 | if ( stylesText.length ) | ||
566 | stylesText = stylesText.replace( semicolonFixRegex, ';' ); | ||
567 | |||
568 | for ( var style in stylesDef ) { | ||
569 | var styleVal = stylesDef[ style ], | ||
570 | text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' ); | ||
571 | |||
572 | // Some browsers don't support 'inherit' property value, leave them intact. (#5242) | ||
573 | if ( styleVal == 'inherit' ) | ||
574 | specialStylesText += text; | ||
575 | else | ||
576 | stylesText += text; | ||
577 | } | ||
578 | |||
579 | // Browsers make some changes to the style when applying them. So, here | ||
580 | // we normalize it to the browser format. | ||
581 | if ( stylesText.length ) | ||
582 | stylesText = CKEDITOR.tools.normalizeCssText( stylesText, true ); | ||
583 | |||
584 | stylesText += specialStylesText; | ||
585 | |||
586 | // Return it, saving it to the next request. | ||
587 | return ( styleDefinition._ST = stylesText ); | ||
588 | }; | ||
589 | |||
590 | /** | ||
591 | * Namespace containing custom style handlers added with {@link CKEDITOR.style#addCustomHandler}. | ||
592 | * | ||
593 | * @since 4.4 | ||
594 | * @class | ||
595 | * @singleton | ||
596 | */ | ||
597 | CKEDITOR.style.customHandlers = {}; | ||
598 | |||
599 | /** | ||
600 | * Creates a {@link CKEDITOR.style} subclass and registers it in the style system. | ||
601 | * Registered class will be used as a handler for a style of this type. This allows | ||
602 | * to extend the styles system, which by default uses only the {@link CKEDITOR.style}, with | ||
603 | * new functionality. Registered classes are accessible in the {@link CKEDITOR.style.customHandlers}. | ||
604 | * | ||
605 | * ### The Style Class Definition | ||
606 | * | ||
607 | * The definition object is used to override properties in a prototype inherited | ||
608 | * from the {@link CKEDITOR.style} class. It must contain a `type` property which is | ||
609 | * a name of the new type and therefore it must be unique. The default style types | ||
610 | * ({@link CKEDITOR#STYLE_BLOCK STYLE_BLOCK}, {@link CKEDITOR#STYLE_INLINE STYLE_INLINE}, | ||
611 | * and {@link CKEDITOR#STYLE_OBJECT STYLE_OBJECT}) are integers, but for easier identification | ||
612 | * it is recommended to use strings as custom type names. | ||
613 | * | ||
614 | * Besides `type`, the definition may contain two more special properties: | ||
615 | * | ||
616 | * * `setup {Function}` – An optional callback executed when a style instance is created. | ||
617 | * Like the style constructor, it is executed in style context and with the style definition as an argument. | ||
618 | * * `assignedTo {Number}` – Can be set to one of the default style types. Some editor | ||
619 | * features like the Styles drop-down assign styles to one of the default groups based on | ||
620 | * the style type. By using this property it is possible to notify them to which group this | ||
621 | * custom style should be assigned. It defaults to the {@link CKEDITOR#STYLE_OBJECT}. | ||
622 | * | ||
623 | * Other properties of the definition object will just be used to extend the prototype inherited | ||
624 | * from the {@link CKEDITOR.style} class. So if the definition contains an `apply` method, it will | ||
625 | * override the {@link CKEDITOR.style#apply} method. | ||
626 | * | ||
627 | * ### Usage | ||
628 | * | ||
629 | * Registering a basic handler: | ||
630 | * | ||
631 | * var styleClass = CKEDITOR.style.addCustomHandler( { | ||
632 | * type: 'custom' | ||
633 | * } ); | ||
634 | * | ||
635 | * var style = new styleClass( { ... } ); | ||
636 | * style instanceof styleClass; // -> true | ||
637 | * style instanceof CKEDITOR.style; // -> true | ||
638 | * style.type; // -> 'custom' | ||
639 | * | ||
640 | * The {@link CKEDITOR.style} constructor used as a factory: | ||
641 | * | ||
642 | * var styleClass = CKEDITOR.style.addCustomHandler( { | ||
643 | * type: 'custom' | ||
644 | * } ); | ||
645 | * | ||
646 | * // Style constructor accepts style definition (do not confuse with style class definition). | ||
647 | * var style = new CKEDITOR.style( { type: 'custom', attributes: ... } ); | ||
648 | * style instanceof styleClass; // -> true | ||
649 | * | ||
650 | * Thanks to that, integration code using styles does not need to know | ||
651 | * which style handler it should use. It is determined by the {@link CKEDITOR.style} constructor. | ||
652 | * | ||
653 | * Overriding existing {@link CKEDITOR.style} methods: | ||
654 | * | ||
655 | * var styleClass = CKEDITOR.style.addCustomHandler( { | ||
656 | * type: 'custom', | ||
657 | * apply: function( editor ) { | ||
658 | * console.log( 'apply' ); | ||
659 | * }, | ||
660 | * remove: function( editor ) { | ||
661 | * console.log( 'remove' ); | ||
662 | * } | ||
663 | * } ); | ||
664 | * | ||
665 | * var style = new CKEDITOR.style( { type: 'custom', attributes: ... } ); | ||
666 | * editor.applyStyle( style ); // logged 'apply' | ||
667 | * | ||
668 | * style = new CKEDITOR.style( { element: 'img', attributes: { 'class': 'foo' } } ); | ||
669 | * editor.applyStyle( style ); // style is really applied if image was selected | ||
670 | * | ||
671 | * ### Practical Recommendations | ||
672 | * | ||
673 | * The style handling job, which includes such tasks as applying, removing, checking state, and | ||
674 | * checking if a style can be applied, is very complex. Therefore without deep knowledge | ||
675 | * about DOM and especially {@link CKEDITOR.dom.range ranges} and {@link CKEDITOR.dom.walker DOM walker} it is impossible | ||
676 | * to implement a completely custom style handler able to handle block, inline, and object type styles. | ||
677 | * However, it is possible to customize the default implementation by overriding default methods and | ||
678 | * reusing them. | ||
679 | * | ||
680 | * The only style handler which can be implemented from scratch without huge effort is a style | ||
681 | * applicable to objects ([read more about types](http://docs.ckeditor.com/#!/guide/dev_styles-section-style-types)). | ||
682 | * Such style can only be applied when a specific object is selected. An example implementation can | ||
683 | * be found in the [widget plugin](https://github.com/ckeditor/ckeditor-dev/blob/master/plugins/widget/plugin.js). | ||
684 | * | ||
685 | * When implementing a style handler from scratch at least the following methods must be defined: | ||
686 | * | ||
687 | * * {@link CKEDITOR.style#apply apply} and {@link CKEDITOR.style#remove remove}, | ||
688 | * * {@link CKEDITOR.style#checkElementRemovable checkElementRemovable} and | ||
689 | * {@link CKEDITOR.style#checkElementMatch checkElementMatch} – Note that both methods reuse the same logic, | ||
690 | * * {@link CKEDITOR.style#checkActive checkActive} – Reuses | ||
691 | * {@link CKEDITOR.style#checkElementMatch checkElementMatch}, | ||
692 | * * {@link CKEDITOR.style#toAllowedContentRules toAllowedContentRules} – Not required, but very useful in | ||
693 | * case of a custom style that has to notify the {@link CKEDITOR.filter} which rules it allows when registered. | ||
694 | * | ||
695 | * @since 4.4 | ||
696 | * @static | ||
697 | * @member CKEDITOR.style | ||
698 | * @param definition The style class definition. | ||
699 | * @returns {CKEDITOR.style} The new style class created for the provided definition. | ||
700 | */ | ||
701 | CKEDITOR.style.addCustomHandler = function( definition ) { | ||
702 | var styleClass = function( styleDefinition ) { | ||
703 | this._ = { | ||
704 | definition: styleDefinition | ||
705 | }; | ||
706 | |||
707 | if ( this.setup ) | ||
708 | this.setup( styleDefinition ); | ||
709 | }; | ||
710 | |||
711 | styleClass.prototype = CKEDITOR.tools.extend( | ||
712 | // Prototype of CKEDITOR.style. | ||
713 | CKEDITOR.tools.prototypedCopy( CKEDITOR.style.prototype ), | ||
714 | // Defaults. | ||
715 | { | ||
716 | assignedTo: CKEDITOR.STYLE_OBJECT | ||
717 | }, | ||
718 | // Passed definition - overrides. | ||
719 | definition, | ||
720 | true | ||
721 | ); | ||
722 | |||
723 | this.customHandlers[ definition.type ] = styleClass; | ||
724 | |||
725 | return styleClass; | ||
726 | }; | ||
727 | |||
728 | // Gets the parent element which blocks the styling for an element. This | ||
729 | // can be done through read-only elements (contenteditable=false) or | ||
730 | // elements with the "data-nostyle" attribute. | ||
731 | function getUnstylableParent( element, root ) { | ||
732 | var unstylable, editable; | ||
733 | |||
734 | while ( ( element = element.getParent() ) ) { | ||
735 | if ( element.equals( root ) ) | ||
736 | break; | ||
737 | |||
738 | if ( element.getAttribute( 'data-nostyle' ) ) | ||
739 | unstylable = element; | ||
740 | else if ( !editable ) { | ||
741 | var contentEditable = element.getAttribute( 'contentEditable' ); | ||
742 | |||
743 | if ( contentEditable == 'false' ) | ||
744 | unstylable = element; | ||
745 | else if ( contentEditable == 'true' ) | ||
746 | editable = 1; | ||
747 | } | ||
748 | } | ||
749 | |||
750 | return unstylable; | ||
751 | } | ||
752 | |||
753 | var posPrecedingIdenticalContained = | ||
754 | CKEDITOR.POSITION_PRECEDING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED, | ||
755 | posFollowingIdenticalContained = | ||
756 | CKEDITOR.POSITION_FOLLOWING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED; | ||
757 | |||
758 | // Checks if the current node can be a child of the style element. | ||
759 | function checkIfNodeCanBeChildOfStyle( def, currentNode, lastNode, nodeName, dtd, nodeIsNoStyle, nodeIsReadonly, includeReadonly ) { | ||
760 | // Style can be applied to text node. | ||
761 | if ( !nodeName ) | ||
762 | return 1; | ||
763 | |||
764 | // Style definitely cannot be applied if DTD or data-nostyle do not allow. | ||
765 | if ( !dtd[ nodeName ] || nodeIsNoStyle ) | ||
766 | return 0; | ||
767 | |||
768 | // Non-editable element cannot be styled is we shouldn't include readonly elements. | ||
769 | if ( nodeIsReadonly && !includeReadonly ) | ||
770 | return 0; | ||
771 | |||
772 | // Check that we haven't passed lastNode yet and that style's childRule allows this style on current element. | ||
773 | return checkPositionAndRule( currentNode, lastNode, def, posPrecedingIdenticalContained ); | ||
774 | } | ||
775 | |||
776 | // Check if the style element can be a child of the current | ||
777 | // node parent or if the element is not defined in the DTD. | ||
778 | function checkIfStyleCanBeChildOf( def, currentParent, elementName, isUnknownElement ) { | ||
779 | return currentParent && | ||
780 | ( ( currentParent.getDtd() || CKEDITOR.dtd.span )[ elementName ] || isUnknownElement ) && | ||
781 | ( !def.parentRule || def.parentRule( currentParent ) ); | ||
782 | } | ||
783 | |||
784 | function checkIfStartsRange( nodeName, currentNode, lastNode ) { | ||
785 | return ( | ||
786 | !nodeName || !CKEDITOR.dtd.$removeEmpty[ nodeName ] || | ||
787 | ( currentNode.getPosition( lastNode ) | posPrecedingIdenticalContained ) == posPrecedingIdenticalContained | ||
788 | ); | ||
789 | } | ||
790 | |||
791 | function checkIfTextOrReadonlyOrEmptyElement( currentNode, nodeIsReadonly ) { | ||
792 | var nodeType = currentNode.type; | ||
793 | return nodeType == CKEDITOR.NODE_TEXT || nodeIsReadonly || ( nodeType == CKEDITOR.NODE_ELEMENT && !currentNode.getChildCount() ); | ||
794 | } | ||
795 | |||
796 | // Checks if position is a subset of posBitFlags and that nodeA fulfills style def rule. | ||
797 | function checkPositionAndRule( nodeA, nodeB, def, posBitFlags ) { | ||
798 | return ( nodeA.getPosition( nodeB ) | posBitFlags ) == posBitFlags && | ||
799 | ( !def.childRule || def.childRule( nodeA ) ); | ||
800 | } | ||
801 | |||
802 | function applyInlineStyle( range ) { | ||
803 | var document = range.document; | ||
804 | |||
805 | if ( range.collapsed ) { | ||
806 | // Create the element to be inserted in the DOM. | ||
807 | var collapsedElement = getElement( this, document ); | ||
808 | |||
809 | // Insert the empty element into the DOM at the range position. | ||
810 | range.insertNode( collapsedElement ); | ||
811 | |||
812 | // Place the selection right inside the empty element. | ||
813 | range.moveToPosition( collapsedElement, CKEDITOR.POSITION_BEFORE_END ); | ||
814 | |||
815 | return; | ||
816 | } | ||
817 | |||
818 | var elementName = this.element, | ||
819 | def = this._.definition, | ||
820 | isUnknownElement; | ||
821 | |||
822 | // Indicates that fully selected read-only elements are to be included in the styling range. | ||
823 | var ignoreReadonly = def.ignoreReadonly, | ||
824 | includeReadonly = ignoreReadonly || def.includeReadonly; | ||
825 | |||
826 | // If the read-only inclusion is not available in the definition, try | ||
827 | // to get it from the root data (most often it's the editable). | ||
828 | if ( includeReadonly == null ) | ||
829 | includeReadonly = range.root.getCustomData( 'cke_includeReadonly' ); | ||
830 | |||
831 | // Get the DTD definition for the element. Defaults to "span". | ||
832 | var dtd = CKEDITOR.dtd[ elementName ]; | ||
833 | if ( !dtd ) { | ||
834 | isUnknownElement = true; | ||
835 | dtd = CKEDITOR.dtd.span; | ||
836 | } | ||
837 | |||
838 | // Expand the range. | ||
839 | range.enlarge( CKEDITOR.ENLARGE_INLINE, 1 ); | ||
840 | range.trim(); | ||
841 | |||
842 | // Get the first node to be processed and the last, which concludes the processing. | ||
843 | var boundaryNodes = range.createBookmark(), | ||
844 | firstNode = boundaryNodes.startNode, | ||
845 | lastNode = boundaryNodes.endNode, | ||
846 | currentNode = firstNode, | ||
847 | styleRange; | ||
848 | |||
849 | if ( !ignoreReadonly ) { | ||
850 | // Check if the boundaries are inside non stylable elements. | ||
851 | var root = range.getCommonAncestor(), | ||
852 | firstUnstylable = getUnstylableParent( firstNode, root ), | ||
853 | lastUnstylable = getUnstylableParent( lastNode, root ); | ||
854 | |||
855 | // If the first element can't be styled, we'll start processing right | ||
856 | // after its unstylable root. | ||
857 | if ( firstUnstylable ) | ||
858 | currentNode = firstUnstylable.getNextSourceNode( true ); | ||
859 | |||
860 | // If the last element can't be styled, we'll stop processing on its | ||
861 | // unstylable root. | ||
862 | if ( lastUnstylable ) | ||
863 | lastNode = lastUnstylable; | ||
864 | } | ||
865 | |||
866 | // Do nothing if the current node now follows the last node to be processed. | ||
867 | if ( currentNode.getPosition( lastNode ) == CKEDITOR.POSITION_FOLLOWING ) | ||
868 | currentNode = 0; | ||
869 | |||
870 | while ( currentNode ) { | ||
871 | var applyStyle = false; | ||
872 | |||
873 | if ( currentNode.equals( lastNode ) ) { | ||
874 | currentNode = null; | ||
875 | applyStyle = true; | ||
876 | } else { | ||
877 | var nodeName = currentNode.type == CKEDITOR.NODE_ELEMENT ? currentNode.getName() : null, | ||
878 | nodeIsReadonly = nodeName && ( currentNode.getAttribute( 'contentEditable' ) == 'false' ), | ||
879 | nodeIsNoStyle = nodeName && currentNode.getAttribute( 'data-nostyle' ); | ||
880 | |||
881 | // Skip bookmarks. | ||
882 | if ( nodeName && currentNode.data( 'cke-bookmark' ) ) { | ||
883 | currentNode = currentNode.getNextSourceNode( true ); | ||
884 | continue; | ||
885 | } | ||
886 | |||
887 | // Find all nested editables of a non-editable block and apply this style inside them. | ||
888 | if ( nodeIsReadonly && includeReadonly && CKEDITOR.dtd.$block[ nodeName ] ) | ||
889 | applyStyleOnNestedEditables.call( this, currentNode ); | ||
890 | |||
891 | // Check if the current node can be a child of the style element. | ||
892 | if ( checkIfNodeCanBeChildOfStyle( def, currentNode, lastNode, nodeName, dtd, nodeIsNoStyle, nodeIsReadonly, includeReadonly ) ) { | ||
893 | var currentParent = currentNode.getParent(); | ||
894 | |||
895 | // Check if the style element can be a child of the current | ||
896 | // node parent or if the element is not defined in the DTD. | ||
897 | if ( checkIfStyleCanBeChildOf( def, currentParent, elementName, isUnknownElement ) ) { | ||
898 | // This node will be part of our range, so if it has not | ||
899 | // been started, place its start right before the node. | ||
900 | // In the case of an element node, it will be included | ||
901 | // only if it is entirely inside the range. | ||
902 | if ( !styleRange && checkIfStartsRange( nodeName, currentNode, lastNode ) ) { | ||
903 | styleRange = range.clone(); | ||
904 | styleRange.setStartBefore( currentNode ); | ||
905 | } | ||
906 | |||
907 | // Non element nodes, readonly elements, or empty | ||
908 | // elements can be added completely to the range. | ||
909 | if ( checkIfTextOrReadonlyOrEmptyElement( currentNode, nodeIsReadonly ) ) { | ||
910 | var includedNode = currentNode; | ||
911 | var parentNode; | ||
912 | |||
913 | // This node is about to be included completelly, but, | ||
914 | // if this is the last node in its parent, we must also | ||
915 | // check if the parent itself can be added completelly | ||
916 | // to the range, otherwise apply the style immediately. | ||
917 | while ( | ||
918 | ( applyStyle = !includedNode.getNext( notBookmark ) ) && | ||
919 | ( parentNode = includedNode.getParent(), dtd[ parentNode.getName() ] ) && | ||
920 | checkPositionAndRule( parentNode, firstNode, def, posFollowingIdenticalContained ) | ||
921 | ) { | ||
922 | includedNode = parentNode; | ||
923 | } | ||
924 | |||
925 | styleRange.setEndAfter( includedNode ); | ||
926 | |||
927 | } | ||
928 | } else { | ||
929 | applyStyle = true; | ||
930 | } | ||
931 | } | ||
932 | // Style isn't applicable to current element, so apply style to | ||
933 | // range ending at previously chosen position, or nowhere if we haven't | ||
934 | // yet started styleRange. | ||
935 | else { | ||
936 | applyStyle = true; | ||
937 | } | ||
938 | |||
939 | // Get the next node to be processed. | ||
940 | // If we're currently on a non-editable element or non-styleable element, | ||
941 | // then we'll be moved to current node's sibling (or even further), so we'll | ||
942 | // avoid messing up its content. | ||
943 | currentNode = currentNode.getNextSourceNode( nodeIsNoStyle || nodeIsReadonly ); | ||
944 | } | ||
945 | |||
946 | // Apply the style if we have something to which apply it. | ||
947 | if ( applyStyle && styleRange && !styleRange.collapsed ) { | ||
948 | // Build the style element, based on the style object definition. | ||
949 | var styleNode = getElement( this, document ), | ||
950 | styleHasAttrs = styleNode.hasAttributes(); | ||
951 | |||
952 | // Get the element that holds the entire range. | ||
953 | var parent = styleRange.getCommonAncestor(); | ||
954 | |||
955 | var removeList = { | ||
956 | styles: {}, | ||
957 | attrs: {}, | ||
958 | // Styles cannot be removed. | ||
959 | blockedStyles: {}, | ||
960 | // Attrs cannot be removed. | ||
961 | blockedAttrs: {} | ||
962 | }; | ||
963 | |||
964 | var attName, styleName, value; | ||
965 | |||
966 | // Loop through the parents, removing the redundant attributes | ||
967 | // from the element to be applied. | ||
968 | while ( styleNode && parent ) { | ||
969 | if ( parent.getName() == elementName ) { | ||
970 | for ( attName in def.attributes ) { | ||
971 | if ( removeList.blockedAttrs[ attName ] || !( value = parent.getAttribute( styleName ) ) ) | ||
972 | continue; | ||
973 | |||
974 | if ( styleNode.getAttribute( attName ) == value ) | ||
975 | removeList.attrs[ attName ] = 1; | ||
976 | else | ||
977 | removeList.blockedAttrs[ attName ] = 1; | ||
978 | } | ||
979 | |||
980 | for ( styleName in def.styles ) { | ||
981 | if ( removeList.blockedStyles[ styleName ] || !( value = parent.getStyle( styleName ) ) ) | ||
982 | continue; | ||
983 | |||
984 | if ( styleNode.getStyle( styleName ) == value ) | ||
985 | removeList.styles[ styleName ] = 1; | ||
986 | else | ||
987 | removeList.blockedStyles[ styleName ] = 1; | ||
988 | } | ||
989 | } | ||
990 | |||
991 | parent = parent.getParent(); | ||
992 | } | ||
993 | |||
994 | for ( attName in removeList.attrs ) | ||
995 | styleNode.removeAttribute( attName ); | ||
996 | |||
997 | for ( styleName in removeList.styles ) | ||
998 | styleNode.removeStyle( styleName ); | ||
999 | |||
1000 | if ( styleHasAttrs && !styleNode.hasAttributes() ) | ||
1001 | styleNode = null; | ||
1002 | |||
1003 | if ( styleNode ) { | ||
1004 | // Move the contents of the range to the style element. | ||
1005 | styleRange.extractContents().appendTo( styleNode ); | ||
1006 | |||
1007 | // Insert it into the range position (it is collapsed after | ||
1008 | // extractContents. | ||
1009 | styleRange.insertNode( styleNode ); | ||
1010 | |||
1011 | // Here we do some cleanup, removing all duplicated | ||
1012 | // elements from the style element. | ||
1013 | removeFromInsideElement.call( this, styleNode ); | ||
1014 | |||
1015 | // Let's merge our new style with its neighbors, if possible. | ||
1016 | styleNode.mergeSiblings(); | ||
1017 | |||
1018 | // As the style system breaks text nodes constantly, let's normalize | ||
1019 | // things for performance. | ||
1020 | // With IE, some paragraphs get broken when calling normalize() | ||
1021 | // repeatedly. Also, for IE, we must normalize body, not documentElement. | ||
1022 | // IE is also known for having a "crash effect" with normalize(). | ||
1023 | // We should try to normalize with IE too in some way, somewhere. | ||
1024 | if ( !CKEDITOR.env.ie ) | ||
1025 | styleNode.$.normalize(); | ||
1026 | } | ||
1027 | // Style already inherit from parents, left just to clear up any internal overrides. (#5931) | ||
1028 | else { | ||
1029 | styleNode = new CKEDITOR.dom.element( 'span' ); | ||
1030 | styleRange.extractContents().appendTo( styleNode ); | ||
1031 | styleRange.insertNode( styleNode ); | ||
1032 | removeFromInsideElement.call( this, styleNode ); | ||
1033 | styleNode.remove( true ); | ||
1034 | } | ||
1035 | |||
1036 | // Style applied, let's release the range, so it gets | ||
1037 | // re-initialization in the next loop. | ||
1038 | styleRange = null; | ||
1039 | } | ||
1040 | } | ||
1041 | |||
1042 | // Remove the bookmark nodes. | ||
1043 | range.moveToBookmark( boundaryNodes ); | ||
1044 | |||
1045 | // Minimize the result range to exclude empty text nodes. (#5374) | ||
1046 | range.shrink( CKEDITOR.SHRINK_TEXT ); | ||
1047 | |||
1048 | // Get inside the remaining element if range.shrink( TEXT ) has failed because of non-editable elements inside. | ||
1049 | // E.g. range.shrink( TEXT ) will not get inside: | ||
1050 | // [<b><i contenteditable="false">x</i></b>] | ||
1051 | // but range.shrink( ELEMENT ) will. | ||
1052 | range.shrink( CKEDITOR.NODE_ELEMENT, true ); | ||
1053 | } | ||
1054 | |||
1055 | function removeInlineStyle( range ) { | ||
1056 | // Make sure our range has included all "collpased" parent inline nodes so | ||
1057 | // that our operation logic can be simpler. | ||
1058 | range.enlarge( CKEDITOR.ENLARGE_INLINE, 1 ); | ||
1059 | |||
1060 | var bookmark = range.createBookmark(), | ||
1061 | startNode = bookmark.startNode; | ||
1062 | |||
1063 | if ( range.collapsed ) { | ||
1064 | var startPath = new CKEDITOR.dom.elementPath( startNode.getParent(), range.root ), | ||
1065 | // The topmost element in elementspatch which we should jump out of. | ||
1066 | boundaryElement; | ||
1067 | |||
1068 | |||
1069 | for ( var i = 0, element; i < startPath.elements.length && ( element = startPath.elements[ i ] ); i++ ) { | ||
1070 | // 1. If it's collaped inside text nodes, try to remove the style from the whole element. | ||
1071 | // | ||
1072 | // 2. Otherwise if it's collapsed on element boundaries, moving the selection | ||
1073 | // outside the styles instead of removing the whole tag, | ||
1074 | // also make sure other inner styles were well preserverd.(#3309) | ||
1075 | if ( element == startPath.block || element == startPath.blockLimit ) | ||
1076 | break; | ||
1077 | |||
1078 | if ( this.checkElementRemovable( element ) ) { | ||
1079 | var isStart; | ||
1080 | |||
1081 | if ( range.collapsed && ( range.checkBoundaryOfElement( element, CKEDITOR.END ) || ( isStart = range.checkBoundaryOfElement( element, CKEDITOR.START ) ) ) ) { | ||
1082 | boundaryElement = element; | ||
1083 | boundaryElement.match = isStart ? 'start' : 'end'; | ||
1084 | } else { | ||
1085 | // Before removing the style node, there may be a sibling to the style node | ||
1086 | // that's exactly the same to the one to be removed. To the user, it makes | ||
1087 | // no difference that they're separate entities in the DOM tree. So, merge | ||
1088 | // them before removal. | ||
1089 | element.mergeSiblings(); | ||
1090 | if ( element.is( this.element ) ) | ||
1091 | removeFromElement.call( this, element ); | ||
1092 | else | ||
1093 | removeOverrides( element, getOverrides( this )[ element.getName() ] ); | ||
1094 | } | ||
1095 | } | ||
1096 | } | ||
1097 | |||
1098 | // Re-create the style tree after/before the boundary element, | ||
1099 | // the replication start from bookmark start node to define the | ||
1100 | // new range. | ||
1101 | if ( boundaryElement ) { | ||
1102 | var clonedElement = startNode; | ||
1103 | for ( i = 0; ; i++ ) { | ||
1104 | var newElement = startPath.elements[ i ]; | ||
1105 | if ( newElement.equals( boundaryElement ) ) | ||
1106 | break; | ||
1107 | // Avoid copying any matched element. | ||
1108 | else if ( newElement.match ) | ||
1109 | continue; | ||
1110 | else | ||
1111 | newElement = newElement.clone(); | ||
1112 | newElement.append( clonedElement ); | ||
1113 | clonedElement = newElement; | ||
1114 | } | ||
1115 | clonedElement[ boundaryElement.match == 'start' ? 'insertBefore' : 'insertAfter' ]( boundaryElement ); | ||
1116 | } | ||
1117 | } else { | ||
1118 | // Now our range isn't collapsed. Lets walk from the start node to the end | ||
1119 | // node via DFS and remove the styles one-by-one. | ||
1120 | var endNode = bookmark.endNode, | ||
1121 | me = this; | ||
1122 | |||
1123 | breakNodes(); | ||
1124 | |||
1125 | // Now, do the DFS walk. | ||
1126 | var currentNode = startNode; | ||
1127 | while ( !currentNode.equals( endNode ) ) { | ||
1128 | // Need to get the next node first because removeFromElement() can remove | ||
1129 | // the current node from DOM tree. | ||
1130 | var nextNode = currentNode.getNextSourceNode(); | ||
1131 | if ( currentNode.type == CKEDITOR.NODE_ELEMENT && this.checkElementRemovable( currentNode ) ) { | ||
1132 | // Remove style from element or overriding element. | ||
1133 | if ( currentNode.getName() == this.element ) | ||
1134 | removeFromElement.call( this, currentNode ); | ||
1135 | else | ||
1136 | removeOverrides( currentNode, getOverrides( this )[ currentNode.getName() ] ); | ||
1137 | |||
1138 | // removeFromElement() may have merged the next node with something before | ||
1139 | // the startNode via mergeSiblings(). In that case, the nextNode would | ||
1140 | // contain startNode and we'll have to call breakNodes() again and also | ||
1141 | // reassign the nextNode to something after startNode. | ||
1142 | if ( nextNode.type == CKEDITOR.NODE_ELEMENT && nextNode.contains( startNode ) ) { | ||
1143 | breakNodes(); | ||
1144 | nextNode = startNode.getNext(); | ||
1145 | } | ||
1146 | } | ||
1147 | currentNode = nextNode; | ||
1148 | } | ||
1149 | } | ||
1150 | |||
1151 | range.moveToBookmark( bookmark ); | ||
1152 | // See the comment for range.shrink in applyInlineStyle. | ||
1153 | range.shrink( CKEDITOR.NODE_ELEMENT, true ); | ||
1154 | |||
1155 | // Find out the style ancestor that needs to be broken down at startNode | ||
1156 | // and endNode. | ||
1157 | function breakNodes() { | ||
1158 | var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ), | ||
1159 | endPath = new CKEDITOR.dom.elementPath( endNode.getParent() ), | ||
1160 | breakStart = null, | ||
1161 | breakEnd = null; | ||
1162 | |||
1163 | for ( var i = 0; i < startPath.elements.length; i++ ) { | ||
1164 | var element = startPath.elements[ i ]; | ||
1165 | |||
1166 | if ( element == startPath.block || element == startPath.blockLimit ) | ||
1167 | break; | ||
1168 | |||
1169 | if ( me.checkElementRemovable( element, true ) ) | ||
1170 | breakStart = element; | ||
1171 | } | ||
1172 | |||
1173 | for ( i = 0; i < endPath.elements.length; i++ ) { | ||
1174 | element = endPath.elements[ i ]; | ||
1175 | |||
1176 | if ( element == endPath.block || element == endPath.blockLimit ) | ||
1177 | break; | ||
1178 | |||
1179 | if ( me.checkElementRemovable( element, true ) ) | ||
1180 | breakEnd = element; | ||
1181 | } | ||
1182 | |||
1183 | if ( breakEnd ) | ||
1184 | endNode.breakParent( breakEnd ); | ||
1185 | if ( breakStart ) | ||
1186 | startNode.breakParent( breakStart ); | ||
1187 | } | ||
1188 | } | ||
1189 | |||
1190 | // Apply style to nested editables inside editablesContainer. | ||
1191 | // @param {CKEDITOR.dom.element} editablesContainer | ||
1192 | // @context CKEDITOR.style | ||
1193 | function applyStyleOnNestedEditables( editablesContainer ) { | ||
1194 | var editables = findNestedEditables( editablesContainer ), | ||
1195 | editable, | ||
1196 | l = editables.length, | ||
1197 | i = 0, | ||
1198 | range = l && new CKEDITOR.dom.range( editablesContainer.getDocument() ); | ||
1199 | |||
1200 | for ( ; i < l; ++i ) { | ||
1201 | editable = editables[ i ]; | ||
1202 | // Check if style is allowed by this editable's ACF. | ||
1203 | if ( checkIfAllowedInEditable( editable, this ) ) { | ||
1204 | range.selectNodeContents( editable ); | ||
1205 | applyInlineStyle.call( this, range ); | ||
1206 | } | ||
1207 | } | ||
1208 | } | ||
1209 | |||
1210 | // Finds nested editables within container. Does not return | ||
1211 | // editables nested in another editable (twice). | ||
1212 | function findNestedEditables( container ) { | ||
1213 | var editables = []; | ||
1214 | |||
1215 | container.forEach( function( element ) { | ||
1216 | if ( element.getAttribute( 'contenteditable' ) == 'true' ) { | ||
1217 | editables.push( element ); | ||
1218 | return false; // Skip children. | ||
1219 | } | ||
1220 | }, CKEDITOR.NODE_ELEMENT, true ); | ||
1221 | |||
1222 | return editables; | ||
1223 | } | ||
1224 | |||
1225 | // Checks if style is allowed in this editable. | ||
1226 | function checkIfAllowedInEditable( editable, style ) { | ||
1227 | var filter = CKEDITOR.filter.instances[ editable.data( 'cke-filter' ) ]; | ||
1228 | |||
1229 | return filter ? filter.check( style ) : 1; | ||
1230 | } | ||
1231 | |||
1232 | // Checks if style is allowed by iterator's active filter. | ||
1233 | function checkIfAllowedByIterator( iterator, style ) { | ||
1234 | return iterator.activeFilter ? iterator.activeFilter.check( style ) : 1; | ||
1235 | } | ||
1236 | |||
1237 | function applyObjectStyle( range ) { | ||
1238 | // Selected or parent element. (#9651) | ||
1239 | var start = range.getEnclosedNode() || range.getCommonAncestor( false, true ), | ||
1240 | element = new CKEDITOR.dom.elementPath( start, range.root ).contains( this.element, 1 ); | ||
1241 | |||
1242 | element && !element.isReadOnly() && setupElement( element, this ); | ||
1243 | } | ||
1244 | |||
1245 | function removeObjectStyle( range ) { | ||
1246 | var parent = range.getCommonAncestor( true, true ), | ||
1247 | element = new CKEDITOR.dom.elementPath( parent, range.root ).contains( this.element, 1 ); | ||
1248 | |||
1249 | if ( !element ) | ||
1250 | return; | ||
1251 | |||
1252 | var style = this, | ||
1253 | def = style._.definition, | ||
1254 | attributes = def.attributes; | ||
1255 | |||
1256 | // Remove all defined attributes. | ||
1257 | if ( attributes ) { | ||
1258 | for ( var att in attributes ) | ||
1259 | element.removeAttribute( att, attributes[ att ] ); | ||
1260 | } | ||
1261 | |||
1262 | // Assign all defined styles. | ||
1263 | if ( def.styles ) { | ||
1264 | for ( var i in def.styles ) { | ||
1265 | if ( def.styles.hasOwnProperty( i ) ) | ||
1266 | element.removeStyle( i ); | ||
1267 | } | ||
1268 | } | ||
1269 | } | ||
1270 | |||
1271 | function applyBlockStyle( range ) { | ||
1272 | // Serializible bookmarks is needed here since | ||
1273 | // elements may be merged. | ||
1274 | var bookmark = range.createBookmark( true ); | ||
1275 | |||
1276 | var iterator = range.createIterator(); | ||
1277 | iterator.enforceRealBlocks = true; | ||
1278 | |||
1279 | // make recognize <br /> tag as a separator in ENTER_BR mode (#5121) | ||
1280 | if ( this._.enterMode ) | ||
1281 | iterator.enlargeBr = ( this._.enterMode != CKEDITOR.ENTER_BR ); | ||
1282 | |||
1283 | var block, | ||
1284 | doc = range.document, | ||
1285 | newBlock; | ||
1286 | |||
1287 | while ( ( block = iterator.getNextParagraph() ) ) { | ||
1288 | if ( !block.isReadOnly() && checkIfAllowedByIterator( iterator, this ) ) { | ||
1289 | newBlock = getElement( this, doc, block ); | ||
1290 | replaceBlock( block, newBlock ); | ||
1291 | } | ||
1292 | } | ||
1293 | |||
1294 | range.moveToBookmark( bookmark ); | ||
1295 | } | ||
1296 | |||
1297 | function removeBlockStyle( range ) { | ||
1298 | // Serializible bookmarks is needed here since | ||
1299 | // elements may be merged. | ||
1300 | var bookmark = range.createBookmark( 1 ); | ||
1301 | |||
1302 | var iterator = range.createIterator(); | ||
1303 | iterator.enforceRealBlocks = true; | ||
1304 | iterator.enlargeBr = this._.enterMode != CKEDITOR.ENTER_BR; | ||
1305 | |||
1306 | var block, | ||
1307 | newBlock; | ||
1308 | |||
1309 | while ( ( block = iterator.getNextParagraph() ) ) { | ||
1310 | if ( this.checkElementRemovable( block ) ) { | ||
1311 | // <pre> get special treatment. | ||
1312 | if ( block.is( 'pre' ) ) { | ||
1313 | newBlock = this._.enterMode == CKEDITOR.ENTER_BR ? null : | ||
1314 | range.document.createElement( this._.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ); | ||
1315 | |||
1316 | newBlock && block.copyAttributes( newBlock ); | ||
1317 | replaceBlock( block, newBlock ); | ||
1318 | } else { | ||
1319 | removeFromElement.call( this, block ); | ||
1320 | } | ||
1321 | } | ||
1322 | } | ||
1323 | |||
1324 | range.moveToBookmark( bookmark ); | ||
1325 | } | ||
1326 | |||
1327 | // Replace the original block with new one, with special treatment | ||
1328 | // for <pre> blocks to make sure content format is well preserved, and merging/splitting adjacent | ||
1329 | // when necessary. (#3188) | ||
1330 | function replaceBlock( block, newBlock ) { | ||
1331 | // Block is to be removed, create a temp element to | ||
1332 | // save contents. | ||
1333 | var removeBlock = !newBlock; | ||
1334 | if ( removeBlock ) { | ||
1335 | newBlock = block.getDocument().createElement( 'div' ); | ||
1336 | block.copyAttributes( newBlock ); | ||
1337 | } | ||
1338 | |||
1339 | var newBlockIsPre = newBlock && newBlock.is( 'pre' ), | ||
1340 | blockIsPre = block.is( 'pre' ), | ||
1341 | isToPre = newBlockIsPre && !blockIsPre, | ||
1342 | isFromPre = !newBlockIsPre && blockIsPre; | ||
1343 | |||
1344 | if ( isToPre ) | ||
1345 | newBlock = toPre( block, newBlock ); | ||
1346 | else if ( isFromPre ) | ||
1347 | // Split big <pre> into pieces before start to convert. | ||
1348 | newBlock = fromPres( removeBlock ? [ block.getHtml() ] : splitIntoPres( block ), newBlock ); | ||
1349 | else | ||
1350 | block.moveChildren( newBlock ); | ||
1351 | |||
1352 | newBlock.replace( block ); | ||
1353 | |||
1354 | if ( newBlockIsPre ) { | ||
1355 | // Merge previous <pre> blocks. | ||
1356 | mergePre( newBlock ); | ||
1357 | } else if ( removeBlock ) { | ||
1358 | removeNoAttribsElement( newBlock ); | ||
1359 | } | ||
1360 | } | ||
1361 | |||
1362 | // Merge a <pre> block with a previous sibling if available. | ||
1363 | function mergePre( preBlock ) { | ||
1364 | var previousBlock; | ||
1365 | if ( !( ( previousBlock = preBlock.getPrevious( nonWhitespaces ) ) && previousBlock.type == CKEDITOR.NODE_ELEMENT && previousBlock.is( 'pre' ) ) ) | ||
1366 | return; | ||
1367 | |||
1368 | // Merge the previous <pre> block contents into the current <pre> | ||
1369 | // block. | ||
1370 | // | ||
1371 | // Another thing to be careful here is that currentBlock might contain | ||
1372 | // a '\n' at the beginning, and previousBlock might contain a '\n' | ||
1373 | // towards the end. These new lines are not normally displayed but they | ||
1374 | // become visible after merging. | ||
1375 | var mergedHtml = replace( previousBlock.getHtml(), /\n$/, '' ) + '\n\n' + | ||
1376 | replace( preBlock.getHtml(), /^\n/, '' ); | ||
1377 | |||
1378 | // Krugle: IE normalizes innerHTML from <pre>, breaking whitespaces. | ||
1379 | if ( CKEDITOR.env.ie ) | ||
1380 | preBlock.$.outerHTML = '<pre>' + mergedHtml + '</pre>'; | ||
1381 | else | ||
1382 | preBlock.setHtml( mergedHtml ); | ||
1383 | |||
1384 | previousBlock.remove(); | ||
1385 | } | ||
1386 | |||
1387 | // Split into multiple <pre> blocks separated by double line-break. | ||
1388 | function splitIntoPres( preBlock ) { | ||
1389 | // Exclude the ones at header OR at tail, | ||
1390 | // and ignore bookmark content between them. | ||
1391 | var duoBrRegex = /(\S\s*)\n(?:\s|(<span[^>]+data-cke-bookmark.*?\/span>))*\n(?!$)/gi, | ||
1392 | pres = [], | ||
1393 | splitedHtml = replace( preBlock.getOuterHtml(), duoBrRegex, function( match, charBefore, bookmark ) { | ||
1394 | return charBefore + '</pre>' + bookmark + '<pre>'; | ||
1395 | } ); | ||
1396 | |||
1397 | splitedHtml.replace( /<pre\b.*?>([\s\S]*?)<\/pre>/gi, function( match, preContent ) { | ||
1398 | pres.push( preContent ); | ||
1399 | } ); | ||
1400 | return pres; | ||
1401 | } | ||
1402 | |||
1403 | // Wrapper function of String::replace without considering of head/tail bookmarks nodes. | ||
1404 | function replace( str, regexp, replacement ) { | ||
1405 | var headBookmark = '', | ||
1406 | tailBookmark = ''; | ||
1407 | |||
1408 | str = str.replace( /(^<span[^>]+data-cke-bookmark.*?\/span>)|(<span[^>]+data-cke-bookmark.*?\/span>$)/gi, function( str, m1, m2 ) { | ||
1409 | m1 && ( headBookmark = m1 ); | ||
1410 | m2 && ( tailBookmark = m2 ); | ||
1411 | return ''; | ||
1412 | } ); | ||
1413 | return headBookmark + str.replace( regexp, replacement ) + tailBookmark; | ||
1414 | } | ||
1415 | |||
1416 | // Converting a list of <pre> into blocks with format well preserved. | ||
1417 | function fromPres( preHtmls, newBlock ) { | ||
1418 | var docFrag; | ||
1419 | if ( preHtmls.length > 1 ) | ||
1420 | docFrag = new CKEDITOR.dom.documentFragment( newBlock.getDocument() ); | ||
1421 | |||
1422 | for ( var i = 0; i < preHtmls.length; i++ ) { | ||
1423 | var blockHtml = preHtmls[ i ]; | ||
1424 | |||
1425 | // 1. Trim the first and last line-breaks immediately after and before <pre>, | ||
1426 | // they're not visible. | ||
1427 | blockHtml = blockHtml.replace( /(\r\n|\r)/g, '\n' ); | ||
1428 | blockHtml = replace( blockHtml, /^[ \t]*\n/, '' ); | ||
1429 | blockHtml = replace( blockHtml, /\n$/, '' ); | ||
1430 | // 2. Convert spaces or tabs at the beginning or at the end to | ||
1431 | blockHtml = replace( blockHtml, /^[ \t]+|[ \t]+$/g, function( match, offset ) { | ||
1432 | if ( match.length == 1 ) // one space, preserve it | ||
1433 | return ' '; | ||
1434 | else if ( !offset ) // beginning of block | ||
1435 | return CKEDITOR.tools.repeat( ' ', match.length - 1 ) + ' '; | ||
1436 | else // end of block | ||
1437 | return ' ' + CKEDITOR.tools.repeat( ' ', match.length - 1 ); | ||
1438 | } ); | ||
1439 | |||
1440 | // 3. Convert \n to <BR>. | ||
1441 | // 4. Convert contiguous (i.e. non-singular) spaces or tabs to | ||
1442 | blockHtml = blockHtml.replace( /\n/g, '<br>' ); | ||
1443 | blockHtml = blockHtml.replace( /[ \t]{2,}/g, function( match ) { | ||
1444 | return CKEDITOR.tools.repeat( ' ', match.length - 1 ) + ' '; | ||
1445 | } ); | ||
1446 | |||
1447 | if ( docFrag ) { | ||
1448 | var newBlockClone = newBlock.clone(); | ||
1449 | newBlockClone.setHtml( blockHtml ); | ||
1450 | docFrag.append( newBlockClone ); | ||
1451 | } else { | ||
1452 | newBlock.setHtml( blockHtml ); | ||
1453 | } | ||
1454 | } | ||
1455 | |||
1456 | return docFrag || newBlock; | ||
1457 | } | ||
1458 | |||
1459 | // Converting from a non-PRE block to a PRE block in formatting operations. | ||
1460 | function toPre( block, newBlock ) { | ||
1461 | var bogus = block.getBogus(); | ||
1462 | bogus && bogus.remove(); | ||
1463 | |||
1464 | // First trim the block content. | ||
1465 | var preHtml = block.getHtml(); | ||
1466 | |||
1467 | // 1. Trim head/tail spaces, they're not visible. | ||
1468 | preHtml = replace( preHtml, /(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, '' ); | ||
1469 | // 2. Delete ANSI whitespaces immediately before and after <BR> because | ||
1470 | // they are not visible. | ||
1471 | preHtml = preHtml.replace( /[ \t\r\n]*(<br[^>]*>)[ \t\r\n]*/gi, '$1' ); | ||
1472 | // 3. Compress other ANSI whitespaces since they're only visible as one | ||
1473 | // single space previously. | ||
1474 | // 4. Convert to spaces since is no longer needed in <PRE>. | ||
1475 | preHtml = preHtml.replace( /([ \t\n\r]+| )/g, ' ' ); | ||
1476 | // 5. Convert any <BR /> to \n. This must not be done earlier because | ||
1477 | // the \n would then get compressed. | ||
1478 | preHtml = preHtml.replace( /<br\b[^>]*>/gi, '\n' ); | ||
1479 | |||
1480 | // Krugle: IE normalizes innerHTML to <pre>, breaking whitespaces. | ||
1481 | if ( CKEDITOR.env.ie ) { | ||
1482 | var temp = block.getDocument().createElement( 'div' ); | ||
1483 | temp.append( newBlock ); | ||
1484 | newBlock.$.outerHTML = '<pre>' + preHtml + '</pre>'; | ||
1485 | newBlock.copyAttributes( temp.getFirst() ); | ||
1486 | newBlock = temp.getFirst().remove(); | ||
1487 | } else { | ||
1488 | newBlock.setHtml( preHtml ); | ||
1489 | } | ||
1490 | |||
1491 | return newBlock; | ||
1492 | } | ||
1493 | |||
1494 | // Removes a style from an element itself, don't care about its subtree. | ||
1495 | function removeFromElement( element, keepDataAttrs ) { | ||
1496 | var def = this._.definition, | ||
1497 | attributes = def.attributes, | ||
1498 | styles = def.styles, | ||
1499 | overrides = getOverrides( this )[ element.getName() ], | ||
1500 | // If the style is only about the element itself, we have to remove the element. | ||
1501 | removeEmpty = CKEDITOR.tools.isEmpty( attributes ) && CKEDITOR.tools.isEmpty( styles ); | ||
1502 | |||
1503 | // Remove definition attributes/style from the elemnt. | ||
1504 | for ( var attName in attributes ) { | ||
1505 | // The 'class' element value must match (#1318). | ||
1506 | if ( ( attName == 'class' || this._.definition.fullMatch ) && element.getAttribute( attName ) != normalizeProperty( attName, attributes[ attName ] ) ) | ||
1507 | continue; | ||
1508 | |||
1509 | // Do not touch data-* attributes (#11011) (#11258). | ||
1510 | if ( keepDataAttrs && attName.slice( 0, 5 ) == 'data-' ) | ||
1511 | continue; | ||
1512 | |||
1513 | removeEmpty = element.hasAttribute( attName ); | ||
1514 | element.removeAttribute( attName ); | ||
1515 | } | ||
1516 | |||
1517 | for ( var styleName in styles ) { | ||
1518 | // Full match style insist on having fully equivalence. (#5018) | ||
1519 | if ( this._.definition.fullMatch && element.getStyle( styleName ) != normalizeProperty( styleName, styles[ styleName ], true ) ) | ||
1520 | continue; | ||
1521 | |||
1522 | removeEmpty = removeEmpty || !!element.getStyle( styleName ); | ||
1523 | element.removeStyle( styleName ); | ||
1524 | } | ||
1525 | |||
1526 | // Remove overrides, but don't remove the element if it's a block element | ||
1527 | removeOverrides( element, overrides, blockElements[ element.getName() ] ); | ||
1528 | |||
1529 | if ( removeEmpty ) { | ||
1530 | if ( this._.definition.alwaysRemoveElement ) | ||
1531 | removeNoAttribsElement( element, 1 ); | ||
1532 | else { | ||
1533 | if ( !CKEDITOR.dtd.$block[ element.getName() ] || this._.enterMode == CKEDITOR.ENTER_BR && !element.hasAttributes() ) | ||
1534 | removeNoAttribsElement( element ); | ||
1535 | else | ||
1536 | element.renameNode( this._.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ); | ||
1537 | } | ||
1538 | } | ||
1539 | } | ||
1540 | |||
1541 | // Removes a style from inside an element. Called on applyStyle to make cleanup | ||
1542 | // before apply. During clean up this function keep data-* attribute in contrast | ||
1543 | // to removeFromElement. | ||
1544 | function removeFromInsideElement( element ) { | ||
1545 | var overrides = getOverrides( this ), | ||
1546 | innerElements = element.getElementsByTag( this.element ), | ||
1547 | innerElement; | ||
1548 | |||
1549 | for ( var i = innerElements.count(); --i >= 0; ) { | ||
1550 | innerElement = innerElements.getItem( i ); | ||
1551 | |||
1552 | // Do not remove elements which are read only (e.g. duplicates inside widgets). | ||
1553 | if ( !innerElement.isReadOnly() ) | ||
1554 | removeFromElement.call( this, innerElement, true ); | ||
1555 | } | ||
1556 | |||
1557 | // Now remove any other element with different name that is | ||
1558 | // defined to be overriden. | ||
1559 | for ( var overrideElement in overrides ) { | ||
1560 | if ( overrideElement != this.element ) { | ||
1561 | innerElements = element.getElementsByTag( overrideElement ); | ||
1562 | |||
1563 | for ( i = innerElements.count() - 1; i >= 0; i-- ) { | ||
1564 | innerElement = innerElements.getItem( i ); | ||
1565 | |||
1566 | // Do not remove elements which are read only (e.g. duplicates inside widgets). | ||
1567 | if ( !innerElement.isReadOnly() ) | ||
1568 | removeOverrides( innerElement, overrides[ overrideElement ] ); | ||
1569 | } | ||
1570 | } | ||
1571 | } | ||
1572 | } | ||
1573 | |||
1574 | // Remove overriding styles/attributes from the specific element. | ||
1575 | // Note: Remove the element if no attributes remain. | ||
1576 | // @param {Object} element | ||
1577 | // @param {Object} overrides | ||
1578 | // @param {Boolean} Don't remove the element | ||
1579 | function removeOverrides( element, overrides, dontRemove ) { | ||
1580 | var attributes = overrides && overrides.attributes; | ||
1581 | |||
1582 | if ( attributes ) { | ||
1583 | for ( var i = 0; i < attributes.length; i++ ) { | ||
1584 | var attName = attributes[ i ][ 0 ], | ||
1585 | actualAttrValue; | ||
1586 | |||
1587 | if ( ( actualAttrValue = element.getAttribute( attName ) ) ) { | ||
1588 | var attValue = attributes[ i ][ 1 ]; | ||
1589 | |||
1590 | // Remove the attribute if: | ||
1591 | // - The override definition value is null ; | ||
1592 | // - The override definition valie is a string that | ||
1593 | // matches the attribute value exactly. | ||
1594 | // - The override definition value is a regex that | ||
1595 | // has matches in the attribute value. | ||
1596 | if ( attValue === null || ( attValue.test && attValue.test( actualAttrValue ) ) || ( typeof attValue == 'string' && actualAttrValue == attValue ) ) | ||
1597 | element.removeAttribute( attName ); | ||
1598 | } | ||
1599 | } | ||
1600 | } | ||
1601 | |||
1602 | if ( !dontRemove ) | ||
1603 | removeNoAttribsElement( element ); | ||
1604 | } | ||
1605 | |||
1606 | // If the element has no more attributes, remove it. | ||
1607 | function removeNoAttribsElement( element, forceRemove ) { | ||
1608 | // If no more attributes remained in the element, remove it, | ||
1609 | // leaving its children. | ||
1610 | if ( !element.hasAttributes() || forceRemove ) { | ||
1611 | if ( CKEDITOR.dtd.$block[ element.getName() ] ) { | ||
1612 | var previous = element.getPrevious( nonWhitespaces ), | ||
1613 | next = element.getNext( nonWhitespaces ); | ||
1614 | |||
1615 | if ( previous && ( previous.type == CKEDITOR.NODE_TEXT || !previous.isBlockBoundary( { br: 1 } ) ) ) | ||
1616 | element.append( 'br', 1 ); | ||
1617 | if ( next && ( next.type == CKEDITOR.NODE_TEXT || !next.isBlockBoundary( { br: 1 } ) ) ) | ||
1618 | element.append( 'br' ); | ||
1619 | |||
1620 | element.remove( true ); | ||
1621 | } else { | ||
1622 | // Removing elements may open points where merging is possible, | ||
1623 | // so let's cache the first and last nodes for later checking. | ||
1624 | var firstChild = element.getFirst(); | ||
1625 | var lastChild = element.getLast(); | ||
1626 | |||
1627 | element.remove( true ); | ||
1628 | |||
1629 | if ( firstChild ) { | ||
1630 | // Check the cached nodes for merging. | ||
1631 | firstChild.type == CKEDITOR.NODE_ELEMENT && firstChild.mergeSiblings(); | ||
1632 | |||
1633 | if ( lastChild && !firstChild.equals( lastChild ) && lastChild.type == CKEDITOR.NODE_ELEMENT ) | ||
1634 | lastChild.mergeSiblings(); | ||
1635 | } | ||
1636 | |||
1637 | } | ||
1638 | } | ||
1639 | } | ||
1640 | |||
1641 | function getElement( style, targetDocument, element ) { | ||
1642 | var el, | ||
1643 | elementName = style.element; | ||
1644 | |||
1645 | // The "*" element name will always be a span for this function. | ||
1646 | if ( elementName == '*' ) | ||
1647 | elementName = 'span'; | ||
1648 | |||
1649 | // Create the element. | ||
1650 | el = new CKEDITOR.dom.element( elementName, targetDocument ); | ||
1651 | |||
1652 | // #6226: attributes should be copied before the new ones are applied | ||
1653 | if ( element ) | ||
1654 | element.copyAttributes( el ); | ||
1655 | |||
1656 | el = setupElement( el, style ); | ||
1657 | |||
1658 | // Avoid ID duplication. | ||
1659 | if ( targetDocument.getCustomData( 'doc_processing_style' ) && el.hasAttribute( 'id' ) ) | ||
1660 | el.removeAttribute( 'id' ); | ||
1661 | else | ||
1662 | targetDocument.setCustomData( 'doc_processing_style', 1 ); | ||
1663 | |||
1664 | return el; | ||
1665 | } | ||
1666 | |||
1667 | function setupElement( el, style ) { | ||
1668 | var def = style._.definition, | ||
1669 | attributes = def.attributes, | ||
1670 | styles = CKEDITOR.style.getStyleText( def ); | ||
1671 | |||
1672 | // Assign all defined attributes. | ||
1673 | if ( attributes ) { | ||
1674 | for ( var att in attributes ) | ||
1675 | el.setAttribute( att, attributes[ att ] ); | ||
1676 | } | ||
1677 | |||
1678 | // Assign all defined styles. | ||
1679 | if ( styles ) | ||
1680 | el.setAttribute( 'style', styles ); | ||
1681 | |||
1682 | return el; | ||
1683 | } | ||
1684 | |||
1685 | function replaceVariables( list, variablesValues ) { | ||
1686 | for ( var item in list ) { | ||
1687 | list[ item ] = list[ item ].replace( varRegex, function( match, varName ) { | ||
1688 | return variablesValues[ varName ]; | ||
1689 | } ); | ||
1690 | } | ||
1691 | } | ||
1692 | |||
1693 | // Returns an object that can be used for style matching comparison. | ||
1694 | // Attributes names and values are all lowercased, and the styles get | ||
1695 | // merged with the style attribute. | ||
1696 | function getAttributesForComparison( styleDefinition ) { | ||
1697 | // If we have already computed it, just return it. | ||
1698 | var attribs = styleDefinition._AC; | ||
1699 | if ( attribs ) | ||
1700 | return attribs; | ||
1701 | |||
1702 | attribs = {}; | ||
1703 | |||
1704 | var length = 0; | ||
1705 | |||
1706 | // Loop through all defined attributes. | ||
1707 | var styleAttribs = styleDefinition.attributes; | ||
1708 | if ( styleAttribs ) { | ||
1709 | for ( var styleAtt in styleAttribs ) { | ||
1710 | length++; | ||
1711 | attribs[ styleAtt ] = styleAttribs[ styleAtt ]; | ||
1712 | } | ||
1713 | } | ||
1714 | |||
1715 | // Includes the style definitions. | ||
1716 | var styleText = CKEDITOR.style.getStyleText( styleDefinition ); | ||
1717 | if ( styleText ) { | ||
1718 | if ( !attribs.style ) | ||
1719 | length++; | ||
1720 | attribs.style = styleText; | ||
1721 | } | ||
1722 | |||
1723 | // Appends the "length" information to the object. | ||
1724 | attribs._length = length; | ||
1725 | |||
1726 | // Return it, saving it to the next request. | ||
1727 | return ( styleDefinition._AC = attribs ); | ||
1728 | } | ||
1729 | |||
1730 | // Get the the collection used to compare the elements and attributes, | ||
1731 | // defined in this style overrides, with other element. All information in | ||
1732 | // it is lowercased. | ||
1733 | // @param {CKEDITOR.style} style | ||
1734 | function getOverrides( style ) { | ||
1735 | if ( style._.overrides ) | ||
1736 | return style._.overrides; | ||
1737 | |||
1738 | var overrides = ( style._.overrides = {} ), | ||
1739 | definition = style._.definition.overrides; | ||
1740 | |||
1741 | if ( definition ) { | ||
1742 | // The override description can be a string, object or array. | ||
1743 | // Internally, well handle arrays only, so transform it if needed. | ||
1744 | if ( !CKEDITOR.tools.isArray( definition ) ) | ||
1745 | definition = [ definition ]; | ||
1746 | |||
1747 | // Loop through all override definitions. | ||
1748 | for ( var i = 0; i < definition.length; i++ ) { | ||
1749 | var override = definition[ i ], | ||
1750 | elementName, | ||
1751 | overrideEl, | ||
1752 | attrs; | ||
1753 | |||
1754 | // If can be a string with the element name. | ||
1755 | if ( typeof override == 'string' ) | ||
1756 | elementName = override.toLowerCase(); | ||
1757 | // Or an object. | ||
1758 | else { | ||
1759 | elementName = override.element ? override.element.toLowerCase() : style.element; | ||
1760 | attrs = override.attributes; | ||
1761 | } | ||
1762 | |||
1763 | // We can have more than one override definition for the same | ||
1764 | // element name, so we attempt to simply append information to | ||
1765 | // it if it already exists. | ||
1766 | overrideEl = overrides[ elementName ] || ( overrides[ elementName ] = {} ); | ||
1767 | |||
1768 | if ( attrs ) { | ||
1769 | // The returning attributes list is an array, because we | ||
1770 | // could have different override definitions for the same | ||
1771 | // attribute name. | ||
1772 | var overrideAttrs = ( overrideEl.attributes = overrideEl.attributes || [] ); | ||
1773 | for ( var attName in attrs ) { | ||
1774 | // Each item in the attributes array is also an array, | ||
1775 | // where [0] is the attribute name and [1] is the | ||
1776 | // override value. | ||
1777 | overrideAttrs.push( [ attName.toLowerCase(), attrs[ attName ] ] ); | ||
1778 | } | ||
1779 | } | ||
1780 | } | ||
1781 | } | ||
1782 | |||
1783 | return overrides; | ||
1784 | } | ||
1785 | |||
1786 | // Make the comparison of attribute value easier by standardizing it. | ||
1787 | function normalizeProperty( name, value, isStyle ) { | ||
1788 | var temp = new CKEDITOR.dom.element( 'span' ); | ||
1789 | temp[ isStyle ? 'setStyle' : 'setAttribute' ]( name, value ); | ||
1790 | return temp[ isStyle ? 'getStyle' : 'getAttribute' ]( name ); | ||
1791 | } | ||
1792 | |||
1793 | // Compare two bunch of styles, with the speciality that value 'inherit' | ||
1794 | // is treated as a wildcard which will match any value. | ||
1795 | // @param {Object/String} source | ||
1796 | // @param {Object/String} target | ||
1797 | function compareCssText( source, target ) { | ||
1798 | if ( typeof source == 'string' ) | ||
1799 | source = CKEDITOR.tools.parseCssText( source ); | ||
1800 | if ( typeof target == 'string' ) | ||
1801 | target = CKEDITOR.tools.parseCssText( target, true ); | ||
1802 | |||
1803 | for ( var name in source ) { | ||
1804 | if ( !( name in target && ( target[ name ] == source[ name ] || source[ name ] == 'inherit' || target[ name ] == 'inherit' ) ) ) | ||
1805 | return false; | ||
1806 | } | ||
1807 | return true; | ||
1808 | } | ||
1809 | |||
1810 | function applyStyleOnSelection( selection, remove, editor ) { | ||
1811 | var doc = selection.document, | ||
1812 | ranges = selection.getRanges(), | ||
1813 | func = remove ? this.removeFromRange : this.applyToRange, | ||
1814 | range; | ||
1815 | |||
1816 | var iterator = ranges.createIterator(); | ||
1817 | while ( ( range = iterator.getNextRange() ) ) | ||
1818 | func.call( this, range, editor ); | ||
1819 | |||
1820 | selection.selectRanges( ranges ); | ||
1821 | doc.removeCustomData( 'doc_processing_style' ); | ||
1822 | } | ||
1823 | } )(); | ||
1824 | |||
1825 | /** | ||
1826 | * Generic style command. It applies a specific style when executed. | ||
1827 | * | ||
1828 | * var boldStyle = new CKEDITOR.style( { element: 'strong' } ); | ||
1829 | * // Register the "bold" command, which applies the bold style. | ||
1830 | * editor.addCommand( 'bold', new CKEDITOR.styleCommand( boldStyle ) ); | ||
1831 | * | ||
1832 | * @class | ||
1833 | * @constructor Creates a styleCommand class instance. | ||
1834 | * @extends CKEDITOR.commandDefinition | ||
1835 | * @param {CKEDITOR.style} style The style to be applied when command is executed. | ||
1836 | * @param {Object} [ext] Additional command definition's properties. | ||
1837 | */ | ||
1838 | CKEDITOR.styleCommand = function( style, ext ) { | ||
1839 | this.style = style; | ||
1840 | this.allowedContent = style; | ||
1841 | this.requiredContent = style; | ||
1842 | |||
1843 | CKEDITOR.tools.extend( this, ext, true ); | ||
1844 | }; | ||
1845 | |||
1846 | /** | ||
1847 | * @param {CKEDITOR.editor} editor | ||
1848 | * @todo | ||
1849 | */ | ||
1850 | CKEDITOR.styleCommand.prototype.exec = function( editor ) { | ||
1851 | editor.focus(); | ||
1852 | |||
1853 | if ( this.state == CKEDITOR.TRISTATE_OFF ) | ||
1854 | editor.applyStyle( this.style ); | ||
1855 | else if ( this.state == CKEDITOR.TRISTATE_ON ) | ||
1856 | editor.removeStyle( this.style ); | ||
1857 | }; | ||
1858 | |||
1859 | /** | ||
1860 | * Manages styles registration and loading. See also {@link CKEDITOR.config#stylesSet}. | ||
1861 | * | ||
1862 | * // The set of styles for the <b>Styles</b> drop-down list. | ||
1863 | * CKEDITOR.stylesSet.add( 'default', [ | ||
1864 | * // Block Styles | ||
1865 | * { name: 'Blue Title', element: 'h3', styles: { 'color': 'Blue' } }, | ||
1866 | * { name: 'Red Title', element: 'h3', styles: { 'color': 'Red' } }, | ||
1867 | * | ||
1868 | * // Inline Styles | ||
1869 | * { name: 'Marker: Yellow', element: 'span', styles: { 'background-color': 'Yellow' } }, | ||
1870 | * { name: 'Marker: Green', element: 'span', styles: { 'background-color': 'Lime' } }, | ||
1871 | * | ||
1872 | * // Object Styles | ||
1873 | * { | ||
1874 | * name: 'Image on Left', | ||
1875 | * element: 'img', | ||
1876 | * attributes: { | ||
1877 | * style: 'padding: 5px; margin-right: 5px', | ||
1878 | * border: '2', | ||
1879 | * align: 'left' | ||
1880 | * } | ||
1881 | * } | ||
1882 | * ] ); | ||
1883 | * | ||
1884 | * @since 3.2 | ||
1885 | * @class | ||
1886 | * @singleton | ||
1887 | * @extends CKEDITOR.resourceManager | ||
1888 | */ | ||
1889 | CKEDITOR.stylesSet = new CKEDITOR.resourceManager( '', 'stylesSet' ); | ||
1890 | |||
1891 | // Backward compatibility (#5025). | ||
1892 | CKEDITOR.addStylesSet = CKEDITOR.tools.bind( CKEDITOR.stylesSet.add, CKEDITOR.stylesSet ); | ||
1893 | CKEDITOR.loadStylesSet = function( name, url, callback ) { | ||
1894 | CKEDITOR.stylesSet.addExternal( name, url, '' ); | ||
1895 | CKEDITOR.stylesSet.load( name, callback ); | ||
1896 | }; | ||
1897 | |||
1898 | CKEDITOR.tools.extend( CKEDITOR.editor.prototype, { | ||
1899 | /** | ||
1900 | * Registers a function to be called whenever the selection position changes in the | ||
1901 | * editing area. The current state is passed to the function. The possible | ||
1902 | * states are {@link CKEDITOR#TRISTATE_ON} and {@link CKEDITOR#TRISTATE_OFF}. | ||
1903 | * | ||
1904 | * // Create a style object for the <b> element. | ||
1905 | * var style = new CKEDITOR.style( { element: 'b' } ); | ||
1906 | * var editor = CKEDITOR.instances.editor1; | ||
1907 | * editor.attachStyleStateChange( style, function( state ) { | ||
1908 | * if ( state == CKEDITOR.TRISTATE_ON ) | ||
1909 | * alert( 'The current state for the B element is ON' ); | ||
1910 | * else | ||
1911 | * alert( 'The current state for the B element is OFF' ); | ||
1912 | * } ); | ||
1913 | * | ||
1914 | * @member CKEDITOR.editor | ||
1915 | * @param {CKEDITOR.style} style The style to be watched. | ||
1916 | * @param {Function} callback The function to be called. | ||
1917 | */ | ||
1918 | attachStyleStateChange: function( style, callback ) { | ||
1919 | // Try to get the list of attached callbacks. | ||
1920 | var styleStateChangeCallbacks = this._.styleStateChangeCallbacks; | ||
1921 | |||
1922 | // If it doesn't exist, it means this is the first call. So, let's create | ||
1923 | // all the structure to manage the style checks and the callback calls. | ||
1924 | if ( !styleStateChangeCallbacks ) { | ||
1925 | // Create the callbacks array. | ||
1926 | styleStateChangeCallbacks = this._.styleStateChangeCallbacks = []; | ||
1927 | |||
1928 | // Attach to the selectionChange event, so we can check the styles at | ||
1929 | // that point. | ||
1930 | this.on( 'selectionChange', function( ev ) { | ||
1931 | // Loop throw all registered callbacks. | ||
1932 | for ( var i = 0; i < styleStateChangeCallbacks.length; i++ ) { | ||
1933 | var callback = styleStateChangeCallbacks[ i ]; | ||
1934 | |||
1935 | // Check the current state for the style defined for that callback. | ||
1936 | var currentState = callback.style.checkActive( ev.data.path, this ) ? | ||
1937 | CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF; | ||
1938 | |||
1939 | // Call the callback function, passing the current state to it. | ||
1940 | callback.fn.call( this, currentState ); | ||
1941 | } | ||
1942 | } ); | ||
1943 | } | ||
1944 | |||
1945 | // Save the callback info, so it can be checked on the next occurrence of | ||
1946 | // selectionChange. | ||
1947 | styleStateChangeCallbacks.push( { style: style, fn: callback } ); | ||
1948 | }, | ||
1949 | |||
1950 | /** | ||
1951 | * Applies the style upon the editor's current selection. Shorthand for | ||
1952 | * {@link CKEDITOR.style#apply}. | ||
1953 | * | ||
1954 | * @member CKEDITOR.editor | ||
1955 | * @param {CKEDITOR.style} style | ||
1956 | */ | ||
1957 | applyStyle: function( style ) { | ||
1958 | style.apply( this ); | ||
1959 | }, | ||
1960 | |||
1961 | /** | ||
1962 | * Removes the style from the editor's current selection. Shorthand for | ||
1963 | * {@link CKEDITOR.style#remove}. | ||
1964 | * | ||
1965 | * @member CKEDITOR.editor | ||
1966 | * @param {CKEDITOR.style} style | ||
1967 | */ | ||
1968 | removeStyle: function( style ) { | ||
1969 | style.remove( this ); | ||
1970 | }, | ||
1971 | |||
1972 | /** | ||
1973 | * Gets the current `stylesSet` for this instance. | ||
1974 | * | ||
1975 | * editor.getStylesSet( function( stylesDefinitions ) {} ); | ||
1976 | * | ||
1977 | * See also {@link CKEDITOR.editor#stylesSet} event. | ||
1978 | * | ||
1979 | * @member CKEDITOR.editor | ||
1980 | * @param {Function} callback The function to be called with the styles data. | ||
1981 | */ | ||
1982 | getStylesSet: function( callback ) { | ||
1983 | if ( !this._.stylesDefinitions ) { | ||
1984 | var editor = this, | ||
1985 | // Respect the backwards compatible definition entry | ||
1986 | configStyleSet = editor.config.stylesCombo_stylesSet || editor.config.stylesSet; | ||
1987 | |||
1988 | // The false value means that none styles should be loaded. | ||
1989 | if ( configStyleSet === false ) { | ||
1990 | callback( null ); | ||
1991 | return; | ||
1992 | } | ||
1993 | |||
1994 | // #5352 Allow to define the styles directly in the config object | ||
1995 | if ( configStyleSet instanceof Array ) { | ||
1996 | editor._.stylesDefinitions = configStyleSet; | ||
1997 | callback( configStyleSet ); | ||
1998 | return; | ||
1999 | } | ||
2000 | |||
2001 | // Default value is 'default'. | ||
2002 | if ( !configStyleSet ) | ||
2003 | configStyleSet = 'default'; | ||
2004 | |||
2005 | var partsStylesSet = configStyleSet.split( ':' ), | ||
2006 | styleSetName = partsStylesSet[ 0 ], | ||
2007 | externalPath = partsStylesSet[ 1 ]; | ||
2008 | |||
2009 | CKEDITOR.stylesSet.addExternal( styleSetName, externalPath ? partsStylesSet.slice( 1 ).join( ':' ) : CKEDITOR.getUrl( 'styles.js' ), '' ); | ||
2010 | |||
2011 | CKEDITOR.stylesSet.load( styleSetName, function( stylesSet ) { | ||
2012 | editor._.stylesDefinitions = stylesSet[ styleSetName ]; | ||
2013 | callback( editor._.stylesDefinitions ); | ||
2014 | } ); | ||
2015 | } else { | ||
2016 | callback( this._.stylesDefinitions ); | ||
2017 | } | ||
2018 | } | ||
2019 | } ); | ||
2020 | |||
2021 | /** | ||
2022 | * Indicates that fully selected read-only elements will be included when | ||
2023 | * applying the style (for inline styles only). | ||
2024 | * | ||
2025 | * @since 3.5 | ||
2026 | * @property {Boolean} [includeReadonly=false] | ||
2027 | * @member CKEDITOR.style | ||
2028 | */ | ||
2029 | |||
2030 | /** | ||
2031 | * Indicates that any matches element of this style will be eventually removed | ||
2032 | * when calling {@link CKEDITOR.editor#removeStyle}. | ||
2033 | * | ||
2034 | * @since 4.0 | ||
2035 | * @property {Boolean} [alwaysRemoveElement=false] | ||
2036 | * @member CKEDITOR.style | ||
2037 | */ | ||
2038 | |||
2039 | /** | ||
2040 | * Disables inline styling on read-only elements. | ||
2041 | * | ||
2042 | * @since 3.5 | ||
2043 | * @cfg {Boolean} [disableReadonlyStyling=false] | ||
2044 | * @member CKEDITOR.config | ||
2045 | */ | ||
2046 | |||
2047 | /** | ||
2048 | * The "styles definition set" to use in the editor. They will be used in the | ||
2049 | * styles combo and the style selector of the div container. | ||
2050 | * | ||
2051 | * The styles may be defined in the page containing the editor, or can be | ||
2052 | * loaded on demand from an external file. In the second case, if this setting | ||
2053 | * contains only a name, the `styles.js` file will be loaded from the | ||
2054 | * CKEditor root folder (what ensures backward compatibility with CKEditor 4.0). | ||
2055 | * | ||
2056 | * Otherwise, this setting has the `name:url` syntax, making it | ||
2057 | * possible to set the URL from which the styles file will be loaded. | ||
2058 | * Note that the `name` has to be equal to the name used in | ||
2059 | * {@link CKEDITOR.stylesSet#add} while registering the styles set. | ||
2060 | * | ||
2061 | * **Note**: Since 4.1 it is possible to set `stylesSet` to `false` | ||
2062 | * to prevent loading any styles set. | ||
2063 | * | ||
2064 | * Read more in the [documentation](#!/guide/dev_styles) | ||
2065 | * and see the [SDK sample](http://sdk.ckeditor.com/samples/styles.html). | ||
2066 | * | ||
2067 | * // Do not load any file. The styles set is empty. | ||
2068 | * config.stylesSet = false; | ||
2069 | * | ||
2070 | * // Load the 'mystyles' styles set from the styles.js file. | ||
2071 | * config.stylesSet = 'mystyles'; | ||
2072 | * | ||
2073 | * // Load the 'mystyles' styles set from a relative URL. | ||
2074 | * config.stylesSet = 'mystyles:/editorstyles/styles.js'; | ||
2075 | * | ||
2076 | * // Load the 'mystyles' styles set from a full URL. | ||
2077 | * config.stylesSet = 'mystyles:http://www.example.com/editorstyles/styles.js'; | ||
2078 | * | ||
2079 | * // Load from a list of definitions. | ||
2080 | * config.stylesSet = [ | ||
2081 | * { name: 'Strong Emphasis', element: 'strong' }, | ||
2082 | * { name: 'Emphasis', element: 'em' }, | ||
2083 | * ... | ||
2084 | * ]; | ||
2085 | * | ||
2086 | * @since 3.3 | ||
2087 | * @cfg {String/Array/Boolean} [stylesSet='default'] | ||
2088 | * @member CKEDITOR.config | ||
2089 | */ | ||
diff --git a/sources/core/template.js b/sources/core/template.js new file mode 100644 index 0000000..a3fe55b --- /dev/null +++ b/sources/core/template.js | |||
@@ -0,0 +1,68 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.template} class, which represents | ||
8 | * an UI template for an editor instance. | ||
9 | */ | ||
10 | |||
11 | ( function() { | ||
12 | var cache = {}, | ||
13 | rePlaceholder = /{([^}]+)}/g, | ||
14 | reEscapableChars = /([\\'])/g, | ||
15 | reNewLine = /\n/g, | ||
16 | reCarriageReturn = /\r/g; | ||
17 | |||
18 | /** | ||
19 | * Lightweight template used to build the output string from variables. | ||
20 | * | ||
21 | * // HTML template for presenting a label UI. | ||
22 | * var tpl = new CKEDITOR.template( '<div class="{cls}">{label}</div>' ); | ||
23 | * alert( tpl.output( { cls: 'cke-label', label: 'foo'} ) ); // '<div class="cke-label">foo</div>' | ||
24 | * | ||
25 | * @class | ||
26 | * @constructor Creates a template class instance. | ||
27 | * @param {String} source The template source. | ||
28 | */ | ||
29 | CKEDITOR.template = function( source ) { | ||
30 | // Builds an optimized function body for the output() method, focused on performance. | ||
31 | // For example, if we have this "source": | ||
32 | // '<div style="{style}">{editorName}</div>' | ||
33 | // ... the resulting function body will be (apart from the "buffer" handling): | ||
34 | // return [ '<div style="', data['style'] == undefined ? '{style}' : data['style'], '">', data['editorName'] == undefined ? '{editorName}' : data['editorName'], '</div>' ].join(''); | ||
35 | |||
36 | // Try to read from the cache. | ||
37 | if ( cache[ source ] ) | ||
38 | this.output = cache[ source ]; | ||
39 | else { | ||
40 | var fn = source | ||
41 | // Escape chars like slash "\" or single quote "'". | ||
42 | .replace( reEscapableChars, '\\$1' ) | ||
43 | .replace( reNewLine, '\\n' ) | ||
44 | .replace( reCarriageReturn, '\\r' ) | ||
45 | // Inject the template keys replacement. | ||
46 | .replace( rePlaceholder, function( m, key ) { | ||
47 | return "',data['" + key + "']==undefined?'{" + key + "}':data['" + key + "'],'"; | ||
48 | } ); | ||
49 | |||
50 | fn = "return buffer?buffer.push('" + fn + "'):['" + fn + "'].join('');"; | ||
51 | this.output = cache[ source ] = Function( 'data', 'buffer', fn ); | ||
52 | } | ||
53 | }; | ||
54 | } )(); | ||
55 | |||
56 | /** | ||
57 | * Processes the template, filling its variables with the provided data. | ||
58 | * | ||
59 | * @method output | ||
60 | * @param {Object} data An object containing properties which values will be | ||
61 | * used to fill the template variables. The property names must match the | ||
62 | * template variables names. Variables without matching properties will be | ||
63 | * kept untouched. | ||
64 | * @param {Array} [buffer] An array into which the output data will be pushed into. | ||
65 | * The number of entries appended to the array is unknown. | ||
66 | * @returns {String/Number} If `buffer` has not been provided, the processed | ||
67 | * template output data, otherwise the new length of `buffer`. | ||
68 | */ | ||
diff --git a/sources/core/tools.js b/sources/core/tools.js new file mode 100644 index 0000000..ae5b4d0 --- /dev/null +++ b/sources/core/tools.js | |||
@@ -0,0 +1,1386 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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.tools} object that contains | ||
8 | * utility functions. | ||
9 | */ | ||
10 | |||
11 | ( function() { | ||
12 | var functions = [], | ||
13 | cssVendorPrefix = | ||
14 | CKEDITOR.env.gecko ? '-moz-' : | ||
15 | CKEDITOR.env.webkit ? '-webkit-' : | ||
16 | CKEDITOR.env.ie ? '-ms-' : | ||
17 | '', | ||
18 | ampRegex = /&/g, | ||
19 | gtRegex = />/g, | ||
20 | ltRegex = /</g, | ||
21 | quoteRegex = /"/g, | ||
22 | tokenCharset = 'abcdefghijklmnopqrstuvwxyz0123456789', | ||
23 | TOKEN_COOKIE_NAME = 'ckCsrfToken', | ||
24 | TOKEN_LENGTH = 40, | ||
25 | |||
26 | allEscRegex = /&(lt|gt|amp|quot|nbsp|shy|#\d{1,5});/g, | ||
27 | namedEntities = { | ||
28 | lt: '<', | ||
29 | gt: '>', | ||
30 | amp: '&', | ||
31 | quot: '"', | ||
32 | nbsp: '\u00a0', | ||
33 | shy: '\u00ad' | ||
34 | }, | ||
35 | allEscDecode = function( match, code ) { | ||
36 | if ( code[ 0 ] == '#' ) { | ||
37 | return String.fromCharCode( parseInt( code.slice( 1 ), 10 ) ); | ||
38 | } else { | ||
39 | return namedEntities[ code ]; | ||
40 | } | ||
41 | }; | ||
42 | |||
43 | CKEDITOR.on( 'reset', function() { | ||
44 | functions = []; | ||
45 | } ); | ||
46 | |||
47 | /** | ||
48 | * Utility functions. | ||
49 | * | ||
50 | * @class | ||
51 | * @singleton | ||
52 | */ | ||
53 | CKEDITOR.tools = { | ||
54 | /** | ||
55 | * Compares the elements of two arrays. | ||
56 | * | ||
57 | * var a = [ 1, 'a', 3 ]; | ||
58 | * var b = [ 1, 3, 'a' ]; | ||
59 | * var c = [ 1, 'a', 3 ]; | ||
60 | * var d = [ 1, 'a', 3, 4 ]; | ||
61 | * | ||
62 | * alert( CKEDITOR.tools.arrayCompare( a, b ) ); // false | ||
63 | * alert( CKEDITOR.tools.arrayCompare( a, c ) ); // true | ||
64 | * alert( CKEDITOR.tools.arrayCompare( a, d ) ); // false | ||
65 | * | ||
66 | * @param {Array} arrayA An array to be compared. | ||
67 | * @param {Array} arrayB The other array to be compared. | ||
68 | * @returns {Boolean} `true` if the arrays have the same length and | ||
69 | * their elements match. | ||
70 | */ | ||
71 | arrayCompare: function( arrayA, arrayB ) { | ||
72 | if ( !arrayA && !arrayB ) | ||
73 | return true; | ||
74 | |||
75 | if ( !arrayA || !arrayB || arrayA.length != arrayB.length ) | ||
76 | return false; | ||
77 | |||
78 | for ( var i = 0; i < arrayA.length; i++ ) { | ||
79 | if ( arrayA[ i ] != arrayB[ i ] ) | ||
80 | return false; | ||
81 | } | ||
82 | |||
83 | return true; | ||
84 | }, | ||
85 | |||
86 | /** | ||
87 | * Finds the index of the first element in an array for which the `compareFunction` returns `true`. | ||
88 | * | ||
89 | * CKEDITOR.tools.getIndex( [ 1, 2, 4, 3, 5 ], function( el ) { | ||
90 | * return el >= 3; | ||
91 | * } ); // 2 | ||
92 | * | ||
93 | * @since 4.5 | ||
94 | * @param {Array} array Array to search in. | ||
95 | * @param {Function} compareFunction Compare function. | ||
96 | * @returns {Number} The index of the first matching element or `-1` if none matches. | ||
97 | */ | ||
98 | getIndex: function( arr, compareFunction ) { | ||
99 | for ( var i = 0; i < arr.length; ++i ) { | ||
100 | if ( compareFunction( arr[ i ] ) ) | ||
101 | return i; | ||
102 | } | ||
103 | return -1; | ||
104 | }, | ||
105 | |||
106 | /** | ||
107 | * Creates a deep copy of an object. | ||
108 | * | ||
109 | * **Note**: Recursive references are not supported. | ||
110 | * | ||
111 | * var obj = { | ||
112 | * name: 'John', | ||
113 | * cars: { | ||
114 | * Mercedes: { color: 'blue' }, | ||
115 | * Porsche: { color: 'red' } | ||
116 | * } | ||
117 | * }; | ||
118 | * var clone = CKEDITOR.tools.clone( obj ); | ||
119 | * clone.name = 'Paul'; | ||
120 | * clone.cars.Porsche.color = 'silver'; | ||
121 | * | ||
122 | * alert( obj.name ); // 'John' | ||
123 | * alert( clone.name ); // 'Paul' | ||
124 | * alert( obj.cars.Porsche.color ); // 'red' | ||
125 | * alert( clone.cars.Porsche.color ); // 'silver' | ||
126 | * | ||
127 | * @param {Object} object The object to be cloned. | ||
128 | * @returns {Object} The object clone. | ||
129 | */ | ||
130 | clone: function( obj ) { | ||
131 | var clone; | ||
132 | |||
133 | // Array. | ||
134 | if ( obj && ( obj instanceof Array ) ) { | ||
135 | clone = []; | ||
136 | |||
137 | for ( var i = 0; i < obj.length; i++ ) | ||
138 | clone[ i ] = CKEDITOR.tools.clone( obj[ i ] ); | ||
139 | |||
140 | return clone; | ||
141 | } | ||
142 | |||
143 | // "Static" types. | ||
144 | if ( obj === null || ( typeof obj != 'object' ) || ( obj instanceof String ) || ( obj instanceof Number ) || ( obj instanceof Boolean ) || ( obj instanceof Date ) || ( obj instanceof RegExp ) ) | ||
145 | return obj; | ||
146 | |||
147 | // DOM objects and window. | ||
148 | if ( obj.nodeType || obj.window === obj ) | ||
149 | return obj; | ||
150 | |||
151 | // Objects. | ||
152 | clone = new obj.constructor(); | ||
153 | |||
154 | for ( var propertyName in obj ) { | ||
155 | var property = obj[ propertyName ]; | ||
156 | clone[ propertyName ] = CKEDITOR.tools.clone( property ); | ||
157 | } | ||
158 | |||
159 | return clone; | ||
160 | }, | ||
161 | |||
162 | /** | ||
163 | * Turns the first letter of a string to upper-case. | ||
164 | * | ||
165 | * @param {String} str | ||
166 | * @param {Boolean} [keepCase] Keep the case of 2nd to last letter. | ||
167 | * @returns {String} | ||
168 | */ | ||
169 | capitalize: function( str, keepCase ) { | ||
170 | return str.charAt( 0 ).toUpperCase() + ( keepCase ? str.slice( 1 ) : str.slice( 1 ).toLowerCase() ); | ||
171 | }, | ||
172 | |||
173 | /** | ||
174 | * Copies the properties from one object to another. By default, properties | ||
175 | * already present in the target object **are not** overwritten. | ||
176 | * | ||
177 | * // Create the sample object. | ||
178 | * var myObject = { | ||
179 | * prop1: true | ||
180 | * }; | ||
181 | * | ||
182 | * // Extend the above object with two properties. | ||
183 | * CKEDITOR.tools.extend( myObject, { | ||
184 | * prop2: true, | ||
185 | * prop3: true | ||
186 | * } ); | ||
187 | * | ||
188 | * // Alert 'prop1', 'prop2' and 'prop3'. | ||
189 | * for ( var p in myObject ) | ||
190 | * alert( p ); | ||
191 | * | ||
192 | * @param {Object} target The object to be extended. | ||
193 | * @param {Object...} source The object(s) from properties will be | ||
194 | * copied. Any number of objects can be passed to this function. | ||
195 | * @param {Boolean} [overwrite] If `true` is specified, it indicates that | ||
196 | * properties already present in the target object could be | ||
197 | * overwritten by subsequent objects. | ||
198 | * @param {Object} [properties] Only properties within the specified names | ||
199 | * list will be received from the source object. | ||
200 | * @returns {Object} The extended object (target). | ||
201 | */ | ||
202 | extend: function( target ) { | ||
203 | var argsLength = arguments.length, | ||
204 | overwrite, propertiesList; | ||
205 | |||
206 | if ( typeof ( overwrite = arguments[ argsLength - 1 ] ) == 'boolean' ) | ||
207 | argsLength--; | ||
208 | else if ( typeof ( overwrite = arguments[ argsLength - 2 ] ) == 'boolean' ) { | ||
209 | propertiesList = arguments[ argsLength - 1 ]; | ||
210 | argsLength -= 2; | ||
211 | } | ||
212 | for ( var i = 1; i < argsLength; i++ ) { | ||
213 | var source = arguments[ i ]; | ||
214 | for ( var propertyName in source ) { | ||
215 | // Only copy existed fields if in overwrite mode. | ||
216 | if ( overwrite === true || target[ propertyName ] == null ) { | ||
217 | // Only copy specified fields if list is provided. | ||
218 | if ( !propertiesList || ( propertyName in propertiesList ) ) | ||
219 | target[ propertyName ] = source[ propertyName ]; | ||
220 | |||
221 | } | ||
222 | } | ||
223 | } | ||
224 | |||
225 | return target; | ||
226 | }, | ||
227 | |||
228 | /** | ||
229 | * Creates an object which is an instance of a class whose prototype is a | ||
230 | * predefined object. All properties defined in the source object are | ||
231 | * automatically inherited by the resulting object, including future | ||
232 | * changes to it. | ||
233 | * | ||
234 | * @param {Object} source The source object to be used as the prototype for | ||
235 | * the final object. | ||
236 | * @returns {Object} The resulting copy. | ||
237 | */ | ||
238 | prototypedCopy: function( source ) { | ||
239 | var copy = function() {}; | ||
240 | copy.prototype = source; | ||
241 | return new copy(); | ||
242 | }, | ||
243 | |||
244 | /** | ||
245 | * Makes fast (shallow) copy of an object. | ||
246 | * This method is faster than {@link #clone} which does | ||
247 | * a deep copy of an object (including arrays). | ||
248 | * | ||
249 | * @since 4.1 | ||
250 | * @param {Object} source The object to be copied. | ||
251 | * @returns {Object} Copy of `source`. | ||
252 | */ | ||
253 | copy: function( source ) { | ||
254 | var obj = {}, | ||
255 | name; | ||
256 | |||
257 | for ( name in source ) | ||
258 | obj[ name ] = source[ name ]; | ||
259 | |||
260 | return obj; | ||
261 | }, | ||
262 | |||
263 | /** | ||
264 | * Checks if an object is an Array. | ||
265 | * | ||
266 | * alert( CKEDITOR.tools.isArray( [] ) ); // true | ||
267 | * alert( CKEDITOR.tools.isArray( 'Test' ) ); // false | ||
268 | * | ||
269 | * @param {Object} object The object to be checked. | ||
270 | * @returns {Boolean} `true` if the object is an Array, otherwise `false`. | ||
271 | */ | ||
272 | isArray: function( object ) { | ||
273 | return Object.prototype.toString.call( object ) == '[object Array]'; | ||
274 | }, | ||
275 | |||
276 | /** | ||
277 | * Whether the object contains no properties of its own. | ||
278 | * | ||
279 | * @param object | ||
280 | * @returns {Boolean} | ||
281 | */ | ||
282 | isEmpty: function( object ) { | ||
283 | for ( var i in object ) { | ||
284 | if ( object.hasOwnProperty( i ) ) | ||
285 | return false; | ||
286 | } | ||
287 | return true; | ||
288 | }, | ||
289 | |||
290 | /** | ||
291 | * Generates an object or a string containing vendor-specific and vendor-free CSS properties. | ||
292 | * | ||
293 | * CKEDITOR.tools.cssVendorPrefix( 'border-radius', '0', true ); | ||
294 | * // On Firefox: '-moz-border-radius:0;border-radius:0' | ||
295 | * // On Chrome: '-webkit-border-radius:0;border-radius:0' | ||
296 | * | ||
297 | * @param {String} property The CSS property name. | ||
298 | * @param {String} value The CSS value. | ||
299 | * @param {Boolean} [asString=false] If `true`, then the returned value will be a CSS string. | ||
300 | * @returns {Object/String} The object containing CSS properties or its stringified version. | ||
301 | */ | ||
302 | cssVendorPrefix: function( property, value, asString ) { | ||
303 | if ( asString ) | ||
304 | return cssVendorPrefix + property + ':' + value + ';' + property + ':' + value; | ||
305 | |||
306 | var ret = {}; | ||
307 | ret[ property ] = value; | ||
308 | ret[ cssVendorPrefix + property ] = value; | ||
309 | |||
310 | return ret; | ||
311 | }, | ||
312 | |||
313 | /** | ||
314 | * Transforms a CSS property name to its relative DOM style name. | ||
315 | * | ||
316 | * alert( CKEDITOR.tools.cssStyleToDomStyle( 'background-color' ) ); // 'backgroundColor' | ||
317 | * alert( CKEDITOR.tools.cssStyleToDomStyle( 'float' ) ); // 'cssFloat' | ||
318 | * | ||
319 | * @method | ||
320 | * @param {String} cssName The CSS property name. | ||
321 | * @returns {String} The transformed name. | ||
322 | */ | ||
323 | cssStyleToDomStyle: ( function() { | ||
324 | var test = document.createElement( 'div' ).style; | ||
325 | |||
326 | var cssFloat = ( typeof test.cssFloat != 'undefined' ) ? 'cssFloat' : ( typeof test.styleFloat != 'undefined' ) ? 'styleFloat' : 'float'; | ||
327 | |||
328 | return function( cssName ) { | ||
329 | if ( cssName == 'float' ) | ||
330 | return cssFloat; | ||
331 | else { | ||
332 | return cssName.replace( /-./g, function( match ) { | ||
333 | return match.substr( 1 ).toUpperCase(); | ||
334 | } ); | ||
335 | } | ||
336 | }; | ||
337 | } )(), | ||
338 | |||
339 | /** | ||
340 | * Builds a HTML snippet from a set of `<style>/<link>`. | ||
341 | * | ||
342 | * @param {String/Array} css Each of which are URLs (absolute) of a CSS file or | ||
343 | * a trunk of style text. | ||
344 | * @returns {String} | ||
345 | */ | ||
346 | buildStyleHtml: function( css ) { | ||
347 | css = [].concat( css ); | ||
348 | var item, | ||
349 | retval = []; | ||
350 | for ( var i = 0; i < css.length; i++ ) { | ||
351 | if ( ( item = css[ i ] ) ) { | ||
352 | // Is CSS style text ? | ||
353 | if ( /@import|[{}]/.test( item ) ) | ||
354 | retval.push( '<style>' + item + '</style>' ); | ||
355 | else | ||
356 | retval.push( '<link type="text/css" rel=stylesheet href="' + item + '">' ); | ||
357 | } | ||
358 | } | ||
359 | return retval.join( '' ); | ||
360 | }, | ||
361 | |||
362 | /** | ||
363 | * Replaces special HTML characters in a string with their relative HTML | ||
364 | * entity values. | ||
365 | * | ||
366 | * alert( CKEDITOR.tools.htmlEncode( 'A > B & C < D' ) ); // 'A > B & C < D' | ||
367 | * | ||
368 | * @param {String} text The string to be encoded. | ||
369 | * @returns {String} The encoded string. | ||
370 | */ | ||
371 | htmlEncode: function( text ) { | ||
372 | // Backwards compatibility - accept also non-string values (casting is done below). | ||
373 | // Since 4.4.8 we return empty string for null and undefined because these values make no sense. | ||
374 | if ( text === undefined || text === null ) { | ||
375 | return ''; | ||
376 | } | ||
377 | |||
378 | return String( text ).replace( ampRegex, '&' ).replace( gtRegex, '>' ).replace( ltRegex, '<' ); | ||
379 | }, | ||
380 | |||
381 | /** | ||
382 | * Decodes HTML entities that browsers tend to encode when used in text nodes. | ||
383 | * | ||
384 | * alert( CKEDITOR.tools.htmlDecode( '<a & b >' ) ); // '<a & b >' | ||
385 | * | ||
386 | * Read more about chosen entities in the [research](http://dev.ckeditor.com/ticket/13105#comment:8). | ||
387 | * | ||
388 | * @param {String} The string to be decoded. | ||
389 | * @returns {String} The decoded string. | ||
390 | */ | ||
391 | htmlDecode: function( text ) { | ||
392 | // See: | ||
393 | // * http://dev.ckeditor.com/ticket/13105#comment:8 and comment:9, | ||
394 | // * http://jsperf.com/wth-is-going-on-with-jsperf JSPerf has some serious problems, but you can observe | ||
395 | // that combined regexp tends to be quicker (except on V8). It will also not be prone to fail on '&lt;' | ||
396 | // (see http://dev.ckeditor.com/ticket/13105#DXWTF:CKEDITOR.tools.htmlEnDecodeAttr). | ||
397 | return text.replace( allEscRegex, allEscDecode ); | ||
398 | }, | ||
399 | |||
400 | /** | ||
401 | * Replaces special HTML characters in HTMLElement attribute with their relative HTML entity values. | ||
402 | * | ||
403 | * alert( CKEDITOR.tools.htmlEncodeAttr( '<a " b >' ) ); // '<a " b >' | ||
404 | * | ||
405 | * @param {String} The attribute value to be encoded. | ||
406 | * @returns {String} The encoded value. | ||
407 | */ | ||
408 | htmlEncodeAttr: function( text ) { | ||
409 | return CKEDITOR.tools.htmlEncode( text ).replace( quoteRegex, '"' ); | ||
410 | }, | ||
411 | |||
412 | /** | ||
413 | * Decodes HTML entities that browsers tend to encode when used in attributes. | ||
414 | * | ||
415 | * alert( CKEDITOR.tools.htmlDecodeAttr( '<a " b>' ) ); // '<a " b>' | ||
416 | * | ||
417 | * Since CKEditor 4.5 this method simply executes {@link #htmlDecode} which covers | ||
418 | * all necessary entities. | ||
419 | * | ||
420 | * @param {String} text The text to be decoded. | ||
421 | * @returns {String} The decoded text. | ||
422 | */ | ||
423 | htmlDecodeAttr: function( text ) { | ||
424 | return CKEDITOR.tools.htmlDecode( text ); | ||
425 | }, | ||
426 | |||
427 | /** | ||
428 | * Transforms text to valid HTML: creates paragraphs, replaces tabs with non-breaking spaces etc. | ||
429 | * | ||
430 | * @since 4.5 | ||
431 | * @param {String} text Text to transform. | ||
432 | * @param {Number} enterMode Editor {@link CKEDITOR.config#enterMode Enter mode}. | ||
433 | * @returns {String} HTML generated from the text. | ||
434 | */ | ||
435 | transformPlainTextToHtml: function( text, enterMode ) { | ||
436 | var isEnterBrMode = enterMode == CKEDITOR.ENTER_BR, | ||
437 | // CRLF -> LF | ||
438 | html = this.htmlEncode( text.replace( /\r\n/g, '\n' ) ); | ||
439 | |||
440 | // Tab ->   x 4; | ||
441 | html = html.replace( /\t/g, ' ' ); | ||
442 | |||
443 | var paragraphTag = enterMode == CKEDITOR.ENTER_P ? 'p' : 'div'; | ||
444 | |||
445 | // Two line-breaks create one paragraphing block. | ||
446 | if ( !isEnterBrMode ) { | ||
447 | var duoLF = /\n{2}/g; | ||
448 | if ( duoLF.test( html ) ) { | ||
449 | var openTag = '<' + paragraphTag + '>', endTag = '</' + paragraphTag + '>'; | ||
450 | html = openTag + html.replace( duoLF, function() { | ||
451 | return endTag + openTag; | ||
452 | } ) + endTag; | ||
453 | } | ||
454 | } | ||
455 | |||
456 | // One <br> per line-break. | ||
457 | html = html.replace( /\n/g, '<br>' ); | ||
458 | |||
459 | // Compensate padding <br> at the end of block, avoid loosing them during insertion. | ||
460 | if ( !isEnterBrMode ) { | ||
461 | html = html.replace( new RegExp( '<br>(?=</' + paragraphTag + '>)' ), function( match ) { | ||
462 | return CKEDITOR.tools.repeat( match, 2 ); | ||
463 | } ); | ||
464 | } | ||
465 | |||
466 | // Preserve spaces at the ends, so they won't be lost after insertion (merged with adjacent ones). | ||
467 | html = html.replace( /^ | $/g, ' ' ); | ||
468 | |||
469 | // Finally, preserve whitespaces that are to be lost. | ||
470 | html = html.replace( /(>|\s) /g, function( match, before ) { | ||
471 | return before + ' '; | ||
472 | } ).replace( / (?=<)/g, ' ' ); | ||
473 | |||
474 | return html; | ||
475 | }, | ||
476 | |||
477 | /** | ||
478 | * Gets a unique number for this CKEDITOR execution session. It returns | ||
479 | * consecutive numbers starting from 1. | ||
480 | * | ||
481 | * alert( CKEDITOR.tools.getNextNumber() ); // (e.g.) 1 | ||
482 | * alert( CKEDITOR.tools.getNextNumber() ); // 2 | ||
483 | * | ||
484 | * @method | ||
485 | * @returns {Number} A unique number. | ||
486 | */ | ||
487 | getNextNumber: ( function() { | ||
488 | var last = 0; | ||
489 | return function() { | ||
490 | return ++last; | ||
491 | }; | ||
492 | } )(), | ||
493 | |||
494 | /** | ||
495 | * Gets a unique ID for CKEditor interface elements. It returns a | ||
496 | * string with the "cke_" prefix and a consecutive number. | ||
497 | * | ||
498 | * alert( CKEDITOR.tools.getNextId() ); // (e.g.) 'cke_1' | ||
499 | * alert( CKEDITOR.tools.getNextId() ); // 'cke_2' | ||
500 | * | ||
501 | * @returns {String} A unique ID. | ||
502 | */ | ||
503 | getNextId: function() { | ||
504 | return 'cke_' + this.getNextNumber(); | ||
505 | }, | ||
506 | |||
507 | /** | ||
508 | * Gets a universally unique ID. It returns a random string | ||
509 | * compliant with ISO/IEC 11578:1996, without dashes, with the "e" prefix to | ||
510 | * make sure that the ID does not start with a number. | ||
511 | * | ||
512 | * @returns {String} A global unique ID. | ||
513 | */ | ||
514 | getUniqueId: function() { | ||
515 | var uuid = 'e'; // Make sure that id does not start with number. | ||
516 | for ( var i = 0; i < 8; i++ ) { | ||
517 | uuid += Math.floor( ( 1 + Math.random() ) * 0x10000 ).toString( 16 ).substring( 1 ); | ||
518 | } | ||
519 | return uuid; | ||
520 | }, | ||
521 | |||
522 | /** | ||
523 | * Creates a function override. | ||
524 | * | ||
525 | * var obj = { | ||
526 | * myFunction: function( name ) { | ||
527 | * alert( 'Name: ' + name ); | ||
528 | * } | ||
529 | * }; | ||
530 | * | ||
531 | * obj.myFunction = CKEDITOR.tools.override( obj.myFunction, function( myFunctionOriginal ) { | ||
532 | * return function( name ) { | ||
533 | * alert( 'Overriden name: ' + name ); | ||
534 | * myFunctionOriginal.call( this, name ); | ||
535 | * }; | ||
536 | * } ); | ||
537 | * | ||
538 | * @param {Function} originalFunction The function to be overridden. | ||
539 | * @param {Function} functionBuilder A function that returns the new | ||
540 | * function. The original function reference will be passed to this function. | ||
541 | * @returns {Function} The new function. | ||
542 | */ | ||
543 | override: function( originalFunction, functionBuilder ) { | ||
544 | var newFn = functionBuilder( originalFunction ); | ||
545 | newFn.prototype = originalFunction.prototype; | ||
546 | return newFn; | ||
547 | }, | ||
548 | |||
549 | /** | ||
550 | * Executes a function after a specified delay. | ||
551 | * | ||
552 | * CKEDITOR.tools.setTimeout( function() { | ||
553 | * alert( 'Executed after 2 seconds' ); | ||
554 | * }, 2000 ); | ||
555 | * | ||
556 | * @param {Function} func The function to be executed. | ||
557 | * @param {Number} [milliseconds=0] The amount of time (in milliseconds) to wait | ||
558 | * to fire the function execution. | ||
559 | * @param {Object} [scope=window] The object to store the function execution scope | ||
560 | * (the `this` object). | ||
561 | * @param {Object/Array} [args] A single object, or an array of objects, to | ||
562 | * pass as argument to the function. | ||
563 | * @param {Object} [ownerWindow=window] The window that will be used to set the | ||
564 | * timeout. | ||
565 | * @returns {Object} A value that can be used to cancel the function execution. | ||
566 | */ | ||
567 | setTimeout: function( func, milliseconds, scope, args, ownerWindow ) { | ||
568 | if ( !ownerWindow ) | ||
569 | ownerWindow = window; | ||
570 | |||
571 | if ( !scope ) | ||
572 | scope = ownerWindow; | ||
573 | |||
574 | return ownerWindow.setTimeout( function() { | ||
575 | if ( args ) | ||
576 | func.apply( scope, [].concat( args ) ); | ||
577 | else | ||
578 | func.apply( scope ); | ||
579 | }, milliseconds || 0 ); | ||
580 | }, | ||
581 | |||
582 | /** | ||
583 | * Removes spaces from the start and the end of a string. The following | ||
584 | * characters are removed: space, tab, line break, line feed. | ||
585 | * | ||
586 | * alert( CKEDITOR.tools.trim( ' example ' ); // 'example' | ||
587 | * | ||
588 | * @method | ||
589 | * @param {String} str The text from which the spaces will be removed. | ||
590 | * @returns {String} The modified string without the boundary spaces. | ||
591 | */ | ||
592 | trim: ( function() { | ||
593 | // We are not using \s because we don't want "non-breaking spaces" to be caught. | ||
594 | var trimRegex = /(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g; | ||
595 | return function( str ) { | ||
596 | return str.replace( trimRegex, '' ); | ||
597 | }; | ||
598 | } )(), | ||
599 | |||
600 | /** | ||
601 | * Removes spaces from the start (left) of a string. The following | ||
602 | * characters are removed: space, tab, line break, line feed. | ||
603 | * | ||
604 | * alert( CKEDITOR.tools.ltrim( ' example ' ); // 'example ' | ||
605 | * | ||
606 | * @method | ||
607 | * @param {String} str The text from which the spaces will be removed. | ||
608 | * @returns {String} The modified string excluding the removed spaces. | ||
609 | */ | ||
610 | ltrim: ( function() { | ||
611 | // We are not using \s because we don't want "non-breaking spaces" to be caught. | ||
612 | var trimRegex = /^[ \t\n\r]+/g; | ||
613 | return function( str ) { | ||
614 | return str.replace( trimRegex, '' ); | ||
615 | }; | ||
616 | } )(), | ||
617 | |||
618 | /** | ||
619 | * Removes spaces from the end (right) of a string. The following | ||
620 | * characters are removed: space, tab, line break, line feed. | ||
621 | * | ||
622 | * alert( CKEDITOR.tools.ltrim( ' example ' ); // ' example' | ||
623 | * | ||
624 | * @method | ||
625 | * @param {String} str The text from which spaces will be removed. | ||
626 | * @returns {String} The modified string excluding the removed spaces. | ||
627 | */ | ||
628 | rtrim: ( function() { | ||
629 | // We are not using \s because we don't want "non-breaking spaces" to be caught. | ||
630 | var trimRegex = /[ \t\n\r]+$/g; | ||
631 | return function( str ) { | ||
632 | return str.replace( trimRegex, '' ); | ||
633 | }; | ||
634 | } )(), | ||
635 | |||
636 | /** | ||
637 | * Returns the index of an element in an array. | ||
638 | * | ||
639 | * var letters = [ 'a', 'b', 0, 'c', false ]; | ||
640 | * alert( CKEDITOR.tools.indexOf( letters, '0' ) ); // -1 because 0 !== '0' | ||
641 | * alert( CKEDITOR.tools.indexOf( letters, false ) ); // 4 because 0 !== false | ||
642 | * | ||
643 | * @param {Array} array The array to be searched. | ||
644 | * @param {Object/Function} value The element to be found. This can be an | ||
645 | * evaluation function which receives a single parameter call for | ||
646 | * each entry in the array, returning `true` if the entry matches. | ||
647 | * @returns {Number} The (zero-based) index of the first entry that matches | ||
648 | * the entry, or `-1` if not found. | ||
649 | */ | ||
650 | indexOf: function( array, value ) { | ||
651 | if ( typeof value == 'function' ) { | ||
652 | for ( var i = 0, len = array.length; i < len; i++ ) { | ||
653 | if ( value( array[ i ] ) ) | ||
654 | return i; | ||
655 | } | ||
656 | } else if ( array.indexOf ) | ||
657 | return array.indexOf( value ); | ||
658 | else { | ||
659 | for ( i = 0, len = array.length; i < len; i++ ) { | ||
660 | if ( array[ i ] === value ) | ||
661 | return i; | ||
662 | } | ||
663 | } | ||
664 | return -1; | ||
665 | }, | ||
666 | |||
667 | /** | ||
668 | * Returns the index of an element in an array. | ||
669 | * | ||
670 | * var obj = { prop: true }; | ||
671 | * var letters = [ 'a', 'b', 0, obj, false ]; | ||
672 | * | ||
673 | * alert( CKEDITOR.tools.indexOf( letters, '0' ) ); // null | ||
674 | * alert( CKEDITOR.tools.indexOf( letters, function( value ) { | ||
675 | * // Return true when passed value has property 'prop'. | ||
676 | * return value && 'prop' in value; | ||
677 | * } ) ); // obj | ||
678 | * | ||
679 | * @param {Array} array The array to be searched. | ||
680 | * @param {Object/Function} value The element to be found. Can be an | ||
681 | * evaluation function which receives a single parameter call for | ||
682 | * each entry in the array, returning `true` if the entry matches. | ||
683 | * @returns Object The value that was found in an array. | ||
684 | */ | ||
685 | search: function( array, value ) { | ||
686 | var index = CKEDITOR.tools.indexOf( array, value ); | ||
687 | return index >= 0 ? array[ index ] : null; | ||
688 | }, | ||
689 | |||
690 | /** | ||
691 | * Creates a function that will always execute in the context of a | ||
692 | * specified object. | ||
693 | * | ||
694 | * var obj = { text: 'My Object' }; | ||
695 | * | ||
696 | * function alertText() { | ||
697 | * alert( this.text ); | ||
698 | * } | ||
699 | * | ||
700 | * var newFunc = CKEDITOR.tools.bind( alertText, obj ); | ||
701 | * newFunc(); // Alerts 'My Object'. | ||
702 | * | ||
703 | * @param {Function} func The function to be executed. | ||
704 | * @param {Object} obj The object to which the execution context will be bound. | ||
705 | * @returns {Function} The function that can be used to execute the | ||
706 | * `func` function in the context of `obj`. | ||
707 | */ | ||
708 | bind: function( func, obj ) { | ||
709 | return function() { | ||
710 | return func.apply( obj, arguments ); | ||
711 | }; | ||
712 | }, | ||
713 | |||
714 | /** | ||
715 | * Class creation based on prototype inheritance which supports of the | ||
716 | * following features: | ||
717 | * | ||
718 | * * Static fields | ||
719 | * * Private fields | ||
720 | * * Public (prototype) fields | ||
721 | * * Chainable base class constructor | ||
722 | * | ||
723 | * @param {Object} definition The class definition object. | ||
724 | * @returns {Function} A class-like JavaScript function. | ||
725 | */ | ||
726 | createClass: function( definition ) { | ||
727 | var $ = definition.$, | ||
728 | baseClass = definition.base, | ||
729 | privates = definition.privates || definition._, | ||
730 | proto = definition.proto, | ||
731 | statics = definition.statics; | ||
732 | |||
733 | // Create the constructor, if not present in the definition. | ||
734 | !$ && ( $ = function() { | ||
735 | baseClass && this.base.apply( this, arguments ); | ||
736 | } ); | ||
737 | |||
738 | if ( privates ) { | ||
739 | var originalConstructor = $; | ||
740 | $ = function() { | ||
741 | // Create (and get) the private namespace. | ||
742 | var _ = this._ || ( this._ = {} ); | ||
743 | |||
744 | // Make some magic so "this" will refer to the main | ||
745 | // instance when coding private functions. | ||
746 | for ( var privateName in privates ) { | ||
747 | var priv = privates[ privateName ]; | ||
748 | |||
749 | _[ privateName ] = ( typeof priv == 'function' ) ? CKEDITOR.tools.bind( priv, this ) : priv; | ||
750 | } | ||
751 | |||
752 | originalConstructor.apply( this, arguments ); | ||
753 | }; | ||
754 | } | ||
755 | |||
756 | if ( baseClass ) { | ||
757 | $.prototype = this.prototypedCopy( baseClass.prototype ); | ||
758 | $.prototype.constructor = $; | ||
759 | // Super references. | ||
760 | $.base = baseClass; | ||
761 | $.baseProto = baseClass.prototype; | ||
762 | // Super constructor. | ||
763 | $.prototype.base = function() { | ||
764 | this.base = baseClass.prototype.base; | ||
765 | baseClass.apply( this, arguments ); | ||
766 | this.base = arguments.callee; | ||
767 | }; | ||
768 | } | ||
769 | |||
770 | if ( proto ) | ||
771 | this.extend( $.prototype, proto, true ); | ||
772 | |||
773 | if ( statics ) | ||
774 | this.extend( $, statics, true ); | ||
775 | |||
776 | return $; | ||
777 | }, | ||
778 | |||
779 | /** | ||
780 | * Creates a function reference that can be called later using | ||
781 | * {@link #callFunction}. This approach is especially useful to | ||
782 | * make DOM attribute function calls to JavaScript-defined functions. | ||
783 | * | ||
784 | * var ref = CKEDITOR.tools.addFunction( function() { | ||
785 | * alert( 'Hello!'); | ||
786 | * } ); | ||
787 | * CKEDITOR.tools.callFunction( ref ); // 'Hello!' | ||
788 | * | ||
789 | * @param {Function} fn The function to be executed on call. | ||
790 | * @param {Object} [scope] The object to have the context on `fn` execution. | ||
791 | * @returns {Number} A unique reference to be used in conjuction with | ||
792 | * {@link #callFunction}. | ||
793 | */ | ||
794 | addFunction: function( fn, scope ) { | ||
795 | return functions.push( function() { | ||
796 | return fn.apply( scope || this, arguments ); | ||
797 | } ) - 1; | ||
798 | }, | ||
799 | |||
800 | /** | ||
801 | * Removes the function reference created with {@link #addFunction}. | ||
802 | * | ||
803 | * @param {Number} ref The function reference created with | ||
804 | * {@link #addFunction}. | ||
805 | */ | ||
806 | removeFunction: function( ref ) { | ||
807 | functions[ ref ] = null; | ||
808 | }, | ||
809 | |||
810 | /** | ||
811 | * Executes a function based on the reference created with {@link #addFunction}. | ||
812 | * | ||
813 | * var ref = CKEDITOR.tools.addFunction( function() { | ||
814 | * alert( 'Hello!'); | ||
815 | * } ); | ||
816 | * CKEDITOR.tools.callFunction( ref ); // 'Hello!' | ||
817 | * | ||
818 | * @param {Number} ref The function reference created with {@link #addFunction}. | ||
819 | * @param {Mixed} params Any number of parameters to be passed to the executed function. | ||
820 | * @returns {Mixed} The return value of the function. | ||
821 | */ | ||
822 | callFunction: function( ref ) { | ||
823 | var fn = functions[ ref ]; | ||
824 | return fn && fn.apply( window, Array.prototype.slice.call( arguments, 1 ) ); | ||
825 | }, | ||
826 | |||
827 | /** | ||
828 | * Appends the `px` length unit to the size value if it is missing. | ||
829 | * | ||
830 | * var cssLength = CKEDITOR.tools.cssLength; | ||
831 | * cssLength( 42 ); // '42px' | ||
832 | * cssLength( '42' ); // '42px' | ||
833 | * cssLength( '42px' ); // '42px' | ||
834 | * cssLength( '42%' ); // '42%' | ||
835 | * cssLength( 'bold' ); // 'bold' | ||
836 | * cssLength( false ); // '' | ||
837 | * cssLength( NaN ); // '' | ||
838 | * | ||
839 | * @method | ||
840 | * @param {Number/String/Boolean} length | ||
841 | */ | ||
842 | cssLength: ( function() { | ||
843 | var pixelRegex = /^-?\d+\.?\d*px$/, | ||
844 | lengthTrimmed; | ||
845 | |||
846 | return function( length ) { | ||
847 | lengthTrimmed = CKEDITOR.tools.trim( length + '' ) + 'px'; | ||
848 | |||
849 | if ( pixelRegex.test( lengthTrimmed ) ) | ||
850 | return lengthTrimmed; | ||
851 | else | ||
852 | return length || ''; | ||
853 | }; | ||
854 | } )(), | ||
855 | |||
856 | /** | ||
857 | * Converts the specified CSS length value to the calculated pixel length inside this page. | ||
858 | * | ||
859 | * **Note:** Percentage-based value is left intact. | ||
860 | * | ||
861 | * @method | ||
862 | * @param {String} cssLength CSS length value. | ||
863 | */ | ||
864 | convertToPx: ( function() { | ||
865 | var calculator; | ||
866 | |||
867 | return function( cssLength ) { | ||
868 | if ( !calculator ) { | ||
869 | calculator = CKEDITOR.dom.element.createFromHtml( '<div style="position:absolute;left:-9999px;' + | ||
870 | 'top:-9999px;margin:0px;padding:0px;border:0px;"' + | ||
871 | '></div>', CKEDITOR.document ); | ||
872 | CKEDITOR.document.getBody().append( calculator ); | ||
873 | } | ||
874 | |||
875 | if ( !( /%$/ ).test( cssLength ) ) { | ||
876 | calculator.setStyle( 'width', cssLength ); | ||
877 | return calculator.$.clientWidth; | ||
878 | } | ||
879 | |||
880 | return cssLength; | ||
881 | }; | ||
882 | } )(), | ||
883 | |||
884 | /** | ||
885 | * String specified by `str` repeats `times` times. | ||
886 | * | ||
887 | * @param {String} str | ||
888 | * @param {Number} times | ||
889 | * @returns {String} | ||
890 | */ | ||
891 | repeat: function( str, times ) { | ||
892 | return new Array( times + 1 ).join( str ); | ||
893 | }, | ||
894 | |||
895 | /** | ||
896 | * Returns the first successfully executed return value of a function that | ||
897 | * does not throw any exception. | ||
898 | * | ||
899 | * @param {Function...} fn | ||
900 | * @returns {Mixed} | ||
901 | */ | ||
902 | tryThese: function() { | ||
903 | var returnValue; | ||
904 | for ( var i = 0, length = arguments.length; i < length; i++ ) { | ||
905 | var lambda = arguments[ i ]; | ||
906 | try { | ||
907 | returnValue = lambda(); | ||
908 | break; | ||
909 | } catch ( e ) {} | ||
910 | } | ||
911 | return returnValue; | ||
912 | }, | ||
913 | |||
914 | /** | ||
915 | * Generates a combined key from a series of params. | ||
916 | * | ||
917 | * var key = CKEDITOR.tools.genKey( 'key1', 'key2', 'key3' ); | ||
918 | * alert( key ); // 'key1-key2-key3'. | ||
919 | * | ||
920 | * @param {String} subKey One or more strings used as subkeys. | ||
921 | * @returns {String} | ||
922 | */ | ||
923 | genKey: function() { | ||
924 | return Array.prototype.slice.call( arguments ).join( '-' ); | ||
925 | }, | ||
926 | |||
927 | /** | ||
928 | * Creates a "deferred" function which will not run immediately, | ||
929 | * but rather runs as soon as the interpreter’s call stack is empty. | ||
930 | * Behaves much like `window.setTimeout` with a delay. | ||
931 | * | ||
932 | * **Note:** The return value of the original function will be lost. | ||
933 | * | ||
934 | * @param {Function} fn The callee function. | ||
935 | * @returns {Function} The new deferred function. | ||
936 | */ | ||
937 | defer: function( fn ) { | ||
938 | return function() { | ||
939 | var args = arguments, | ||
940 | self = this; | ||
941 | window.setTimeout( function() { | ||
942 | fn.apply( self, args ); | ||
943 | }, 0 ); | ||
944 | }; | ||
945 | }, | ||
946 | |||
947 | /** | ||
948 | * Normalizes CSS data in order to avoid differences in the style attribute. | ||
949 | * | ||
950 | * @param {String} styleText The style data to be normalized. | ||
951 | * @param {Boolean} [nativeNormalize=false] Parse the data using the browser. | ||
952 | * @returns {String} The normalized value. | ||
953 | */ | ||
954 | normalizeCssText: function( styleText, nativeNormalize ) { | ||
955 | var props = [], | ||
956 | name, | ||
957 | parsedProps = CKEDITOR.tools.parseCssText( styleText, true, nativeNormalize ); | ||
958 | |||
959 | for ( name in parsedProps ) | ||
960 | props.push( name + ':' + parsedProps[ name ] ); | ||
961 | |||
962 | props.sort(); | ||
963 | |||
964 | return props.length ? ( props.join( ';' ) + ';' ) : ''; | ||
965 | }, | ||
966 | |||
967 | /** | ||
968 | * Finds and converts `rgb(x,x,x)` color definition to hexadecimal notation. | ||
969 | * | ||
970 | * @param {String} styleText The style data (or just a string containing RGB colors) to be converted. | ||
971 | * @returns {String} The style data with RGB colors converted to hexadecimal equivalents. | ||
972 | */ | ||
973 | convertRgbToHex: function( styleText ) { | ||
974 | return styleText.replace( /(?:rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\))/gi, function( match, red, green, blue ) { | ||
975 | var color = [ red, green, blue ]; | ||
976 | // Add padding zeros if the hex value is less than 0x10. | ||
977 | for ( var i = 0; i < 3; i++ ) | ||
978 | color[ i ] = ( '0' + parseInt( color[ i ], 10 ).toString( 16 ) ).slice( -2 ); | ||
979 | return '#' + color.join( '' ); | ||
980 | } ); | ||
981 | }, | ||
982 | |||
983 | /** | ||
984 | * Turns inline style text properties into one hash. | ||
985 | * | ||
986 | * @param {String} styleText The style data to be parsed. | ||
987 | * @param {Boolean} [normalize=false] Normalize properties and values | ||
988 | * (e.g. trim spaces, convert to lower case). | ||
989 | * @param {Boolean} [nativeNormalize=false] Parse the data using the browser. | ||
990 | * @returns {Object} The object containing parsed properties. | ||
991 | */ | ||
992 | parseCssText: function( styleText, normalize, nativeNormalize ) { | ||
993 | var retval = {}; | ||
994 | |||
995 | if ( nativeNormalize ) { | ||
996 | // Injects the style in a temporary span object, so the browser parses it, | ||
997 | // retrieving its final format. | ||
998 | var temp = new CKEDITOR.dom.element( 'span' ); | ||
999 | temp.setAttribute( 'style', styleText ); | ||
1000 | styleText = CKEDITOR.tools.convertRgbToHex( temp.getAttribute( 'style' ) || '' ); | ||
1001 | } | ||
1002 | |||
1003 | // IE will leave a single semicolon when failed to parse the style text. (#3891) | ||
1004 | if ( !styleText || styleText == ';' ) | ||
1005 | return retval; | ||
1006 | |||
1007 | styleText.replace( /"/g, '"' ).replace( /\s*([^:;\s]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value ) { | ||
1008 | if ( normalize ) { | ||
1009 | name = name.toLowerCase(); | ||
1010 | // Normalize font-family property, ignore quotes and being case insensitive. (#7322) | ||
1011 | // http://www.w3.org/TR/css3-fonts/#font-family-the-font-family-property | ||
1012 | if ( name == 'font-family' ) | ||
1013 | value = value.toLowerCase().replace( /["']/g, '' ).replace( /\s*,\s*/g, ',' ); | ||
1014 | value = CKEDITOR.tools.trim( value ); | ||
1015 | } | ||
1016 | |||
1017 | retval[ name ] = value; | ||
1018 | } ); | ||
1019 | return retval; | ||
1020 | }, | ||
1021 | |||
1022 | /** | ||
1023 | * Serializes the `style name => value` hash to a style text. | ||
1024 | * | ||
1025 | * var styleObj = CKEDITOR.tools.parseCssText( 'color: red; border: none' ); | ||
1026 | * console.log( styleObj.color ); // -> 'red' | ||
1027 | * CKEDITOR.tools.writeCssText( styleObj ); // -> 'color:red; border:none' | ||
1028 | * CKEDITOR.tools.writeCssText( styleObj, true ); // -> 'border:none; color:red' | ||
1029 | * | ||
1030 | * @since 4.1 | ||
1031 | * @param {Object} styles The object contaning style properties. | ||
1032 | * @param {Boolean} [sort] Whether to sort CSS properties. | ||
1033 | * @returns {String} The serialized style text. | ||
1034 | */ | ||
1035 | writeCssText: function( styles, sort ) { | ||
1036 | var name, | ||
1037 | stylesArr = []; | ||
1038 | |||
1039 | for ( name in styles ) | ||
1040 | stylesArr.push( name + ':' + styles[ name ] ); | ||
1041 | |||
1042 | if ( sort ) | ||
1043 | stylesArr.sort(); | ||
1044 | |||
1045 | return stylesArr.join( '; ' ); | ||
1046 | }, | ||
1047 | |||
1048 | /** | ||
1049 | * Compares two objects. | ||
1050 | * | ||
1051 | * **Note:** This method performs shallow, non-strict comparison. | ||
1052 | * | ||
1053 | * @since 4.1 | ||
1054 | * @param {Object} left | ||
1055 | * @param {Object} right | ||
1056 | * @param {Boolean} [onlyLeft] Check only the properties that are present in the `left` object. | ||
1057 | * @returns {Boolean} Whether objects are identical. | ||
1058 | */ | ||
1059 | objectCompare: function( left, right, onlyLeft ) { | ||
1060 | var name; | ||
1061 | |||
1062 | if ( !left && !right ) | ||
1063 | return true; | ||
1064 | if ( !left || !right ) | ||
1065 | return false; | ||
1066 | |||
1067 | for ( name in left ) { | ||
1068 | if ( left[ name ] != right[ name ] ) | ||
1069 | return false; | ||
1070 | |||
1071 | } | ||
1072 | |||
1073 | if ( !onlyLeft ) { | ||
1074 | for ( name in right ) { | ||
1075 | if ( left[ name ] != right[ name ] ) | ||
1076 | return false; | ||
1077 | } | ||
1078 | } | ||
1079 | |||
1080 | return true; | ||
1081 | }, | ||
1082 | |||
1083 | /** | ||
1084 | * Returns an array of passed object's keys. | ||
1085 | * | ||
1086 | * console.log( CKEDITOR.tools.objectKeys( { foo: 1, bar: false } ); | ||
1087 | * // -> [ 'foo', 'bar' ] | ||
1088 | * | ||
1089 | * @since 4.1 | ||
1090 | * @param {Object} obj | ||
1091 | * @returns {Array} Object's keys. | ||
1092 | */ | ||
1093 | objectKeys: function( obj ) { | ||
1094 | var keys = []; | ||
1095 | for ( var i in obj ) | ||
1096 | keys.push( i ); | ||
1097 | |||
1098 | return keys; | ||
1099 | }, | ||
1100 | |||
1101 | /** | ||
1102 | * Converts an array to an object by rewriting array items | ||
1103 | * to object properties. | ||
1104 | * | ||
1105 | * var arr = [ 'foo', 'bar', 'foo' ]; | ||
1106 | * console.log( CKEDITOR.tools.convertArrayToObject( arr ) ); | ||
1107 | * // -> { foo: true, bar: true } | ||
1108 | * console.log( CKEDITOR.tools.convertArrayToObject( arr, 1 ) ); | ||
1109 | * // -> { foo: 1, bar: 1 } | ||
1110 | * | ||
1111 | * @since 4.1 | ||
1112 | * @param {Array} arr The array to be converted to an object. | ||
1113 | * @param [fillWith=true] Set each property of an object to `fillWith` value. | ||
1114 | */ | ||
1115 | convertArrayToObject: function( arr, fillWith ) { | ||
1116 | var obj = {}; | ||
1117 | |||
1118 | if ( arguments.length == 1 ) | ||
1119 | fillWith = true; | ||
1120 | |||
1121 | for ( var i = 0, l = arr.length; i < l; ++i ) | ||
1122 | obj[ arr[ i ] ] = fillWith; | ||
1123 | |||
1124 | return obj; | ||
1125 | }, | ||
1126 | |||
1127 | /** | ||
1128 | * Tries to fix the `document.domain` of the current document to match the | ||
1129 | * parent window domain, avoiding "Same Origin" policy issues. | ||
1130 | * This is an Internet Explorer only requirement. | ||
1131 | * | ||
1132 | * @since 4.1.2 | ||
1133 | * @returns {Boolean} `true` if the current domain is already good or if | ||
1134 | * it has been fixed successfully. | ||
1135 | */ | ||
1136 | fixDomain: function() { | ||
1137 | var domain; | ||
1138 | |||
1139 | while ( 1 ) { | ||
1140 | try { | ||
1141 | // Try to access the parent document. It throws | ||
1142 | // "access denied" if restricted by the "Same Origin" policy. | ||
1143 | domain = window.parent.document.domain; | ||
1144 | break; | ||
1145 | } catch ( e ) { | ||
1146 | // Calculate the value to set to document.domain. | ||
1147 | domain = domain ? | ||
1148 | |||
1149 | // If it is not the first pass, strip one part of the | ||
1150 | // name. E.g. "test.example.com" => "example.com" | ||
1151 | domain.replace( /.+?(?:\.|$)/, '' ) : | ||
1152 | |||
1153 | // In the first pass, we'll handle the | ||
1154 | // "document.domain = document.domain" case. | ||
1155 | document.domain; | ||
1156 | |||
1157 | // Stop here if there is no more domain parts available. | ||
1158 | if ( !domain ) | ||
1159 | break; | ||
1160 | |||
1161 | document.domain = domain; | ||
1162 | } | ||
1163 | } | ||
1164 | |||
1165 | return !!domain; | ||
1166 | }, | ||
1167 | |||
1168 | /** | ||
1169 | * Buffers `input` events (or any `input` calls) | ||
1170 | * and triggers `output` not more often than once per `minInterval`. | ||
1171 | * | ||
1172 | * var buffer = CKEDITOR.tools.eventsBuffer( 200, function() { | ||
1173 | * console.log( 'foo!' ); | ||
1174 | * } ); | ||
1175 | * | ||
1176 | * buffer.input(); | ||
1177 | * // 'foo!' logged immediately. | ||
1178 | * buffer.input(); | ||
1179 | * // Nothing logged. | ||
1180 | * buffer.input(); | ||
1181 | * // Nothing logged. | ||
1182 | * // ... after 200ms a single 'foo!' will be logged. | ||
1183 | * | ||
1184 | * Can be easily used with events: | ||
1185 | * | ||
1186 | * var buffer = CKEDITOR.tools.eventsBuffer( 200, function() { | ||
1187 | * console.log( 'foo!' ); | ||
1188 | * } ); | ||
1189 | * | ||
1190 | * editor.on( 'key', buffer.input ); | ||
1191 | * // Note: There is no need to bind buffer as a context. | ||
1192 | * | ||
1193 | * @since 4.2.1 | ||
1194 | * @param {Number} minInterval Minimum interval between `output` calls in milliseconds. | ||
1195 | * @param {Function} output Function that will be executed as `output`. | ||
1196 | * @param {Object} [scopeObj] The object used to scope the listener call (the `this` object). | ||
1197 | * @returns {Object} | ||
1198 | * @returns {Function} return.input Buffer's input method. | ||
1199 | * @returns {Function} return.reset Resets buffered events — `output` will not be executed | ||
1200 | * until next `input` is triggered. | ||
1201 | */ | ||
1202 | eventsBuffer: function( minInterval, output, scopeObj ) { | ||
1203 | var scheduled, | ||
1204 | lastOutput = 0; | ||
1205 | |||
1206 | function triggerOutput() { | ||
1207 | lastOutput = ( new Date() ).getTime(); | ||
1208 | scheduled = false; | ||
1209 | if ( scopeObj ) { | ||
1210 | output.call( scopeObj ); | ||
1211 | } else { | ||
1212 | output(); | ||
1213 | } | ||
1214 | } | ||
1215 | |||
1216 | return { | ||
1217 | input: function() { | ||
1218 | if ( scheduled ) | ||
1219 | return; | ||
1220 | |||
1221 | var diff = ( new Date() ).getTime() - lastOutput; | ||
1222 | |||
1223 | // If less than minInterval passed after last check, | ||
1224 | // schedule next for minInterval after previous one. | ||
1225 | if ( diff < minInterval ) | ||
1226 | scheduled = setTimeout( triggerOutput, minInterval - diff ); | ||
1227 | else | ||
1228 | triggerOutput(); | ||
1229 | }, | ||
1230 | |||
1231 | reset: function() { | ||
1232 | if ( scheduled ) | ||
1233 | clearTimeout( scheduled ); | ||
1234 | |||
1235 | scheduled = lastOutput = 0; | ||
1236 | } | ||
1237 | }; | ||
1238 | }, | ||
1239 | |||
1240 | /** | ||
1241 | * Enables HTML5 elements for older browsers (IE8) in the passed document. | ||
1242 | * | ||
1243 | * In IE8 this method can also be executed on a document fragment. | ||
1244 | * | ||
1245 | * **Note:** This method has to be used in the `<head>` section of the document. | ||
1246 | * | ||
1247 | * @since 4.3 | ||
1248 | * @param {Object} doc Native `Document` or `DocumentFragment` in which the elements will be enabled. | ||
1249 | * @param {Boolean} [withAppend] Whether to append created elements to the `doc`. | ||
1250 | */ | ||
1251 | enableHtml5Elements: function( doc, withAppend ) { | ||
1252 | var els = 'abbr,article,aside,audio,bdi,canvas,data,datalist,details,figcaption,figure,footer,header,hgroup,main,mark,meter,nav,output,progress,section,summary,time,video'.split( ',' ), | ||
1253 | i = els.length, | ||
1254 | el; | ||
1255 | |||
1256 | while ( i-- ) { | ||
1257 | el = doc.createElement( els[ i ] ); | ||
1258 | if ( withAppend ) | ||
1259 | doc.appendChild( el ); | ||
1260 | } | ||
1261 | }, | ||
1262 | |||
1263 | /** | ||
1264 | * Checks if any of the `arr` items match the provided regular expression. | ||
1265 | * | ||
1266 | * @param {Array} arr The array whose items will be checked. | ||
1267 | * @param {RegExp} regexp The regular expression. | ||
1268 | * @returns {Boolean} Returns `true` for the first occurrence of the search pattern. | ||
1269 | * @since 4.4 | ||
1270 | */ | ||
1271 | checkIfAnyArrayItemMatches: function( arr, regexp ) { | ||
1272 | for ( var i = 0, l = arr.length; i < l; ++i ) { | ||
1273 | if ( arr[ i ].match( regexp ) ) | ||
1274 | return true; | ||
1275 | } | ||
1276 | return false; | ||
1277 | }, | ||
1278 | |||
1279 | /** | ||
1280 | * Checks if any of the `obj` properties match the provided regular expression. | ||
1281 | * | ||
1282 | * @param obj The object whose properties will be checked. | ||
1283 | * @param {RegExp} regexp The regular expression. | ||
1284 | * @returns {Boolean} Returns `true` for the first occurrence of the search pattern. | ||
1285 | * @since 4.4 | ||
1286 | */ | ||
1287 | checkIfAnyObjectPropertyMatches: function( obj, regexp ) { | ||
1288 | for ( var i in obj ) { | ||
1289 | if ( i.match( regexp ) ) | ||
1290 | return true; | ||
1291 | } | ||
1292 | return false; | ||
1293 | }, | ||
1294 | |||
1295 | /** | ||
1296 | * The data URI of a transparent image. May be used e.g. in HTML as an image source or in CSS in `url()`. | ||
1297 | * | ||
1298 | * @since 4.4 | ||
1299 | * @readonly | ||
1300 | */ | ||
1301 | transparentImageData: 'data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==', | ||
1302 | |||
1303 | |||
1304 | /** | ||
1305 | * Returns the value of the cookie with a given name or `null` if the cookie is not found. | ||
1306 | * | ||
1307 | * @since 4.5.6 | ||
1308 | * @param {String} name | ||
1309 | * @returns {String} | ||
1310 | */ | ||
1311 | getCookie: function( name ) { | ||
1312 | name = name.toLowerCase(); | ||
1313 | var parts = document.cookie.split( ';' ); | ||
1314 | var pair, key; | ||
1315 | |||
1316 | for ( var i = 0; i < parts.length; i++ ) { | ||
1317 | pair = parts[ i ].split( '=' ); | ||
1318 | key = decodeURIComponent( CKEDITOR.tools.trim( pair[ 0 ] ).toLowerCase() ); | ||
1319 | |||
1320 | if ( key === name ) { | ||
1321 | return decodeURIComponent( pair.length > 1 ? pair[ 1 ] : '' ); | ||
1322 | } | ||
1323 | } | ||
1324 | |||
1325 | return null; | ||
1326 | }, | ||
1327 | |||
1328 | /** | ||
1329 | * Sets the value of the cookie with a given name. | ||
1330 | * | ||
1331 | * @since 4.5.6 | ||
1332 | * @param {String} name | ||
1333 | * @param {String} value | ||
1334 | */ | ||
1335 | setCookie: function( name, value ) { | ||
1336 | document.cookie = encodeURIComponent( name ) + '=' + encodeURIComponent( value ) + ';path=/'; | ||
1337 | }, | ||
1338 | |||
1339 | /** | ||
1340 | * Returns the CSRF token value. The value is a hash stored in `document.cookie` | ||
1341 | * under the `ckCsrfToken` key. The CSRF token can be used to secure the communication | ||
1342 | * between the web browser and the server, i.e. for the file upload feature in the editor. | ||
1343 | * | ||
1344 | * @since 4.5.6 | ||
1345 | * @returns {String} | ||
1346 | */ | ||
1347 | getCsrfToken: function() { | ||
1348 | var token = CKEDITOR.tools.getCookie( TOKEN_COOKIE_NAME ); | ||
1349 | |||
1350 | if ( !token || token.length != TOKEN_LENGTH ) { | ||
1351 | token = generateToken( TOKEN_LENGTH ); | ||
1352 | CKEDITOR.tools.setCookie( TOKEN_COOKIE_NAME, token ); | ||
1353 | } | ||
1354 | |||
1355 | return token; | ||
1356 | } | ||
1357 | }; | ||
1358 | |||
1359 | // Generates a CSRF token with a given length. | ||
1360 | // | ||
1361 | // @since 4.5.6 | ||
1362 | // @param {Number} length | ||
1363 | // @returns {string} | ||
1364 | function generateToken( length ) { | ||
1365 | var randValues = []; | ||
1366 | var result = ''; | ||
1367 | |||
1368 | if ( window.crypto && window.crypto.getRandomValues ) { | ||
1369 | randValues = new Uint8Array( length ); | ||
1370 | window.crypto.getRandomValues( randValues ); | ||
1371 | } else { | ||
1372 | for ( var i = 0; i < length; i++ ) { | ||
1373 | randValues.push( Math.floor( Math.random() * 256 ) ); | ||
1374 | } | ||
1375 | } | ||
1376 | |||
1377 | for ( var j = 0; j < randValues.length; j++ ) { | ||
1378 | var character = tokenCharset.charAt( randValues[ j ] % tokenCharset.length ); | ||
1379 | result += Math.random() > 0.5 ? character.toUpperCase() : character; | ||
1380 | } | ||
1381 | |||
1382 | return result; | ||
1383 | } | ||
1384 | } )(); | ||
1385 | |||
1386 | // PACKAGER_RENAME( CKEDITOR.tools ) | ||
diff --git a/sources/core/ui.js b/sources/core/ui.js new file mode 100644 index 0000000..29ab0ad --- /dev/null +++ b/sources/core/ui.js | |||
@@ -0,0 +1,185 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * Contains UI features related to an editor instance. | ||
8 | * | ||
9 | * @class | ||
10 | * @mixins CKEDITOR.event | ||
11 | * @constructor Creates a `ui` class instance. | ||
12 | * @param {CKEDITOR.editor} editor The editor instance. | ||
13 | */ | ||
14 | CKEDITOR.ui = function( editor ) { | ||
15 | if ( editor.ui ) | ||
16 | return editor.ui; | ||
17 | |||
18 | this.items = {}; | ||
19 | this.instances = {}; | ||
20 | this.editor = editor; | ||
21 | |||
22 | /** | ||
23 | * Object used to store private stuff. | ||
24 | * | ||
25 | * @private | ||
26 | */ | ||
27 | this._ = { | ||
28 | handlers: {} | ||
29 | }; | ||
30 | |||
31 | return this; | ||
32 | }; | ||
33 | |||
34 | // PACKAGER_RENAME( CKEDITOR.ui ) | ||
35 | |||
36 | CKEDITOR.ui.prototype = { | ||
37 | /** | ||
38 | * Adds a UI item to the items collection. These items can be later used in | ||
39 | * the interface. | ||
40 | * | ||
41 | * // Add a new button named 'MyBold'. | ||
42 | * editorInstance.ui.add( 'MyBold', CKEDITOR.UI_BUTTON, { | ||
43 | * label: 'My Bold', | ||
44 | * command: 'bold' | ||
45 | * } ); | ||
46 | * | ||
47 | * @param {String} name The UI item name. | ||
48 | * @param {Object} type The item type. | ||
49 | * @param {Object} definition The item definition. The properties of this | ||
50 | * object depend on the item type. | ||
51 | */ | ||
52 | add: function( name, type, definition ) { | ||
53 | // Compensate the unique name of this ui item to definition. | ||
54 | definition.name = name.toLowerCase(); | ||
55 | |||
56 | var item = this.items[ name ] = { | ||
57 | type: type, | ||
58 | // The name of {@link CKEDITOR.command} which associate with this UI. | ||
59 | command: definition.command || null, | ||
60 | args: Array.prototype.slice.call( arguments, 2 ) | ||
61 | }; | ||
62 | |||
63 | CKEDITOR.tools.extend( item, definition ); | ||
64 | }, | ||
65 | |||
66 | /** | ||
67 | * Retrieves the created UI objects by name. | ||
68 | * | ||
69 | * @param {String} name The name of the UI definition. | ||
70 | */ | ||
71 | get: function( name ) { | ||
72 | return this.instances[ name ]; | ||
73 | }, | ||
74 | |||
75 | /** | ||
76 | * Gets a UI object. | ||
77 | * | ||
78 | * @param {String} name The UI item name. | ||
79 | * @returns {Object} The UI element. | ||
80 | */ | ||
81 | create: function( name ) { | ||
82 | var item = this.items[ name ], | ||
83 | handler = item && this._.handlers[ item.type ], | ||
84 | command = item && item.command && this.editor.getCommand( item.command ); | ||
85 | |||
86 | var result = handler && handler.create.apply( this, item.args ); | ||
87 | |||
88 | this.instances[ name ] = result; | ||
89 | |||
90 | // Add reference inside command object. | ||
91 | if ( command ) | ||
92 | command.uiItems.push( result ); | ||
93 | |||
94 | if ( result && !result.type ) | ||
95 | result.type = item.type; | ||
96 | |||
97 | return result; | ||
98 | }, | ||
99 | |||
100 | /** | ||
101 | * Adds a handler for a UI item type. The handler is responsible for | ||
102 | * transforming UI item definitions into UI objects. | ||
103 | * | ||
104 | * @param {Object} type The item type. | ||
105 | * @param {Object} handler The handler definition. | ||
106 | */ | ||
107 | addHandler: function( type, handler ) { | ||
108 | this._.handlers[ type ] = handler; | ||
109 | }, | ||
110 | |||
111 | /** | ||
112 | * Returns the unique DOM element that represents one editor's UI part, also known as "space". | ||
113 | * There are 3 main editor spaces available: `top`, `contents` and `bottom` | ||
114 | * and their availability depends on editor type. | ||
115 | * | ||
116 | * // Hide the bottom space in the UI. | ||
117 | * var bottom = editor.ui.space( 'bottom' ); | ||
118 | * bottom.setStyle( 'display', 'none' ); | ||
119 | * | ||
120 | * @param {String} name The name of the space. | ||
121 | * @returns {CKEDITOR.dom.element} The element that represents the space. | ||
122 | */ | ||
123 | space: function( name ) { | ||
124 | return CKEDITOR.document.getById( this.spaceId( name ) ); | ||
125 | }, | ||
126 | |||
127 | /** | ||
128 | * Returns the HTML ID for a specific UI space name. | ||
129 | * | ||
130 | * @param {String} name The name of the space. | ||
131 | * @returns {String} The ID of an element representing this space in the DOM. | ||
132 | */ | ||
133 | spaceId: function( name ) { | ||
134 | return this.editor.id + '_' + name; | ||
135 | } | ||
136 | }; | ||
137 | |||
138 | CKEDITOR.event.implementOn( CKEDITOR.ui ); | ||
139 | |||
140 | /** | ||
141 | * Internal event fired when a new UI element is ready. | ||
142 | * | ||
143 | * @event ready | ||
144 | * @param {Object} data The new UI element. | ||
145 | */ | ||
146 | |||
147 | /** | ||
148 | * Virtual class which just illustrates the features of handler objects to be | ||
149 | * passed to the {@link CKEDITOR.ui#addHandler} function. | ||
150 | * This class is not really a part of the API, so do not call its constructor. | ||
151 | * | ||
152 | * @class CKEDITOR.ui.handlerDefinition | ||
153 | */ | ||
154 | |||
155 | /** | ||
156 | * Transforms an item definition into a UI item object. | ||
157 | * | ||
158 | * editorInstance.ui.addHandler( CKEDITOR.UI_BUTTON, { | ||
159 | * create: function( definition ) { | ||
160 | * return new CKEDITOR.ui.button( definition ); | ||
161 | * } | ||
162 | * } ); | ||
163 | * | ||
164 | * @method create | ||
165 | * @param {Object} definition The item definition. | ||
166 | * @returns {Object} The UI element. | ||
167 | * @todo We lack the "UI element" abstract super class. | ||
168 | */ | ||
169 | |||
170 | /** | ||
171 | * The element in the {@link CKEDITOR#document host page's document} that contains the editor content. | ||
172 | * If the [fixed editor UI](#!/guide/dev_uitypes-section-fixed-user-interface) is used, then it will be set to | ||
173 | * `editor.ui.space( 'contents' )` — i.e. the `<div>` which contains the editor `<iframe>` (in case of classic editor) | ||
174 | * or {@link CKEDITOR.editable} (in case of inline editor). Otherwise it is set to the {@link CKEDITOR.editable} itself. | ||
175 | * | ||
176 | * Use the position of this element if you need to position elements placed in the host page's document relatively to the | ||
177 | * editor content. | ||
178 | * | ||
179 | * var editor = CKEDITOR.instances.editor1; | ||
180 | * console.log( editor.ui.contentsElement.getName() ); // 'div' | ||
181 | * | ||
182 | * @since 4.5 | ||
183 | * @readonly | ||
184 | * @property {CKEDITOR.dom.element} contentsElement | ||
185 | */ | ||