aboutsummaryrefslogtreecommitdiff
path: root/sources/core
diff options
context:
space:
mode:
Diffstat (limited to 'sources/core')
-rw-r--r--sources/core/_bootstrap.js74
-rw-r--r--sources/core/ckeditor.js204
-rw-r--r--sources/core/ckeditor_base.js314
-rw-r--r--sources/core/ckeditor_basic.js94
-rw-r--r--sources/core/command.js275
-rw-r--r--sources/core/commanddefinition.js148
-rw-r--r--sources/core/config.js451
-rw-r--r--sources/core/creators/inline.js157
-rw-r--r--sources/core/creators/themedui.js541
-rw-r--r--sources/core/dataprocessor.js70
-rw-r--r--sources/core/dom.js13
-rw-r--r--sources/core/dom/comment.js53
-rw-r--r--sources/core/dom/document.js326
-rw-r--r--sources/core/dom/documentfragment.js62
-rw-r--r--sources/core/dom/domobject.js266
-rw-r--r--sources/core/dom/element.js2107
-rw-r--r--sources/core/dom/elementpath.js251
-rw-r--r--sources/core/dom/event.js208
-rw-r--r--sources/core/dom/iterator.js565
-rw-r--r--sources/core/dom/node.js897
-rw-r--r--sources/core/dom/nodelist.js43
-rw-r--r--sources/core/dom/range.js2860
-rw-r--r--sources/core/dom/rangelist.js199
-rw-r--r--sources/core/dom/text.js135
-rw-r--r--sources/core/dom/walker.js652
-rw-r--r--sources/core/dom/window.js95
-rw-r--r--sources/core/dtd.js349
-rw-r--r--sources/core/editable.js3158
-rw-r--r--sources/core/editor.js1963
-rw-r--r--sources/core/editor_basic.js36
-rw-r--r--sources/core/env.js361
-rw-r--r--sources/core/event.js389
-rw-r--r--sources/core/eventInfo.js115
-rw-r--r--sources/core/filter.js2440
-rw-r--r--sources/core/focusmanager.js273
-rw-r--r--sources/core/htmldataprocessor.js1036
-rw-r--r--sources/core/htmlparser.js205
-rw-r--r--sources/core/htmlparser/basicwriter.js152
-rw-r--r--sources/core/htmlparser/cdata.js48
-rw-r--r--sources/core/htmlparser/comment.js80
-rw-r--r--sources/core/htmlparser/element.js536
-rw-r--r--sources/core/htmlparser/filter.js407
-rw-r--r--sources/core/htmlparser/fragment.js646
-rw-r--r--sources/core/htmlparser/node.js156
-rw-r--r--sources/core/htmlparser/text.js70
-rw-r--r--sources/core/keystrokehandler.js169
-rw-r--r--sources/core/lang.js103
-rw-r--r--sources/core/loader.js225
-rw-r--r--sources/core/log.js127
-rw-r--r--sources/core/plugindefinition.js177
-rw-r--r--sources/core/plugins.js119
-rw-r--r--sources/core/resourcemanager.js228
-rw-r--r--sources/core/scriptloader.js203
-rw-r--r--sources/core/selection.js2183
-rw-r--r--sources/core/skin.js350
-rw-r--r--sources/core/style.js2089
-rw-r--r--sources/core/template.js68
-rw-r--r--sources/core/tools.js1386
-rw-r--r--sources/core/ui.js185
59 files changed, 31092 insertions, 0 deletions
diff --git a/sources/core/_bootstrap.js b/sources/core/_bootstrap.js
new file mode 100644
index 00000000..dac236bd
--- /dev/null
+++ b/sources/core/_bootstrap.js
@@ -0,0 +1,74 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview 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 00000000..948bc13b
--- /dev/null
+++ b/sources/core/ckeditor.js
@@ -0,0 +1,204 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview 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.
14delete 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 */
25CKEDITOR.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 */
34CKEDITOR.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 */
42CKEDITOR.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 */
69CKEDITOR.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
140CKEDITOR.on( 'instanceDestroyed', function() {
141 if ( CKEDITOR.tools.isEmpty( this.instances ) )
142 CKEDITOR.fire( 'reset' );
143} );
144
145// Load the bootstrap script.
146CKEDITOR.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 */
155CKEDITOR.TRISTATE_ON = 1;
156
157/**
158 * Used to indicate the OFF or INACTIVE state.
159 *
160 * @readonly
161 * @property {Number} [=2]
162 */
163CKEDITOR.TRISTATE_OFF = 2;
164
165/**
166 * Used to indicate the DISABLED state.
167 *
168 * @readonly
169 * @property {Number} [=0]
170 */
171CKEDITOR.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 00000000..10ae573d
--- /dev/null
+++ b/sources/core/ckeditor_base.js
@@ -0,0 +1,314 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview 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
19if ( !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 00000000..f34921dc
--- /dev/null
+++ b/sources/core/ckeditor_basic.js
@@ -0,0 +1,94 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview 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
12if ( 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 00000000..c4e8a7ae
--- /dev/null
+++ b/sources/core/command.js
@@ -0,0 +1,275 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * Represents a 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 */
24CKEDITOR.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
179CKEDITOR.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
250CKEDITOR.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 00000000..e09fee3f
--- /dev/null
+++ b/sources/core/commanddefinition.js
@@ -0,0 +1,148 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the "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 00000000..6bab846c
--- /dev/null
+++ b/sources/core/config.js
@@ -0,0 +1,451 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 */
24CKEDITOR.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 */
39CKEDITOR.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 */
54CKEDITOR.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 */
67CKEDITOR.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) &ndash; 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) &ndash; The same as the user interface language direction.
133 * * [Inline editor](#!/guide/dev_inline)&ndash; The same as the editable element text direction.
134 * * `'ltr'` &ndash; Indicates a Left-To-Right text direction (like in English).
135 * * `'rtl'` &ndash; 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) &ndash; New `<p>` paragraphs are created.
154 * * {@link CKEDITOR#ENTER_BR} (2) &ndash; Lines are broken with `<br>` elements.
155 * * {@link CKEDITOR#ENTER_DIV} (3) &ndash; 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) &ndash; New `<p>` paragraphs are created.
195 * * {@link CKEDITOR#ENTER_BR} (2) &ndash; Lines are broken with `<br>` elements.
196 * * {@link CKEDITOR#ENTER_DIV} (3) &ndash; 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 00000000..f6c3facd
--- /dev/null
+++ b/sources/core/creators/inline.js
@@ -0,0 +1,157 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /** @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 00000000..b8bfcf3d
--- /dev/null
+++ b/sources/core/creators/themedui.js
@@ -0,0 +1,541 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 /** @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 */
16CKEDITOR.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 */
487CKEDITOR.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 00000000..f3ca2746
--- /dev/null
+++ b/sources/core/dataprocessor.js
@@ -0,0 +1,70 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the "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 00000000..4e0e2563
--- /dev/null
+++ b/sources/core/dom.js
@@ -0,0 +1,13 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom} object, which contains DOM
8 * manipulation objects and function.
9 */
10
11CKEDITOR.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 00000000..b4e67833
--- /dev/null
+++ b/sources/core/dom/comment.js
@@ -0,0 +1,53 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.comment} class, which represents
8 * a DOM comment node.
9 */
10
11/**
12 * Represents a DOM comment node.
13 *
14 * var nativeNode = document.createComment( 'Example' );
15 * var comment = new CKEDITOR.dom.comment( nativeNode );
16 *
17 * var comment = new CKEDITOR.dom.comment( 'Example' );
18 *
19 * @class
20 * @extends CKEDITOR.dom.node
21 * @constructor Creates a comment class instance.
22 * @param {Object/String} comment A native DOM comment node or a string containing
23 * the text to use to create a new comment node.
24 * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
25 * the node in case of new node creation. Defaults to the current document.
26 */
27CKEDITOR.dom.comment = function( comment, ownerDocument ) {
28 if ( typeof comment == 'string' )
29 comment = ( ownerDocument ? ownerDocument.$ : document ).createComment( comment );
30
31 CKEDITOR.dom.domObject.call( this, comment );
32};
33
34CKEDITOR.dom.comment.prototype = new CKEDITOR.dom.node();
35
36CKEDITOR.tools.extend( CKEDITOR.dom.comment.prototype, {
37 /**
38 * The node type. This is a constant value set to {@link CKEDITOR#NODE_COMMENT}.
39 *
40 * @readonly
41 * @property {Number} [=CKEDITOR.NODE_COMMENT]
42 */
43 type: CKEDITOR.NODE_COMMENT,
44
45 /**
46 * Gets the outer HTML of this comment.
47 *
48 * @returns {String} The HTML `<!-- comment value -->`.
49 */
50 getOuterHtml: function() {
51 return '<!--' + this.$.nodeValue + '-->';
52 }
53} );
diff --git a/sources/core/dom/document.js b/sources/core/dom/document.js
new file mode 100644
index 00000000..ea0290f0
--- /dev/null
+++ b/sources/core/dom/document.js
@@ -0,0 +1,326 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.document} class which
8 * represents a DOM document.
9 */
10
11/**
12 * Represents a DOM document.
13 *
14 * var document = new CKEDITOR.dom.document( document );
15 *
16 * @class
17 * @extends CKEDITOR.dom.domObject
18 * @constructor Creates a document class instance.
19 * @param {Object} domDocument A native DOM document.
20 */
21CKEDITOR.dom.document = function( domDocument ) {
22 CKEDITOR.dom.domObject.call( this, domDocument );
23};
24
25// PACKAGER_RENAME( CKEDITOR.dom.document )
26
27CKEDITOR.dom.document.prototype = new CKEDITOR.dom.domObject();
28
29CKEDITOR.tools.extend( CKEDITOR.dom.document.prototype, {
30 /**
31 * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT}.
32 *
33 * @readonly
34 * @property {Number} [=CKEDITOR.NODE_DOCUMENT]
35 */
36 type: CKEDITOR.NODE_DOCUMENT,
37
38 /**
39 * Appends a CSS file to the document.
40 *
41 * CKEDITOR.document.appendStyleSheet( '/mystyles.css' );
42 *
43 * @param {String} cssFileUrl The CSS file URL.
44 */
45 appendStyleSheet: function( cssFileUrl ) {
46 if ( this.$.createStyleSheet )
47 this.$.createStyleSheet( cssFileUrl );
48 else {
49 var link = new CKEDITOR.dom.element( 'link' );
50 link.setAttributes( {
51 rel: 'stylesheet',
52 type: 'text/css',
53 href: cssFileUrl
54 } );
55
56 this.getHead().append( link );
57 }
58 },
59
60 /**
61 * Creates a CSS stylesheet and inserts it into the document.
62 *
63 * @param cssStyleText {String} CSS style text.
64 * @returns {Object} The created DOM native stylesheet object.
65 */
66 appendStyleText: function( cssStyleText ) {
67 if ( this.$.createStyleSheet ) {
68 var styleSheet = this.$.createStyleSheet( '' );
69 styleSheet.cssText = cssStyleText;
70 } else {
71 var style = new CKEDITOR.dom.element( 'style', this );
72 style.append( new CKEDITOR.dom.text( cssStyleText, this ) );
73 this.getHead().append( style );
74 }
75
76 return styleSheet || style.$.sheet;
77 },
78
79 /**
80 * Creates a {@link CKEDITOR.dom.element} instance in this document.
81 *
82 * @param {String} name The name of the element.
83 * @param {Object} [attributesAndStyles]
84 * @param {Object} [attributesAndStyles.attributes] Attributes that will be set.
85 * @param {Object} [attributesAndStyles.styles] Styles that will be set.
86 * @returns {CKEDITOR.dom.element}
87 */
88 createElement: function( name, attribsAndStyles ) {
89 var element = new CKEDITOR.dom.element( name, this );
90
91 if ( attribsAndStyles ) {
92 if ( attribsAndStyles.attributes )
93 element.setAttributes( attribsAndStyles.attributes );
94
95 if ( attribsAndStyles.styles )
96 element.setStyles( attribsAndStyles.styles );
97 }
98
99 return element;
100 },
101
102 /**
103 * Creates a {@link CKEDITOR.dom.text} instance in this document.
104 *
105 * @param {String} text Value of the text node.
106 * @returns {CKEDITOR.dom.element}
107 */
108 createText: function( text ) {
109 return new CKEDITOR.dom.text( text, this );
110 },
111
112 /**
113 * Moves the selection focus to this document's window.
114 */
115 focus: function() {
116 this.getWindow().focus();
117 },
118
119 /**
120 * Returns the element that is currently designated as the active element in the document.
121 *
122 * **Note:** Only one element can be active at a time in a document.
123 * An active element does not necessarily have focus,
124 * but an element with focus is always the active element in a document.
125 *
126 * @returns {CKEDITOR.dom.element} Active element or `null` if an IE8-9 bug is encountered.
127 * See [#10030](http://dev.ckeditor.com/ticket/10030).
128 */
129 getActive: function() {
130 var $active;
131 try {
132 $active = this.$.activeElement;
133 } catch ( e ) {
134 return null;
135 }
136 return new CKEDITOR.dom.element( $active );
137 },
138
139 /**
140 * Gets an element based on its ID.
141 *
142 * var element = CKEDITOR.document.getById( 'myElement' );
143 * alert( element.getId() ); // 'myElement'
144 *
145 * @param {String} elementId The element ID.
146 * @returns {CKEDITOR.dom.element} The element instance, or `null` if not found.
147 */
148 getById: function( elementId ) {
149 var $ = this.$.getElementById( elementId );
150 return $ ? new CKEDITOR.dom.element( $ ) : null;
151 },
152
153 /**
154 * Gets a node based on its address. See {@link CKEDITOR.dom.node#getAddress}.
155 *
156 * @param {Array} address
157 * @param {Boolean} [normalized=false]
158 */
159 getByAddress: function( address, normalized ) {
160 var $ = this.$.documentElement;
161
162 for ( var i = 0; $ && i < address.length; i++ ) {
163 var target = address[ i ];
164
165 if ( !normalized ) {
166 $ = $.childNodes[ target ];
167 continue;
168 }
169
170 var currentIndex = -1;
171
172 for ( var j = 0; j < $.childNodes.length; j++ ) {
173 var candidate = $.childNodes[ j ];
174
175 if ( normalized === true && candidate.nodeType == 3 && candidate.previousSibling && candidate.previousSibling.nodeType == 3 )
176 continue;
177
178 currentIndex++;
179
180 if ( currentIndex == target ) {
181 $ = candidate;
182 break;
183 }
184 }
185 }
186
187 return $ ? new CKEDITOR.dom.node( $ ) : null;
188 },
189
190 /**
191 * Gets elements list based on a given tag name.
192 *
193 * @param {String} tagName The element tag name.
194 * @returns {CKEDITOR.dom.nodeList} The nodes list.
195 */
196 getElementsByTag: function( tagName, namespace ) {
197 if ( !( CKEDITOR.env.ie && ( document.documentMode <= 8 ) ) && namespace )
198 tagName = namespace + ':' + tagName;
199 return new CKEDITOR.dom.nodeList( this.$.getElementsByTagName( tagName ) );
200 },
201
202 /**
203 * Gets the `<head>` element for this document.
204 *
205 * var element = CKEDITOR.document.getHead();
206 * alert( element.getName() ); // 'head'
207 *
208 * @returns {CKEDITOR.dom.element} The `<head>` element.
209 */
210 getHead: function() {
211 var head = this.$.getElementsByTagName( 'head' )[ 0 ];
212 if ( !head )
213 head = this.getDocumentElement().append( new CKEDITOR.dom.element( 'head' ), true );
214 else
215 head = new CKEDITOR.dom.element( head );
216
217 return head;
218 },
219
220 /**
221 * Gets the `<body>` element for this document.
222 *
223 * var element = CKEDITOR.document.getBody();
224 * alert( element.getName() ); // 'body'
225 *
226 * @returns {CKEDITOR.dom.element} The `<body>` element.
227 */
228 getBody: function() {
229 return new CKEDITOR.dom.element( this.$.body );
230 },
231
232 /**
233 * Gets the DOM document element for this document.
234 *
235 * @returns {CKEDITOR.dom.element} The DOM document element.
236 */
237 getDocumentElement: function() {
238 return new CKEDITOR.dom.element( this.$.documentElement );
239 },
240
241 /**
242 * Gets the window object that stores this document.
243 *
244 * @returns {CKEDITOR.dom.window} The window object.
245 */
246 getWindow: function() {
247 return new CKEDITOR.dom.window( this.$.parentWindow || this.$.defaultView );
248 },
249
250 /**
251 * Defines the document content through `document.write`. Note that the
252 * previous document content will be lost (cleaned).
253 *
254 * document.write(
255 * '<html>' +
256 * '<head><title>Sample Document</title></head>' +
257 * '<body>Document content created by code.</body>' +
258 * '</html>'
259 * );
260 *
261 * @since 3.5
262 * @param {String} html The HTML defining the document content.
263 */
264 write: function( html ) {
265 // Don't leave any history log in IE. (#5657)
266 this.$.open( 'text/html', 'replace' );
267
268 // Support for custom document.domain in IE.
269 //
270 // The script must be appended because if placed before the
271 // doctype, IE will go into quirks mode and mess with
272 // the editable, e.g. by changing its default height.
273 if ( CKEDITOR.env.ie )
274 html = html.replace( /(?:^\s*<!DOCTYPE[^>]*?>)|^/i, '$&\n<script data-cke-temp="1">(' + CKEDITOR.tools.fixDomain + ')();</script>' );
275
276 this.$.write( html );
277 this.$.close();
278 },
279
280 /**
281 * Wrapper for `querySelectorAll`. Returns a list of elements within this document that match
282 * the specified `selector`.
283 *
284 * **Note:** The returned list is not a live collection (like the result of native `querySelectorAll`).
285 *
286 * @since 4.3
287 * @param {String} selector
288 * @returns {CKEDITOR.dom.nodeList}
289 */
290 find: function( selector ) {
291 return new CKEDITOR.dom.nodeList( this.$.querySelectorAll( selector ) );
292 },
293
294 /**
295 * Wrapper for `querySelector`. Returns the first element within this document that matches
296 * the specified `selector`.
297 *
298 * @since 4.3
299 * @param {String} selector
300 * @returns {CKEDITOR.dom.element}
301 */
302 findOne: function( selector ) {
303 var el = this.$.querySelector( selector );
304
305 return el ? new CKEDITOR.dom.element( el ) : null;
306 },
307
308 /**
309 * Internet Explorer 8 only method. It returns a document fragment which has all HTML5 elements enabled.
310 *
311 * @since 4.3
312 * @private
313 * @returns DocumentFragment
314 */
315 _getHtml5ShivFrag: function() {
316 var $frag = this.getCustomData( 'html5ShivFrag' );
317
318 if ( !$frag ) {
319 $frag = this.$.createDocumentFragment();
320 CKEDITOR.tools.enableHtml5Elements( $frag, true );
321 this.setCustomData( 'html5ShivFrag', $frag );
322 }
323
324 return $frag;
325 }
326} );
diff --git a/sources/core/dom/documentfragment.js b/sources/core/dom/documentfragment.js
new file mode 100644
index 00000000..5015144d
--- /dev/null
+++ b/sources/core/dom/documentfragment.js
@@ -0,0 +1,62 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * DocumentFragment is a "lightweight" or "minimal" Document object. It is
8 * commonly used to extract a portion of a document's tree or to create a new
9 * fragment of a document. Various operations may take document fragment objects
10 * as arguments and result in all the child nodes of the document fragment being
11 * moved to the child list of this node.
12 *
13 * @class
14 * @constructor Creates a document fragment class instance.
15 * @param {CKEDITOR.dom.document/DocumentFragment} [nodeOrDoc=CKEDITOR.document]
16 */
17CKEDITOR.dom.documentFragment = function( nodeOrDoc ) {
18 nodeOrDoc = nodeOrDoc || CKEDITOR.document;
19
20 if ( nodeOrDoc.type == CKEDITOR.NODE_DOCUMENT )
21 this.$ = nodeOrDoc.$.createDocumentFragment();
22 else
23 this.$ = nodeOrDoc;
24};
25
26CKEDITOR.tools.extend( CKEDITOR.dom.documentFragment.prototype, CKEDITOR.dom.element.prototype, {
27 /**
28 * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
29 *
30 * @readonly
31 * @property {Number} [=CKEDITOR.NODE_DOCUMENT_FRAGMENT]
32 */
33 type: CKEDITOR.NODE_DOCUMENT_FRAGMENT,
34
35 /**
36 * Inserts the document fragment content after the specified node.
37 *
38 * @param {CKEDITOR.dom.node} node
39 */
40 insertAfterNode: function( node ) {
41 node = node.$;
42 node.parentNode.insertBefore( this.$, node.nextSibling );
43 },
44
45 /**
46 * Gets HTML of this document fragment's children.
47 *
48 * @since 4.5
49 * @returns {String} The HTML of this document fragment's children.
50 */
51 getHtml: function() {
52 var container = new CKEDITOR.dom.element( 'div' );
53
54 this.clone( 1, 1 ).appendTo( container );
55
56 return container.getHtml().replace( /\s*data-cke-expando=".*?"/g, '' );
57 }
58}, true, {
59 'append': 1, 'appendBogus': 1, 'clone': 1, 'getFirst': 1, 'getHtml': 1, 'getLast': 1, 'getParent': 1, 'getNext': 1, 'getPrevious': 1,
60 'appendTo': 1, 'moveChildren': 1, 'insertBefore': 1, 'insertAfterNode': 1, 'replace': 1, 'trim': 1, 'type': 1,
61 'ltrim': 1, 'rtrim': 1, 'getDocument': 1, 'getChildCount': 1, 'getChild': 1, 'getChildren': 1
62} );
diff --git a/sources/core/dom/domobject.js b/sources/core/dom/domobject.js
new file mode 100644
index 00000000..21a351d7
--- /dev/null
+++ b/sources/core/dom/domobject.js
@@ -0,0 +1,266 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.editor} class, which is the base
8 * for other classes representing DOM objects.
9 */
10
11/**
12 * Represents a DOM object. This class is not intended to be used directly. It
13 * serves as the base class for other classes representing specific DOM
14 * objects.
15 *
16 * @class
17 * @mixins CKEDITOR.event
18 * @constructor Creates a domObject class instance.
19 * @param {Object} nativeDomObject A native DOM object.
20 */
21CKEDITOR.dom.domObject = function( nativeDomObject ) {
22 if ( nativeDomObject ) {
23 /**
24 * The native DOM object represented by this class instance.
25 *
26 * var element = new CKEDITOR.dom.element( 'span' );
27 * alert( element.$.nodeType ); // '1'
28 *
29 * @readonly
30 * @property {Object}
31 */
32 this.$ = nativeDomObject;
33 }
34};
35
36CKEDITOR.dom.domObject.prototype = ( function() {
37 // Do not define other local variables here. We want to keep the native
38 // listener closures as clean as possible.
39
40 var getNativeListener = function( domObject, eventName ) {
41 return function( domEvent ) {
42 // In FF, when reloading the page with the editor focused, it may
43 // throw an error because the CKEDITOR global is not anymore
44 // available. So, we check it here first. (#2923)
45 if ( typeof CKEDITOR != 'undefined' )
46 domObject.fire( eventName, new CKEDITOR.dom.event( domEvent ) );
47 };
48 };
49
50 return {
51
52 /**
53 * Gets the private `_` object which is bound to the native
54 * DOM object using {@link #getCustomData}.
55 *
56 * var elementA = new CKEDITOR.dom.element( nativeElement );
57 * elementA.getPrivate().value = 1;
58 * ...
59 * var elementB = new CKEDITOR.dom.element( nativeElement );
60 * elementB.getPrivate().value; // 1
61 *
62 * @returns {Object} The private object.
63 */
64 getPrivate: function() {
65 var priv;
66
67 // Get the main private object from the custom data. Create it if not defined.
68 if ( !( priv = this.getCustomData( '_' ) ) )
69 this.setCustomData( '_', ( priv = {} ) );
70
71 return priv;
72 },
73
74 // Docs inherited from event.
75 on: function( eventName ) {
76 // We customize the "on" function here. The basic idea is that we'll have
77 // only one listener for a native event, which will then call all listeners
78 // set to the event.
79
80 // Get the listeners holder object.
81 var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
82
83 if ( !nativeListeners ) {
84 nativeListeners = {};
85 this.setCustomData( '_cke_nativeListeners', nativeListeners );
86 }
87
88 // Check if we have a listener for that event.
89 if ( !nativeListeners[ eventName ] ) {
90 var listener = nativeListeners[ eventName ] = getNativeListener( this, eventName );
91
92 if ( this.$.addEventListener )
93 this.$.addEventListener( eventName, listener, !!CKEDITOR.event.useCapture );
94 else if ( this.$.attachEvent )
95 this.$.attachEvent( 'on' + eventName, listener );
96 }
97
98 // Call the original implementation.
99 return CKEDITOR.event.prototype.on.apply( this, arguments );
100 },
101
102 // Docs inherited from event.
103 removeListener: function( eventName ) {
104 // Call the original implementation.
105 CKEDITOR.event.prototype.removeListener.apply( this, arguments );
106
107 // If we don't have listeners for this event, clean the DOM up.
108 if ( !this.hasListeners( eventName ) ) {
109 var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
110 var listener = nativeListeners && nativeListeners[ eventName ];
111 if ( listener ) {
112 if ( this.$.removeEventListener )
113 this.$.removeEventListener( eventName, listener, false );
114 else if ( this.$.detachEvent )
115 this.$.detachEvent( 'on' + eventName, listener );
116
117 delete nativeListeners[ eventName ];
118 }
119 }
120 },
121
122 /**
123 * Removes any listener set on this object.
124 *
125 * To avoid memory leaks we must assure that there are no
126 * references left after the object is no longer needed.
127 */
128 removeAllListeners: function() {
129 var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
130 for ( var eventName in nativeListeners ) {
131 var listener = nativeListeners[ eventName ];
132 if ( this.$.detachEvent )
133 this.$.detachEvent( 'on' + eventName, listener );
134 else if ( this.$.removeEventListener )
135 this.$.removeEventListener( eventName, listener, false );
136
137 delete nativeListeners[ eventName ];
138 }
139
140 // Remove events from events object so fire() method will not call
141 // listeners (#11400).
142 CKEDITOR.event.prototype.removeAllListeners.call( this );
143 }
144 };
145} )();
146
147( function( domObjectProto ) {
148 var customData = {};
149
150 CKEDITOR.on( 'reset', function() {
151 customData = {};
152 } );
153
154 /**
155 * Determines whether the specified object is equal to the current object.
156 *
157 * var doc = new CKEDITOR.dom.document( document );
158 * alert( doc.equals( CKEDITOR.document ) ); // true
159 * alert( doc == CKEDITOR.document ); // false
160 *
161 * @param {Object} object The object to compare with the current object.
162 * @returns {Boolean} `true` if the object is equal.
163 */
164 domObjectProto.equals = function( object ) {
165 // Try/Catch to avoid IE permission error when object is from different document.
166 try {
167 return ( object && object.$ === this.$ );
168 } catch ( er ) {
169 return false;
170 }
171 };
172
173 /**
174 * Sets a data slot value for this object. These values are shared by all
175 * instances pointing to that same DOM object.
176 *
177 * **Note:** The created data slot is only guaranteed to be available on this unique DOM node,
178 * thus any wish to continue access to it from other element clones (either created by
179 * clone node or from `innerHtml`) will fail. For such usage please use
180 * {@link CKEDITOR.dom.element#setAttribute} instead.
181 *
182 * **Note**: This method does not work on text nodes prior to Internet Explorer 9.
183 *
184 * var element = new CKEDITOR.dom.element( 'span' );
185 * element.setCustomData( 'hasCustomData', true );
186 *
187 * @param {String} key A key used to identify the data slot.
188 * @param {Object} value The value to set to the data slot.
189 * @returns {CKEDITOR.dom.domObject} This DOM object instance.
190 * @chainable
191 */
192 domObjectProto.setCustomData = function( key, value ) {
193 var expandoNumber = this.getUniqueId(),
194 dataSlot = customData[ expandoNumber ] || ( customData[ expandoNumber ] = {} );
195
196 dataSlot[ key ] = value;
197
198 return this;
199 };
200
201 /**
202 * Gets the value set to a data slot in this object.
203 *
204 * var element = new CKEDITOR.dom.element( 'span' );
205 * alert( element.getCustomData( 'hasCustomData' ) ); // e.g. 'true'
206 * alert( element.getCustomData( 'nonExistingKey' ) ); // null
207 *
208 * @param {String} key The key used to identify the data slot.
209 * @returns {Object} This value set to the data slot.
210 */
211 domObjectProto.getCustomData = function( key ) {
212 var expandoNumber = this.$[ 'data-cke-expando' ],
213 dataSlot = expandoNumber && customData[ expandoNumber ];
214
215 return ( dataSlot && key in dataSlot ) ? dataSlot[ key ] : null;
216 };
217
218 /**
219 * Removes the value in the data slot under the given `key`.
220 *
221 * @param {String} key
222 * @returns {Object} Removed value or `null` if not found.
223 */
224 domObjectProto.removeCustomData = function( key ) {
225 var expandoNumber = this.$[ 'data-cke-expando' ],
226 dataSlot = expandoNumber && customData[ expandoNumber ],
227 retval, hadKey;
228
229 if ( dataSlot ) {
230 retval = dataSlot[ key ];
231 hadKey = key in dataSlot;
232 delete dataSlot[ key ];
233 }
234
235 return hadKey ? retval : null;
236 };
237
238 /**
239 * Removes any data stored in this object.
240 * To avoid memory leaks we must assure that there are no
241 * references left after the object is no longer needed.
242 */
243 domObjectProto.clearCustomData = function() {
244 // Clear all event listeners
245 this.removeAllListeners();
246
247 var expandoNumber = this.$[ 'data-cke-expando' ];
248 expandoNumber && delete customData[ expandoNumber ];
249 };
250
251 /**
252 * Gets an ID that can be used to identify this DOM object in
253 * the running session.
254 *
255 * **Note**: This method does not work on text nodes prior to Internet Explorer 9.
256 *
257 * @returns {Number} A unique ID.
258 */
259 domObjectProto.getUniqueId = function() {
260 return this.$[ 'data-cke-expando' ] || ( this.$[ 'data-cke-expando' ] = CKEDITOR.tools.getNextNumber() );
261 };
262
263 // Implement CKEDITOR.event.
264 CKEDITOR.event.implementOn( domObjectProto );
265
266} )( CKEDITOR.dom.domObject.prototype );
diff --git a/sources/core/dom/element.js b/sources/core/dom/element.js
new file mode 100644
index 00000000..d5181282
--- /dev/null
+++ b/sources/core/dom/element.js
@@ -0,0 +1,2107 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.element} class, which
8 * represents a DOM element.
9 */
10
11/**
12 * Represents a DOM element.
13 *
14 * // Create a new <span> element.
15 * var element = new CKEDITOR.dom.element( 'span' );
16 *
17 * // Create an element based on a native DOM element.
18 * var element = new CKEDITOR.dom.element( document.getElementById( 'myId' ) );
19 *
20 * @class
21 * @extends CKEDITOR.dom.node
22 * @constructor Creates an element class instance.
23 * @param {Object/String} element A native DOM element or the element name for
24 * new elements.
25 * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
26 * the element in case of element creation.
27 */
28CKEDITOR.dom.element = function( element, ownerDocument ) {
29 if ( typeof element == 'string' )
30 element = ( ownerDocument ? ownerDocument.$ : document ).createElement( element );
31
32 // Call the base constructor (we must not call CKEDITOR.dom.node).
33 CKEDITOR.dom.domObject.call( this, element );
34};
35
36// PACKAGER_RENAME( CKEDITOR.dom.element )
37/**
38 * The the {@link CKEDITOR.dom.element} representing and element. If the
39 * element is a native DOM element, it will be transformed into a valid
40 * CKEDITOR.dom.element object.
41 *
42 * var element = new CKEDITOR.dom.element( 'span' );
43 * alert( element == CKEDITOR.dom.element.get( element ) ); // true
44 *
45 * var element = document.getElementById( 'myElement' );
46 * alert( CKEDITOR.dom.element.get( element ).getName() ); // (e.g.) 'p'
47 *
48 * @static
49 * @param {String/Object} element Element's id or name or native DOM element.
50 * @returns {CKEDITOR.dom.element} The transformed element.
51 */
52CKEDITOR.dom.element.get = function( element ) {
53 var el = typeof element == 'string' ? document.getElementById( element ) || document.getElementsByName( element )[ 0 ] : element;
54
55 return el && ( el.$ ? el : new CKEDITOR.dom.element( el ) );
56};
57
58CKEDITOR.dom.element.prototype = new CKEDITOR.dom.node();
59
60/**
61 * Creates an instance of the {@link CKEDITOR.dom.element} class based on the
62 * HTML representation of an element.
63 *
64 * var element = CKEDITOR.dom.element.createFromHtml( '<strong class="anyclass">My element</strong>' );
65 * alert( element.getName() ); // 'strong'
66 *
67 * @static
68 * @param {String} html The element HTML. It should define only one element in
69 * the "root" level. The "root" element can have child nodes, but not siblings.
70 * @returns {CKEDITOR.dom.element} The element instance.
71 */
72CKEDITOR.dom.element.createFromHtml = function( html, ownerDocument ) {
73 var temp = new CKEDITOR.dom.element( 'div', ownerDocument );
74 temp.setHtml( html );
75
76 // When returning the node, remove it from its parent to detach it.
77 return temp.getFirst().remove();
78};
79
80/**
81 * Sets {@link CKEDITOR.dom.element#setCustomData custom data} on an element in a way that it is later
82 * possible to {@link #clearAllMarkers clear all data} set on all elements sharing the same database.
83 *
84 * This mechanism is very useful when processing some portion of DOM. All markers can later be removed
85 * by calling the {@link #clearAllMarkers} method, hence markers will not leak to second pass of this algorithm.
86 *
87 * var database = {};
88 * CKEDITOR.dom.element.setMarker( database, element1, 'foo', 'bar' );
89 * CKEDITOR.dom.element.setMarker( database, element2, 'oof', [ 1, 2, 3 ] );
90 *
91 * element1.getCustomData( 'foo' ); // 'bar'
92 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
93 *
94 * CKEDITOR.dom.element.clearAllMarkers( database );
95 *
96 * element1.getCustomData( 'foo' ); // null
97 *
98 * @static
99 * @param {Object} database
100 * @param {CKEDITOR.dom.element} element
101 * @param {String} name
102 * @param {Object} value
103 * @returns {CKEDITOR.dom.element} The element.
104 */
105CKEDITOR.dom.element.setMarker = function( database, element, name, value ) {
106 var id = element.getCustomData( 'list_marker_id' ) || ( element.setCustomData( 'list_marker_id', CKEDITOR.tools.getNextNumber() ).getCustomData( 'list_marker_id' ) ),
107 markerNames = element.getCustomData( 'list_marker_names' ) || ( element.setCustomData( 'list_marker_names', {} ).getCustomData( 'list_marker_names' ) );
108 database[ id ] = element;
109 markerNames[ name ] = 1;
110
111 return element.setCustomData( name, value );
112};
113
114/**
115 * Removes all markers added using this database. See the {@link #setMarker} method for more information.
116 *
117 * @param {Object} database
118 * @static
119 */
120CKEDITOR.dom.element.clearAllMarkers = function( database ) {
121 for ( var i in database )
122 CKEDITOR.dom.element.clearMarkers( database, database[ i ], 1 );
123};
124
125/**
126 * Removes all markers added to this element and removes it from the database if
127 * `removeFromDatabase` was passed. See the {@link #setMarker} method for more information.
128 *
129 * var database = {};
130 * CKEDITOR.dom.element.setMarker( database, element1, 'foo', 'bar' );
131 * CKEDITOR.dom.element.setMarker( database, element2, 'oof', [ 1, 2, 3 ] );
132 *
133 * element1.getCustomData( 'foo' ); // 'bar'
134 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
135 *
136 * CKEDITOR.dom.element.clearMarkers( database, element1, true );
137 *
138 * element1.getCustomData( 'foo' ); // null
139 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
140 *
141 * @param {Object} database
142 * @static
143 */
144CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatabase ) {
145 var names = element.getCustomData( 'list_marker_names' ),
146 id = element.getCustomData( 'list_marker_id' );
147 for ( var i in names )
148 element.removeCustomData( i );
149 element.removeCustomData( 'list_marker_names' );
150 if ( removeFromDatabase ) {
151 element.removeCustomData( 'list_marker_id' );
152 delete database[ id ];
153 }
154};
155
156( function() {
157 var elementsClassList = document.createElement( '_' ).classList,
158 supportsClassLists = typeof elementsClassList !== 'undefined' && String( elementsClassList.add ).match( /\[Native code\]/gi ) !== null,
159 rclass = /[\n\t\r]/g;
160
161 function hasClass( classNames, className ) {
162 // Source: jQuery.
163 return ( ' ' + classNames + ' ' ).replace( rclass, ' ' ).indexOf( ' ' + className + ' ' ) > -1;
164 }
165
166 CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, {
167 /**
168 * The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}.
169 *
170 * @readonly
171 * @property {Number} [=CKEDITOR.NODE_ELEMENT]
172 */
173 type: CKEDITOR.NODE_ELEMENT,
174
175 /**
176 * Adds a CSS class to the element. It appends the class to the
177 * already existing names.
178 *
179 * var element = new CKEDITOR.dom.element( 'div' );
180 * element.addClass( 'classA' ); // <div class="classA">
181 * element.addClass( 'classB' ); // <div class="classA classB">
182 * element.addClass( 'classA' ); // <div class="classA classB">
183 *
184 * **Note:** Since CKEditor 4.5 this method cannot be used with multiple classes (`'classA classB'`).
185 *
186 * @chainable
187 * @method addClass
188 * @param {String} className The name of the class to be added.
189 */
190 addClass: supportsClassLists ?
191 function( className ) {
192 this.$.classList.add( className );
193
194 return this;
195 } : function( className ) {
196 var c = this.$.className;
197 if ( c ) {
198 if ( !hasClass( c, className ) )
199 c += ' ' + className;
200 }
201 this.$.className = c || className;
202
203 return this;
204 },
205
206 /**
207 * Removes a CSS class name from the elements classes. Other classes
208 * remain untouched.
209 *
210 * var element = new CKEDITOR.dom.element( 'div' );
211 * element.addClass( 'classA' ); // <div class="classA">
212 * element.addClass( 'classB' ); // <div class="classA classB">
213 * element.removeClass( 'classA' ); // <div class="classB">
214 * element.removeClass( 'classB' ); // <div>
215 *
216 * @chainable
217 * @method removeClass
218 * @param {String} className The name of the class to remove.
219 */
220 removeClass: supportsClassLists ?
221 function( className ) {
222 var $ = this.$;
223 $.classList.remove( className );
224
225 if ( !$.className )
226 $.removeAttribute( 'class' );
227
228 return this;
229 } : function( className ) {
230 var c = this.getAttribute( 'class' );
231 if ( c && hasClass( c, className ) ) {
232 c = c
233 .replace( new RegExp( '(?:^|\\s+)' + className + '(?=\\s|$)' ), '' )
234 .replace( /^\s+/, '' );
235
236 if ( c )
237 this.setAttribute( 'class', c );
238 else
239 this.removeAttribute( 'class' );
240 }
241
242 return this;
243 },
244
245 /**
246 * Checks if element has class name.
247 *
248 * @param {String} className
249 * @returns {Boolean}
250 */
251 hasClass: function( className ) {
252 return hasClass( this.$.className, className );
253 },
254
255 /**
256 * Append a node as a child of this element.
257 *
258 * var p = new CKEDITOR.dom.element( 'p' );
259 *
260 * var strong = new CKEDITOR.dom.element( 'strong' );
261 * p.append( strong );
262 *
263 * var em = p.append( 'em' );
264 *
265 * // Result: '<p><strong></strong><em></em></p>'
266 *
267 * @param {CKEDITOR.dom.node/String} node The node or element name to be appended.
268 * @param {Boolean} [toStart=false] Indicates that the element is to be appended at the start.
269 * @returns {CKEDITOR.dom.node} The appended node.
270 */
271 append: function( node, toStart ) {
272 if ( typeof node == 'string' )
273 node = this.getDocument().createElement( node );
274
275 if ( toStart )
276 this.$.insertBefore( node.$, this.$.firstChild );
277 else
278 this.$.appendChild( node.$ );
279
280 return node;
281 },
282
283 /**
284 * Append HTML as a child(ren) of this element.
285 *
286 * @param {String} html
287 */
288 appendHtml: function( html ) {
289 if ( !this.$.childNodes.length )
290 this.setHtml( html );
291 else {
292 var temp = new CKEDITOR.dom.element( 'div', this.getDocument() );
293 temp.setHtml( html );
294 temp.moveChildren( this );
295 }
296 },
297
298 /**
299 * Append text to this element.
300 *
301 * var p = new CKEDITOR.dom.element( 'p' );
302 * p.appendText( 'This is' );
303 * p.appendText( ' some text' );
304 *
305 * // Result: '<p>This is some text</p>'
306 *
307 * @param {String} text The text to be appended.
308 */
309 appendText: function( text ) {
310 // On IE8 it is impossible to append node to script tag, so we use its text.
311 // On the contrary, on Safari the text property is unpredictable in links. (#13232)
312 if ( this.$.text != null && CKEDITOR.env.ie && CKEDITOR.env.version < 9 )
313 this.$.text += text;
314 else
315 this.append( new CKEDITOR.dom.text( text ) );
316 },
317
318 /**
319 * Appends a `<br>` filler element to this element if the filler is not present already.
320 * By default filler is appended only if {@link CKEDITOR.env#needsBrFiller} is `true`,
321 * however when `force` is set to `true` filler will be appended regardless of the environment.
322 *
323 * @param {Boolean} [force] Append filler regardless of the environment.
324 */
325 appendBogus: function( force ) {
326 if ( !force && !CKEDITOR.env.needsBrFiller )
327 return;
328
329 var lastChild = this.getLast();
330
331 // Ignore empty/spaces text.
332 while ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.rtrim( lastChild.getText() ) )
333 lastChild = lastChild.getPrevious();
334 if ( !lastChild || !lastChild.is || !lastChild.is( 'br' ) ) {
335 var bogus = this.getDocument().createElement( 'br' );
336
337 CKEDITOR.env.gecko && bogus.setAttribute( 'type', '_moz' );
338
339 this.append( bogus );
340 }
341 },
342
343 /**
344 * Breaks one of the ancestor element in the element position, moving
345 * this element between the broken parts.
346 *
347 * // Before breaking:
348 * // <b>This <i>is some<span /> sample</i> test text</b>
349 * // If "element" is <span /> and "parent" is <i>:
350 * // <b>This <i>is some</i><span /><i> sample</i> test text</b>
351 * element.breakParent( parent );
352 *
353 * // Before breaking:
354 * // <b>This <i>is some<span /> sample</i> test text</b>
355 * // If "element" is <span /> and "parent" is <b>:
356 * // <b>This <i>is some</i></b><span /><b><i> sample</i> test text</b>
357 * element.breakParent( parent );
358 *
359 * @param {CKEDITOR.dom.element} parent The anscestor element to get broken.
360 * @param {Boolean} [cloneId=false] Whether to preserve ancestor ID attributes while breaking.
361 */
362 breakParent: function( parent, cloneId ) {
363 var range = new CKEDITOR.dom.range( this.getDocument() );
364
365 // We'll be extracting part of this element, so let's use our
366 // range to get the correct piece.
367 range.setStartAfter( this );
368 range.setEndAfter( parent );
369
370 // Extract it.
371 var docFrag = range.extractContents( false, cloneId || false );
372
373 // Move the element outside the broken element.
374 range.insertNode( this.remove() );
375
376 // Re-insert the extracted piece after the element.
377 docFrag.insertAfterNode( this );
378 },
379
380 /**
381 * Checks if this element contains given node.
382 *
383 * @method
384 * @param {CKEDITOR.dom.node} node
385 * @returns {Boolean}
386 */
387 contains: !document.compareDocumentPosition ?
388 function( node ) {
389 var $ = this.$;
390
391 return node.type != CKEDITOR.NODE_ELEMENT ? $.contains( node.getParent().$ ) : $ != node.$ && $.contains( node.$ );
392 } : function( node ) {
393 return !!( this.$.compareDocumentPosition( node.$ ) & 16 );
394 },
395
396 /**
397 * Moves the selection focus to this element.
398 *
399 * var element = CKEDITOR.document.getById( 'myTextarea' );
400 * element.focus();
401 *
402 * @method
403 * @param {Boolean} defer Whether to asynchronously defer the
404 * execution by 100 ms.
405 */
406 focus: ( function() {
407 function exec() {
408 // IE throws error if the element is not visible.
409 try {
410 this.$.focus();
411 } catch ( e ) {}
412 }
413
414 return function( defer ) {
415 if ( defer )
416 CKEDITOR.tools.setTimeout( exec, 100, this );
417 else
418 exec.call( this );
419 };
420 } )(),
421
422 /**
423 * Gets the inner HTML of this element.
424 *
425 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
426 * alert( element.getHtml() ); // '<b>Example</b>'
427 *
428 * @returns {String} The inner HTML of this element.
429 */
430 getHtml: function() {
431 var retval = this.$.innerHTML;
432 // Strip <?xml:namespace> tags in IE. (#3341).
433 return CKEDITOR.env.ie ? retval.replace( /<\?[^>]*>/g, '' ) : retval;
434 },
435
436 /**
437 * Gets the outer (inner plus tags) HTML of this element.
438 *
439 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="bold"><b>Example</b></div>' );
440 * alert( element.getOuterHtml() ); // '<div class="bold"><b>Example</b></div>'
441 *
442 * @returns {String} The outer HTML of this element.
443 */
444 getOuterHtml: function() {
445 if ( this.$.outerHTML ) {
446 // IE includes the <?xml:namespace> tag in the outerHTML of
447 // namespaced element. So, we must strip it here. (#3341)
448 return this.$.outerHTML.replace( /<\?[^>]*>/, '' );
449 }
450
451 var tmpDiv = this.$.ownerDocument.createElement( 'div' );
452 tmpDiv.appendChild( this.$.cloneNode( true ) );
453 return tmpDiv.innerHTML;
454 },
455
456 /**
457 * Retrieve the bounding rectangle of the current element, in pixels,
458 * relative to the upper-left corner of the browser's client area.
459 *
460 * @returns {Object} The dimensions of the DOM element including
461 * `left`, `top`, `right`, `bottom`, `width` and `height`.
462 */
463 getClientRect: function() {
464 // http://help.dottoro.com/ljvmcrrn.php
465 var rect = CKEDITOR.tools.extend( {}, this.$.getBoundingClientRect() );
466
467 !rect.width && ( rect.width = rect.right - rect.left );
468 !rect.height && ( rect.height = rect.bottom - rect.top );
469
470 return rect;
471 },
472
473 /**
474 * Sets the inner HTML of this element.
475 *
476 * var p = new CKEDITOR.dom.element( 'p' );
477 * p.setHtml( '<b>Inner</b> HTML' );
478 *
479 * // Result: '<p><b>Inner</b> HTML</p>'
480 *
481 * @method
482 * @param {String} html The HTML to be set for this element.
483 * @returns {String} The inserted HTML.
484 */
485 setHtml: ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) ?
486 // old IEs throws error on HTML manipulation (through the "innerHTML" property)
487 // on the element which resides in an DTD invalid position, e.g. <span><div></div></span>
488 // fortunately it can be worked around with DOM manipulation.
489 function( html ) {
490 try {
491 var $ = this.$;
492
493 // Fix the case when setHtml is called on detached element.
494 // HTML5 shiv used for document in which this element was created
495 // won't affect that detached element. So get document fragment with
496 // all HTML5 elements enabled and set innerHTML while this element is appended to it.
497 if ( this.getParent() )
498 return ( $.innerHTML = html );
499 else {
500 var $frag = this.getDocument()._getHtml5ShivFrag();
501 $frag.appendChild( $ );
502 $.innerHTML = html;
503 $frag.removeChild( $ );
504
505 return html;
506 }
507 }
508 catch ( e ) {
509 this.$.innerHTML = '';
510
511 var temp = new CKEDITOR.dom.element( 'body', this.getDocument() );
512 temp.$.innerHTML = html;
513
514 var children = temp.getChildren();
515 while ( children.count() )
516 this.append( children.getItem( 0 ) );
517
518 return html;
519 }
520 } : function( html ) {
521 return ( this.$.innerHTML = html );
522 },
523
524 /**
525 * Sets the element contents as plain text.
526 *
527 * var element = new CKEDITOR.dom.element( 'div' );
528 * element.setText( 'A > B & C < D' );
529 * alert( element.innerHTML ); // 'A &gt; B &amp; C &lt; D'
530 *
531 * @param {String} text The text to be set.
532 * @returns {String} The inserted text.
533 */
534 setText: ( function() {
535 var supportsTextContent = document.createElement( 'p' );
536 supportsTextContent.innerHTML = 'x';
537 supportsTextContent = supportsTextContent.textContent;
538
539 return function( text ) {
540 this.$[ supportsTextContent ? 'textContent' : 'innerText' ] = text;
541 };
542 } )(),
543
544 /**
545 * Gets the value of an element attribute.
546 *
547 * var element = CKEDITOR.dom.element.createFromHtml( '<input type="text" />' );
548 * alert( element.getAttribute( 'type' ) ); // 'text'
549 *
550 * @method
551 * @param {String} name The attribute name.
552 * @returns {String} The attribute value or null if not defined.
553 */
554 getAttribute: ( function() {
555 var standard = function( name ) {
556 return this.$.getAttribute( name, 2 );
557 };
558
559 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
560 return function( name ) {
561 switch ( name ) {
562 case 'class':
563 name = 'className';
564 break;
565
566 case 'http-equiv':
567 name = 'httpEquiv';
568 break;
569
570 case 'name':
571 return this.$.name;
572
573 case 'tabindex':
574 var tabIndex = standard.call( this, name );
575
576 // IE returns tabIndex=0 by default for all
577 // elements. For those elements,
578 // getAtrribute( 'tabindex', 2 ) returns 32768
579 // instead. So, we must make this check to give a
580 // uniform result among all browsers.
581 if ( tabIndex !== 0 && this.$.tabIndex === 0 )
582 tabIndex = null;
583
584 return tabIndex;
585
586 case 'checked':
587 var attr = this.$.attributes.getNamedItem( name ),
588 attrValue = attr.specified ? attr.nodeValue // For value given by parser.
589 : this.$.checked; // For value created via DOM interface.
590
591 return attrValue ? 'checked' : null;
592
593 case 'hspace':
594 case 'value':
595 return this.$[ name ];
596
597 case 'style':
598 // IE does not return inline styles via getAttribute(). See #2947.
599 return this.$.style.cssText;
600
601 case 'contenteditable':
602 case 'contentEditable':
603 return this.$.attributes.getNamedItem( 'contentEditable' ).specified ? this.$.getAttribute( 'contentEditable' ) : null;
604 }
605
606 return standard.call( this, name );
607 };
608 } else {
609 return standard;
610 }
611 } )(),
612
613 /**
614 * Gets the nodes list containing all children of this element.
615 *
616 * @returns {CKEDITOR.dom.nodeList}
617 */
618 getChildren: function() {
619 return new CKEDITOR.dom.nodeList( this.$.childNodes );
620 },
621
622 /**
623 * Gets the current computed value of one of the element CSS style
624 * properties.
625 *
626 * var element = new CKEDITOR.dom.element( 'span' );
627 * alert( element.getComputedStyle( 'display' ) ); // 'inline'
628 *
629 * @method
630 * @param {String} propertyName The style property name.
631 * @returns {String} The property value.
632 */
633 getComputedStyle: ( document.defaultView && document.defaultView.getComputedStyle ) ?
634 function( propertyName ) {
635 var style = this.getWindow().$.getComputedStyle( this.$, null );
636
637 // Firefox may return null if we call the above on a hidden iframe. (#9117)
638 return style ? style.getPropertyValue( propertyName ) : '';
639 } : function( propertyName ) {
640 return this.$.currentStyle[ CKEDITOR.tools.cssStyleToDomStyle( propertyName ) ];
641 },
642
643 /**
644 * Gets the DTD entries for this element.
645 *
646 * @returns {Object} An object containing the list of elements accepted
647 * by this element.
648 */
649 getDtd: function() {
650 var dtd = CKEDITOR.dtd[ this.getName() ];
651
652 this.getDtd = function() {
653 return dtd;
654 };
655
656 return dtd;
657 },
658
659 /**
660 * Gets all this element's descendants having given tag name.
661 *
662 * @method
663 * @param {String} tagName
664 */
665 getElementsByTag: CKEDITOR.dom.document.prototype.getElementsByTag,
666
667 /**
668 * Gets the computed tabindex for this element.
669 *
670 * var element = CKEDITOR.document.getById( 'myDiv' );
671 * alert( element.getTabIndex() ); // (e.g.) '-1'
672 *
673 * @method
674 * @returns {Number} The tabindex value.
675 */
676 getTabIndex: function() {
677 var tabIndex = this.$.tabIndex;
678
679 // IE returns tabIndex=0 by default for all elements. In
680 // those cases we must check that the element really has
681 // the tabindex attribute set to zero, or it is one of
682 // those element that should have zero by default.
683 if ( tabIndex === 0 && !CKEDITOR.dtd.$tabIndex[ this.getName() ] && parseInt( this.getAttribute( 'tabindex' ), 10 ) !== 0 )
684 return -1;
685
686 return tabIndex;
687 },
688
689 /**
690 * Gets the text value of this element.
691 *
692 * Only in IE (which uses innerText), `<br>` will cause linebreaks,
693 * and sucessive whitespaces (including line breaks) will be reduced to
694 * a single space. This behavior is ok for us, for now. It may change
695 * in the future.
696 *
697 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Sample <i>text</i>.</div>' );
698 * alert( <b>element.getText()</b> ); // 'Sample text.'
699 *
700 * @returns {String} The text value.
701 */
702 getText: function() {
703 return this.$.textContent || this.$.innerText || '';
704 },
705
706 /**
707 * Gets the window object that contains this element.
708 *
709 * @returns {CKEDITOR.dom.window} The window object.
710 */
711 getWindow: function() {
712 return this.getDocument().getWindow();
713 },
714
715 /**
716 * Gets the value of the `id` attribute of this element.
717 *
718 * var element = CKEDITOR.dom.element.createFromHtml( '<p id="myId"></p>' );
719 * alert( element.getId() ); // 'myId'
720 *
721 * @returns {String} The element id, or null if not available.
722 */
723 getId: function() {
724 return this.$.id || null;
725 },
726
727 /**
728 * Gets the value of the `name` attribute of this element.
729 *
730 * var element = CKEDITOR.dom.element.createFromHtml( '<input name="myName"></input>' );
731 * alert( <b>element.getNameAtt()</b> ); // 'myName'
732 *
733 * @returns {String} The element name, or null if not available.
734 */
735 getNameAtt: function() {
736 return this.$.name || null;
737 },
738
739 /**
740 * Gets the element name (tag name). The returned name is guaranteed to
741 * be always full lowercased.
742 *
743 * var element = new CKEDITOR.dom.element( 'span' );
744 * alert( element.getName() ); // 'span'
745 *
746 * @returns {String} The element name.
747 */
748 getName: function() {
749 // Cache the lowercased name inside a closure.
750 var nodeName = this.$.nodeName.toLowerCase();
751
752 if ( CKEDITOR.env.ie && ( document.documentMode <= 8 ) ) {
753 var scopeName = this.$.scopeName;
754 if ( scopeName != 'HTML' )
755 nodeName = scopeName.toLowerCase() + ':' + nodeName;
756 }
757
758 this.getName = function() {
759 return nodeName;
760 };
761
762 return this.getName();
763 },
764
765 /**
766 * Gets the value set to this element. This value is usually available
767 * for form field elements.
768 *
769 * @returns {String} The element value.
770 */
771 getValue: function() {
772 return this.$.value;
773 },
774
775 /**
776 * Gets the first child node of this element.
777 *
778 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
779 * var first = element.getFirst();
780 * alert( first.getName() ); // 'b'
781 *
782 * @param {Function} evaluator Filtering the result node.
783 * @returns {CKEDITOR.dom.node} The first child node or null if not available.
784 */
785 getFirst: function( evaluator ) {
786 var first = this.$.firstChild,
787 retval = first && new CKEDITOR.dom.node( first );
788 if ( retval && evaluator && !evaluator( retval ) )
789 retval = retval.getNext( evaluator );
790
791 return retval;
792 },
793
794 /**
795 * See {@link #getFirst}.
796 *
797 * @param {Function} evaluator Filtering the result node.
798 * @returns {CKEDITOR.dom.node}
799 */
800 getLast: function( evaluator ) {
801 var last = this.$.lastChild,
802 retval = last && new CKEDITOR.dom.node( last );
803 if ( retval && evaluator && !evaluator( retval ) )
804 retval = retval.getPrevious( evaluator );
805
806 return retval;
807 },
808
809 /**
810 * Gets CSS style value.
811 *
812 * @param {String} name The CSS property name.
813 * @returns {String} Style value.
814 */
815 getStyle: function( name ) {
816 return this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ];
817 },
818
819 /**
820 * Checks if the element name matches the specified criteria.
821 *
822 * var element = new CKEDITOR.element( 'span' );
823 * alert( element.is( 'span' ) ); // true
824 * alert( element.is( 'p', 'span' ) ); // true
825 * alert( element.is( 'p' ) ); // false
826 * alert( element.is( 'p', 'div' ) ); // false
827 * alert( element.is( { p:1,span:1 } ) ); // true
828 *
829 * @param {String.../Object} name One or more names to be checked, or a {@link CKEDITOR.dtd} object.
830 * @returns {Boolean} `true` if the element name matches any of the names.
831 */
832 is: function() {
833 var name = this.getName();
834
835 // Check against the specified DTD liternal.
836 if ( typeof arguments[ 0 ] == 'object' )
837 return !!arguments[ 0 ][ name ];
838
839 // Check for tag names
840 for ( var i = 0; i < arguments.length; i++ ) {
841 if ( arguments[ i ] == name )
842 return true;
843 }
844 return false;
845 },
846
847 /**
848 * Decide whether one element is able to receive cursor.
849 *
850 * @param {Boolean} [textCursor=true] Only consider element that could receive text child.
851 */
852 isEditable: function( textCursor ) {
853 var name = this.getName();
854
855 if ( this.isReadOnly() || this.getComputedStyle( 'display' ) == 'none' ||
856 this.getComputedStyle( 'visibility' ) == 'hidden' ||
857 CKEDITOR.dtd.$nonEditable[ name ] ||
858 CKEDITOR.dtd.$empty[ name ] ||
859 ( this.is( 'a' ) &&
860 ( this.data( 'cke-saved-name' ) || this.hasAttribute( 'name' ) ) &&
861 !this.getChildCount()
862 ) ) {
863 return false;
864 }
865
866 if ( textCursor !== false ) {
867 // Get the element DTD (defaults to span for unknown elements).
868 var dtd = CKEDITOR.dtd[ name ] || CKEDITOR.dtd.span;
869 // In the DTD # == text node.
870 return !!( dtd && dtd[ '#' ] );
871 }
872
873 return true;
874 },
875
876 /**
877 * Compare this element's inner html, tag name, attributes, etc. with other one.
878 *
879 * See [W3C's DOM Level 3 spec - node#isEqualNode](http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-isEqualNode)
880 * for more details.
881 *
882 * @param {CKEDITOR.dom.element} otherElement Element to compare.
883 * @returns {Boolean}
884 */
885 isIdentical: function( otherElement ) {
886 // do shallow clones, but with IDs
887 var thisEl = this.clone( 0, 1 ),
888 otherEl = otherElement.clone( 0, 1 );
889
890 // Remove distractions.
891 thisEl.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] );
892 otherEl.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] );
893
894 // Native comparison available.
895 if ( thisEl.$.isEqualNode ) {
896 // Styles order matters.
897 thisEl.$.style.cssText = CKEDITOR.tools.normalizeCssText( thisEl.$.style.cssText );
898 otherEl.$.style.cssText = CKEDITOR.tools.normalizeCssText( otherEl.$.style.cssText );
899 return thisEl.$.isEqualNode( otherEl.$ );
900 } else {
901 thisEl = thisEl.getOuterHtml();
902 otherEl = otherEl.getOuterHtml();
903
904 // Fix tiny difference between link href in older IEs.
905 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && this.is( 'a' ) ) {
906 var parent = this.getParent();
907 if ( parent.type == CKEDITOR.NODE_ELEMENT ) {
908 var el = parent.clone();
909 el.setHtml( thisEl ), thisEl = el.getHtml();
910 el.setHtml( otherEl ), otherEl = el.getHtml();
911 }
912 }
913
914 return thisEl == otherEl;
915 }
916 },
917
918 /**
919 * Checks if this element is visible. May not work if the element is
920 * child of an element with visibility set to `hidden`, but works well
921 * on the great majority of cases.
922 *
923 * @returns {Boolean} True if the element is visible.
924 */
925 isVisible: function() {
926 var isVisible = ( this.$.offsetHeight || this.$.offsetWidth ) && this.getComputedStyle( 'visibility' ) != 'hidden',
927 elementWindow, elementWindowFrame;
928
929 // Webkit and Opera report non-zero offsetHeight despite that
930 // element is inside an invisible iframe. (#4542)
931 if ( isVisible && CKEDITOR.env.webkit ) {
932 elementWindow = this.getWindow();
933
934 if ( !elementWindow.equals( CKEDITOR.document.getWindow() ) && ( elementWindowFrame = elementWindow.$.frameElement ) )
935 isVisible = new CKEDITOR.dom.element( elementWindowFrame ).isVisible();
936
937 }
938
939 return !!isVisible;
940 },
941
942 /**
943 * Whether it's an empty inline elements which has no visual impact when removed.
944 *
945 * @returns {Boolean}
946 */
947 isEmptyInlineRemoveable: function() {
948 if ( !CKEDITOR.dtd.$removeEmpty[ this.getName() ] )
949 return false;
950
951 var children = this.getChildren();
952 for ( var i = 0, count = children.count(); i < count; i++ ) {
953 var child = children.getItem( i );
954
955 if ( child.type == CKEDITOR.NODE_ELEMENT && child.data( 'cke-bookmark' ) )
956 continue;
957
958 if ( child.type == CKEDITOR.NODE_ELEMENT && !child.isEmptyInlineRemoveable() || child.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( child.getText() ) )
959 return false;
960
961 }
962 return true;
963 },
964
965 /**
966 * Checks if the element has any defined attributes.
967 *
968 * var element = CKEDITOR.dom.element.createFromHtml( '<div title="Test">Example</div>' );
969 * alert( element.hasAttributes() ); // true
970 *
971 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Example</div>' );
972 * alert( element.hasAttributes() ); // false
973 *
974 * @method
975 * @returns {Boolean} True if the element has attributes.
976 */
977 hasAttributes: CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ?
978 function() {
979 var attributes = this.$.attributes;
980
981 for ( var i = 0; i < attributes.length; i++ ) {
982 var attribute = attributes[ i ];
983
984 switch ( attribute.nodeName ) {
985 case 'class':
986 // IE has a strange bug. If calling removeAttribute('className'),
987 // the attributes collection will still contain the "class"
988 // attribute, which will be marked as "specified", even if the
989 // outerHTML of the element is not displaying the class attribute.
990 // Note : I was not able to reproduce it outside the editor,
991 // but I've faced it while working on the TC of #1391.
992 if ( this.getAttribute( 'class' ) ) {
993 return true;
994 }
995
996 // Attributes to be ignored.
997 /* falls through */
998 case 'data-cke-expando':
999 continue;
1000
1001
1002 /* falls through */
1003 default:
1004 if ( attribute.specified ) {
1005 return true;
1006 }
1007 }
1008 }
1009
1010 return false;
1011 } : function() {
1012 var attrs = this.$.attributes,
1013 attrsNum = attrs.length;
1014
1015 // The _moz_dirty attribute might get into the element after pasting (#5455)
1016 var execludeAttrs = { 'data-cke-expando': 1, _moz_dirty: 1 };
1017
1018 return attrsNum > 0 && ( attrsNum > 2 || !execludeAttrs[ attrs[ 0 ].nodeName ] || ( attrsNum == 2 && !execludeAttrs[ attrs[ 1 ].nodeName ] ) );
1019 },
1020
1021 /**
1022 * Checks if the specified attribute is defined for this element.
1023 *
1024 * @method
1025 * @param {String} name The attribute name.
1026 * @returns {Boolean} `true` if the specified attribute is defined.
1027 */
1028 hasAttribute: ( function() {
1029 function ieHasAttribute( name ) {
1030 var $attr = this.$.attributes.getNamedItem( name );
1031
1032 if ( this.getName() == 'input' ) {
1033 switch ( name ) {
1034 case 'class':
1035 return this.$.className.length > 0;
1036 case 'checked':
1037 return !!this.$.checked;
1038 case 'value':
1039 var type = this.getAttribute( 'type' );
1040 return type == 'checkbox' || type == 'radio' ? this.$.value != 'on' : !!this.$.value;
1041 }
1042 }
1043
1044 if ( !$attr )
1045 return false;
1046
1047 return $attr.specified;
1048 }
1049
1050 if ( CKEDITOR.env.ie ) {
1051 if ( CKEDITOR.env.version < 8 ) {
1052 return function( name ) {
1053 // On IE < 8 the name attribute cannot be retrieved
1054 // right after the element creation and setting the
1055 // name with setAttribute.
1056 if ( name == 'name' )
1057 return !!this.$.name;
1058
1059 return ieHasAttribute.call( this, name );
1060 };
1061 } else {
1062 return ieHasAttribute;
1063 }
1064 } else {
1065 return function( name ) {
1066 // On other browsers specified property is deprecated and return always true,
1067 // but fortunately $.attributes contains only specified attributes.
1068 return !!this.$.attributes.getNamedItem( name );
1069 };
1070 }
1071 } )(),
1072
1073 /**
1074 * Hides this element (sets `display: none`).
1075 *
1076 * var element = CKEDITOR.document.getById( 'myElement' );
1077 * element.hide();
1078 */
1079 hide: function() {
1080 this.setStyle( 'display', 'none' );
1081 },
1082
1083 /**
1084 * Moves this element's children to the target element.
1085 *
1086 * @param {CKEDITOR.dom.element} target
1087 * @param {Boolean} [toStart=false] Insert moved children at the
1088 * beginning of the target element.
1089 */
1090 moveChildren: function( target, toStart ) {
1091 var $ = this.$;
1092 target = target.$;
1093
1094 if ( $ == target )
1095 return;
1096
1097 var child;
1098
1099 if ( toStart ) {
1100 while ( ( child = $.lastChild ) )
1101 target.insertBefore( $.removeChild( child ), target.firstChild );
1102 } else {
1103 while ( ( child = $.firstChild ) )
1104 target.appendChild( $.removeChild( child ) );
1105 }
1106 },
1107
1108 /**
1109 * Merges sibling elements that are identical to this one.
1110 *
1111 * Identical child elements are also merged. For example:
1112 *
1113 * <b><i></i></b><b><i></i></b> => <b><i></i></b>
1114 *
1115 * @method
1116 * @param {Boolean} [inlineOnly=true] Allow only inline elements to be merged.
1117 */
1118 mergeSiblings: ( function() {
1119 function mergeElements( element, sibling, isNext ) {
1120 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) {
1121 // Jumping over bookmark nodes and empty inline elements, e.g. <b><i></i></b>,
1122 // queuing them to be moved later. (#5567)
1123 var pendingNodes = [];
1124
1125 while ( sibling.data( 'cke-bookmark' ) || sibling.isEmptyInlineRemoveable() ) {
1126 pendingNodes.push( sibling );
1127 sibling = isNext ? sibling.getNext() : sibling.getPrevious();
1128 if ( !sibling || sibling.type != CKEDITOR.NODE_ELEMENT )
1129 return;
1130 }
1131
1132 if ( element.isIdentical( sibling ) ) {
1133 // Save the last child to be checked too, to merge things like
1134 // <b><i></i></b><b><i></i></b> => <b><i></i></b>
1135 var innerSibling = isNext ? element.getLast() : element.getFirst();
1136
1137 // Move pending nodes first into the target element.
1138 while ( pendingNodes.length )
1139 pendingNodes.shift().move( element, !isNext );
1140
1141 sibling.moveChildren( element, !isNext );
1142 sibling.remove();
1143
1144 // Now check the last inner child (see two comments above).
1145 if ( innerSibling && innerSibling.type == CKEDITOR.NODE_ELEMENT )
1146 innerSibling.mergeSiblings();
1147 }
1148 }
1149 }
1150
1151 return function( inlineOnly ) {
1152 // Merge empty links and anchors also. (#5567)
1153 if ( !( inlineOnly === false || CKEDITOR.dtd.$removeEmpty[ this.getName() ] || this.is( 'a' ) ) ) {
1154 return;
1155 }
1156
1157 mergeElements( this, this.getNext(), true );
1158 mergeElements( this, this.getPrevious() );
1159 };
1160 } )(),
1161
1162 /**
1163 * Shows this element (displays it).
1164 *
1165 * var element = CKEDITOR.document.getById( 'myElement' );
1166 * element.show();
1167 */
1168 show: function() {
1169 this.setStyles( {
1170 display: '',
1171 visibility: ''
1172 } );
1173 },
1174
1175 /**
1176 * Sets the value of an element attribute.
1177 *
1178 * var element = CKEDITOR.document.getById( 'myElement' );
1179 * element.setAttribute( 'class', 'myClass' );
1180 * element.setAttribute( 'title', 'This is an example' );
1181 *
1182 * @method
1183 * @param {String} name The name of the attribute.
1184 * @param {String} value The value to be set to the attribute.
1185 * @returns {CKEDITOR.dom.element} This element instance.
1186 */
1187 setAttribute: ( function() {
1188 var standard = function( name, value ) {
1189 this.$.setAttribute( name, value );
1190 return this;
1191 };
1192
1193 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
1194 return function( name, value ) {
1195 if ( name == 'class' )
1196 this.$.className = value;
1197 else if ( name == 'style' )
1198 this.$.style.cssText = value;
1199 else if ( name == 'tabindex' ) // Case sensitive.
1200 this.$.tabIndex = value;
1201 else if ( name == 'checked' )
1202 this.$.checked = value;
1203 else if ( name == 'contenteditable' )
1204 standard.call( this, 'contentEditable', value );
1205 else
1206 standard.apply( this, arguments );
1207 return this;
1208 };
1209 } else if ( CKEDITOR.env.ie8Compat && CKEDITOR.env.secure ) {
1210 return function( name, value ) {
1211 // IE8 throws error when setting src attribute to non-ssl value. (#7847)
1212 if ( name == 'src' && value.match( /^http:\/\// ) ) {
1213 try {
1214 standard.apply( this, arguments );
1215 } catch ( e ) {}
1216 } else {
1217 standard.apply( this, arguments );
1218 }
1219 return this;
1220 };
1221 } else {
1222 return standard;
1223 }
1224 } )(),
1225
1226 /**
1227 * Sets the value of several element attributes.
1228 *
1229 * var element = CKEDITOR.document.getById( 'myElement' );
1230 * element.setAttributes( {
1231 * 'class': 'myClass',
1232 * title: 'This is an example'
1233 * } );
1234 *
1235 * @chainable
1236 * @param {Object} attributesPairs An object containing the names and
1237 * values of the attributes.
1238 * @returns {CKEDITOR.dom.element} This element instance.
1239 */
1240 setAttributes: function( attributesPairs ) {
1241 for ( var name in attributesPairs )
1242 this.setAttribute( name, attributesPairs[ name ] );
1243 return this;
1244 },
1245
1246 /**
1247 * Sets the element value. This function is usually used with form
1248 * field element.
1249 *
1250 * @chainable
1251 * @param {String} value The element value.
1252 * @returns {CKEDITOR.dom.element} This element instance.
1253 */
1254 setValue: function( value ) {
1255 this.$.value = value;
1256 return this;
1257 },
1258
1259 /**
1260 * Removes an attribute from the element.
1261 *
1262 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="classA"></div>' );
1263 * element.removeAttribute( 'class' );
1264 *
1265 * @method
1266 * @param {String} name The attribute name.
1267 */
1268 removeAttribute: ( function() {
1269 var standard = function( name ) {
1270 this.$.removeAttribute( name );
1271 };
1272
1273 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
1274 return function( name ) {
1275 if ( name == 'class' )
1276 name = 'className';
1277 else if ( name == 'tabindex' )
1278 name = 'tabIndex';
1279 else if ( name == 'contenteditable' )
1280 name = 'contentEditable';
1281 standard.call( this, name );
1282 };
1283 } else {
1284 return standard;
1285 }
1286 } )(),
1287
1288 /**
1289 * Removes all element's attributes or just given ones.
1290 *
1291 * @param {Array} [attributes] The array with attributes names.
1292 */
1293 removeAttributes: function( attributes ) {
1294 if ( CKEDITOR.tools.isArray( attributes ) ) {
1295 for ( var i = 0; i < attributes.length; i++ )
1296 this.removeAttribute( attributes[ i ] );
1297 } else {
1298 for ( var attr in attributes )
1299 attributes.hasOwnProperty( attr ) && this.removeAttribute( attr );
1300 }
1301 },
1302
1303 /**
1304 * Removes a style from the element.
1305 *
1306 * var element = CKEDITOR.dom.element.createFromHtml( '<div style="display:none"></div>' );
1307 * element.removeStyle( 'display' );
1308 *
1309 * @method
1310 * @param {String} name The style name.
1311 */
1312 removeStyle: function( name ) {
1313 // Removes the specified property from the current style object.
1314 var $ = this.$.style;
1315
1316 // "removeProperty" need to be specific on the following styles.
1317 if ( !$.removeProperty && ( name == 'border' || name == 'margin' || name == 'padding' ) ) {
1318 var names = expandedRules( name );
1319 for ( var i = 0 ; i < names.length ; i++ )
1320 this.removeStyle( names[ i ] );
1321 return;
1322 }
1323
1324 $.removeProperty ? $.removeProperty( name ) : $.removeAttribute( CKEDITOR.tools.cssStyleToDomStyle( name ) );
1325
1326 // Eventually remove empty style attribute.
1327 if ( !this.$.style.cssText )
1328 this.removeAttribute( 'style' );
1329 },
1330
1331 /**
1332 * Sets the value of an element style.
1333 *
1334 * var element = CKEDITOR.document.getById( 'myElement' );
1335 * element.setStyle( 'background-color', '#ff0000' );
1336 * element.setStyle( 'margin-top', '10px' );
1337 * element.setStyle( 'float', 'right' );
1338 *
1339 * @param {String} name The name of the style. The CSS naming notation
1340 * must be used (e.g. `background-color`).
1341 * @param {String} value The value to be set to the style.
1342 * @returns {CKEDITOR.dom.element} This element instance.
1343 */
1344 setStyle: function( name, value ) {
1345 this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ] = value;
1346 return this;
1347 },
1348
1349 /**
1350 * Sets the value of several element styles.
1351 *
1352 * var element = CKEDITOR.document.getById( 'myElement' );
1353 * element.setStyles( {
1354 * position: 'absolute',
1355 * float: 'right'
1356 * } );
1357 *
1358 * @param {Object} stylesPairs An object containing the names and
1359 * values of the styles.
1360 * @returns {CKEDITOR.dom.element} This element instance.
1361 */
1362 setStyles: function( stylesPairs ) {
1363 for ( var name in stylesPairs )
1364 this.setStyle( name, stylesPairs[ name ] );
1365 return this;
1366 },
1367
1368 /**
1369 * Sets the opacity of an element.
1370 *
1371 * var element = CKEDITOR.document.getById( 'myElement' );
1372 * element.setOpacity( 0.75 );
1373 *
1374 * @param {Number} opacity A number within the range `[0.0, 1.0]`.
1375 */
1376 setOpacity: function( opacity ) {
1377 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
1378 opacity = Math.round( opacity * 100 );
1379 this.setStyle( 'filter', opacity >= 100 ? '' : 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + opacity + ')' );
1380 } else {
1381 this.setStyle( 'opacity', opacity );
1382 }
1383 },
1384
1385 /**
1386 * Makes the element and its children unselectable.
1387 *
1388 * var element = CKEDITOR.document.getById( 'myElement' );
1389 * element.unselectable();
1390 *
1391 * @method
1392 */
1393 unselectable: function() {
1394 // CSS unselectable.
1395 this.setStyles( CKEDITOR.tools.cssVendorPrefix( 'user-select', 'none' ) );
1396
1397 // For IE/Opera which doesn't support for the above CSS style,
1398 // the unselectable="on" attribute only specifies the selection
1399 // process cannot start in the element itself, and it doesn't inherit.
1400 if ( CKEDITOR.env.ie ) {
1401 this.setAttribute( 'unselectable', 'on' );
1402
1403 var element,
1404 elements = this.getElementsByTag( '*' );
1405
1406 for ( var i = 0, count = elements.count() ; i < count ; i++ ) {
1407 element = elements.getItem( i );
1408 element.setAttribute( 'unselectable', 'on' );
1409 }
1410 }
1411 },
1412
1413 /**
1414 * Gets closest positioned (`position != static`) ancestor.
1415 *
1416 * @returns {CKEDITOR.dom.element} Positioned ancestor or `null`.
1417 */
1418 getPositionedAncestor: function() {
1419 var current = this;
1420 while ( current.getName() != 'html' ) {
1421 if ( current.getComputedStyle( 'position' ) != 'static' )
1422 return current;
1423
1424 current = current.getParent();
1425 }
1426 return null;
1427 },
1428
1429 /**
1430 * Gets this element's position in document.
1431 *
1432 * @param {CKEDITOR.dom.document} [refDocument]
1433 * @returns {Object} Element's position.
1434 * @returns {Number} return.x
1435 * @returns {Number} return.y
1436 * @todo refDocument
1437 */
1438 getDocumentPosition: function( refDocument ) {
1439 var x = 0,
1440 y = 0,
1441 doc = this.getDocument(),
1442 body = doc.getBody(),
1443 quirks = doc.$.compatMode == 'BackCompat';
1444
1445 if ( document.documentElement.getBoundingClientRect ) {
1446 var box = this.$.getBoundingClientRect(),
1447 $doc = doc.$,
1448 $docElem = $doc.documentElement;
1449
1450 var clientTop = $docElem.clientTop || body.$.clientTop || 0,
1451 clientLeft = $docElem.clientLeft || body.$.clientLeft || 0,
1452 needAdjustScrollAndBorders = true;
1453
1454 // #3804: getBoundingClientRect() works differently on IE and non-IE
1455 // browsers, regarding scroll positions.
1456 //
1457 // On IE, the top position of the <html> element is always 0, no matter
1458 // how much you scrolled down.
1459 //
1460 // On other browsers, the top position of the <html> element is negative
1461 // scrollTop.
1462 if ( CKEDITOR.env.ie ) {
1463 var inDocElem = doc.getDocumentElement().contains( this ),
1464 inBody = doc.getBody().contains( this );
1465
1466 needAdjustScrollAndBorders = ( quirks && inBody ) || ( !quirks && inDocElem );
1467 }
1468
1469 // #12747.
1470 if ( needAdjustScrollAndBorders ) {
1471 var scrollRelativeLeft,
1472 scrollRelativeTop;
1473
1474 // See #12758 to know more about document.(documentElement|body).scroll(Left|Top) in Webkit.
1475 if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version >= 12 ) ) {
1476 scrollRelativeLeft = body.$.scrollLeft || $docElem.scrollLeft;
1477 scrollRelativeTop = body.$.scrollTop || $docElem.scrollTop;
1478 } else {
1479 var scrollRelativeElement = quirks ? body.$ : $docElem;
1480
1481 scrollRelativeLeft = scrollRelativeElement.scrollLeft;
1482 scrollRelativeTop = scrollRelativeElement.scrollTop;
1483 }
1484
1485 x = box.left + scrollRelativeLeft - clientLeft;
1486 y = box.top + scrollRelativeTop - clientTop;
1487 }
1488 } else {
1489 var current = this,
1490 previous = null,
1491 offsetParent;
1492 while ( current && !( current.getName() == 'body' || current.getName() == 'html' ) ) {
1493 x += current.$.offsetLeft - current.$.scrollLeft;
1494 y += current.$.offsetTop - current.$.scrollTop;
1495
1496 // Opera includes clientTop|Left into offsetTop|Left.
1497 if ( !current.equals( this ) ) {
1498 x += ( current.$.clientLeft || 0 );
1499 y += ( current.$.clientTop || 0 );
1500 }
1501
1502 var scrollElement = previous;
1503 while ( scrollElement && !scrollElement.equals( current ) ) {
1504 x -= scrollElement.$.scrollLeft;
1505 y -= scrollElement.$.scrollTop;
1506 scrollElement = scrollElement.getParent();
1507 }
1508
1509 previous = current;
1510 current = ( offsetParent = current.$.offsetParent ) ? new CKEDITOR.dom.element( offsetParent ) : null;
1511 }
1512 }
1513
1514 if ( refDocument ) {
1515 var currentWindow = this.getWindow(),
1516 refWindow = refDocument.getWindow();
1517
1518 if ( !currentWindow.equals( refWindow ) && currentWindow.$.frameElement ) {
1519 var iframePosition = ( new CKEDITOR.dom.element( currentWindow.$.frameElement ) ).getDocumentPosition( refDocument );
1520
1521 x += iframePosition.x;
1522 y += iframePosition.y;
1523 }
1524 }
1525
1526 if ( !document.documentElement.getBoundingClientRect ) {
1527 // In Firefox, we'll endup one pixel before the element positions,
1528 // so we must add it here.
1529 if ( CKEDITOR.env.gecko && !quirks ) {
1530 x += this.$.clientLeft ? 1 : 0;
1531 y += this.$.clientTop ? 1 : 0;
1532 }
1533 }
1534
1535 return { x: x, y: y };
1536 },
1537
1538 /**
1539 * Make any page element visible inside the browser viewport.
1540 *
1541 * @param {Boolean} [alignToTop=false]
1542 */
1543 scrollIntoView: function( alignToTop ) {
1544 var parent = this.getParent();
1545 if ( !parent )
1546 return;
1547
1548 // Scroll the element into parent container from the inner out.
1549 do {
1550 // Check ancestors that overflows.
1551 var overflowed =
1552 parent.$.clientWidth && parent.$.clientWidth < parent.$.scrollWidth ||
1553 parent.$.clientHeight && parent.$.clientHeight < parent.$.scrollHeight;
1554
1555 // Skip body element, which will report wrong clientHeight when containing
1556 // floated content. (#9523)
1557 if ( overflowed && !parent.is( 'body' ) )
1558 this.scrollIntoParent( parent, alignToTop, 1 );
1559
1560 // Walk across the frame.
1561 if ( parent.is( 'html' ) ) {
1562 var win = parent.getWindow();
1563
1564 // Avoid security error.
1565 try {
1566 var iframe = win.$.frameElement;
1567 iframe && ( parent = new CKEDITOR.dom.element( iframe ) );
1568 } catch ( er ) {}
1569 }
1570 }
1571 while ( ( parent = parent.getParent() ) );
1572 },
1573
1574 /**
1575 * Make any page element visible inside one of the ancestors by scrolling the parent.
1576 *
1577 * @param {CKEDITOR.dom.element/CKEDITOR.dom.window} parent The container to scroll into.
1578 * @param {Boolean} [alignToTop] Align the element's top side with the container's
1579 * when `true` is specified; align the bottom with viewport bottom when
1580 * `false` is specified. Otherwise scroll on either side with the minimum
1581 * amount to show the element.
1582 * @param {Boolean} [hscroll] Whether horizontal overflow should be considered.
1583 */
1584 scrollIntoParent: function( parent, alignToTop, hscroll ) {
1585 !parent && ( parent = this.getWindow() );
1586
1587 var doc = parent.getDocument();
1588 var isQuirks = doc.$.compatMode == 'BackCompat';
1589
1590 // On window <html> is scrolled while quirks scrolls <body>.
1591 if ( parent instanceof CKEDITOR.dom.window )
1592 parent = isQuirks ? doc.getBody() : doc.getDocumentElement();
1593
1594 // Scroll the parent by the specified amount.
1595 function scrollBy( x, y ) {
1596 // Webkit doesn't support "scrollTop/scrollLeft"
1597 // on documentElement/body element.
1598 if ( /body|html/.test( parent.getName() ) )
1599 parent.getWindow().$.scrollBy( x, y );
1600 else {
1601 parent.$.scrollLeft += x;
1602 parent.$.scrollTop += y;
1603 }
1604 }
1605
1606 // Figure out the element position relative to the specified window.
1607 function screenPos( element, refWin ) {
1608 var pos = { x: 0, y: 0 };
1609
1610 if ( !( element.is( isQuirks ? 'body' : 'html' ) ) ) {
1611 var box = element.$.getBoundingClientRect();
1612 pos.x = box.left, pos.y = box.top;
1613 }
1614
1615 var win = element.getWindow();
1616 if ( !win.equals( refWin ) ) {
1617 var outerPos = screenPos( CKEDITOR.dom.element.get( win.$.frameElement ), refWin );
1618 pos.x += outerPos.x, pos.y += outerPos.y;
1619 }
1620
1621 return pos;
1622 }
1623
1624 // calculated margin size.
1625 function margin( element, side ) {
1626 return parseInt( element.getComputedStyle( 'margin-' + side ) || 0, 10 ) || 0;
1627 }
1628
1629 var win = parent.getWindow();
1630
1631 var thisPos = screenPos( this, win ),
1632 parentPos = screenPos( parent, win ),
1633 eh = this.$.offsetHeight,
1634 ew = this.$.offsetWidth,
1635 ch = parent.$.clientHeight,
1636 cw = parent.$.clientWidth,
1637 lt, br;
1638
1639 // Left-top margins.
1640 lt = {
1641 x: thisPos.x - margin( this, 'left' ) - parentPos.x || 0,
1642 y: thisPos.y - margin( this, 'top' ) - parentPos.y || 0
1643 };
1644
1645 // Bottom-right margins.
1646 br = {
1647 x: thisPos.x + ew + margin( this, 'right' ) - ( ( parentPos.x ) + cw ) || 0,
1648 y: thisPos.y + eh + margin( this, 'bottom' ) - ( ( parentPos.y ) + ch ) || 0
1649 };
1650
1651 // 1. Do the specified alignment as much as possible;
1652 // 2. Otherwise be smart to scroll only the minimum amount;
1653 // 3. Never cut at the top;
1654 // 4. DO NOT scroll when already visible.
1655 if ( lt.y < 0 || br.y > 0 )
1656 scrollBy( 0, alignToTop === true ? lt.y : alignToTop === false ? br.y : lt.y < 0 ? lt.y : br.y );
1657
1658 if ( hscroll && ( lt.x < 0 || br.x > 0 ) )
1659 scrollBy( lt.x < 0 ? lt.x : br.x, 0 );
1660 },
1661
1662 /**
1663 * Switch the `class` attribute to reflect one of the triple states of an
1664 * element in one of {@link CKEDITOR#TRISTATE_ON}, {@link CKEDITOR#TRISTATE_OFF}
1665 * or {@link CKEDITOR#TRISTATE_DISABLED}.
1666 *
1667 * link.setState( CKEDITOR.TRISTATE_ON );
1668 * // <a class="cke_on" aria-pressed="true">...</a>
1669 * link.setState( CKEDITOR.TRISTATE_OFF );
1670 * // <a class="cke_off">...</a>
1671 * link.setState( CKEDITOR.TRISTATE_DISABLED );
1672 * // <a class="cke_disabled" aria-disabled="true">...</a>
1673 *
1674 * span.setState( CKEDITOR.TRISTATE_ON, 'cke_button' );
1675 * // <span class="cke_button_on">...</span>
1676 *
1677 * @param {Number} state Indicate the element state. One of {@link CKEDITOR#TRISTATE_ON},
1678 * {@link CKEDITOR#TRISTATE_OFF}, {@link CKEDITOR#TRISTATE_DISABLED}.
1679 * @param [base='cke'] The prefix apply to each of the state class name.
1680 * @param [useAria=true] Whether toggle the ARIA state attributes besides of class name change.
1681 */
1682 setState: function( state, base, useAria ) {
1683 base = base || 'cke';
1684
1685 switch ( state ) {
1686 case CKEDITOR.TRISTATE_ON:
1687 this.addClass( base + '_on' );
1688 this.removeClass( base + '_off' );
1689 this.removeClass( base + '_disabled' );
1690 useAria && this.setAttribute( 'aria-pressed', true );
1691 useAria && this.removeAttribute( 'aria-disabled' );
1692 break;
1693
1694 case CKEDITOR.TRISTATE_DISABLED:
1695 this.addClass( base + '_disabled' );
1696 this.removeClass( base + '_off' );
1697 this.removeClass( base + '_on' );
1698 useAria && this.setAttribute( 'aria-disabled', true );
1699 useAria && this.removeAttribute( 'aria-pressed' );
1700 break;
1701
1702 default:
1703 this.addClass( base + '_off' );
1704 this.removeClass( base + '_on' );
1705 this.removeClass( base + '_disabled' );
1706 useAria && this.removeAttribute( 'aria-pressed' );
1707 useAria && this.removeAttribute( 'aria-disabled' );
1708 break;
1709 }
1710 },
1711
1712 /**
1713 * Returns the inner document of this `<iframe>` element.
1714 *
1715 * @returns {CKEDITOR.dom.document} The inner document.
1716 */
1717 getFrameDocument: function() {
1718 var $ = this.$;
1719
1720 try {
1721 // In IE, with custom document.domain, it may happen that
1722 // the iframe is not yet available, resulting in "Access
1723 // Denied" for the following property access.
1724 $.contentWindow.document;
1725 } catch ( e ) {
1726 // Trick to solve this issue, forcing the iframe to get ready
1727 // by simply setting its "src" property.
1728 $.src = $.src;
1729 }
1730
1731 return $ && new CKEDITOR.dom.document( $.contentWindow.document );
1732 },
1733
1734 /**
1735 * Copy all the attributes from one node to the other, kinda like a clone
1736 * skipAttributes is an object with the attributes that must **not** be copied.
1737 *
1738 * @param {CKEDITOR.dom.element} dest The destination element.
1739 * @param {Object} skipAttributes A dictionary of attributes to skip.
1740 */
1741 copyAttributes: function( dest, skipAttributes ) {
1742 var attributes = this.$.attributes;
1743 skipAttributes = skipAttributes || {};
1744
1745 for ( var n = 0; n < attributes.length; n++ ) {
1746 var attribute = attributes[ n ];
1747
1748 // Lowercase attribute name hard rule is broken for
1749 // some attribute on IE, e.g. CHECKED.
1750 var attrName = attribute.nodeName.toLowerCase(),
1751 attrValue;
1752
1753 // We can set the type only once, so do it with the proper value, not copying it.
1754 if ( attrName in skipAttributes )
1755 continue;
1756
1757 if ( attrName == 'checked' && ( attrValue = this.getAttribute( attrName ) ) )
1758 dest.setAttribute( attrName, attrValue );
1759 // IE contains not specified attributes in $.attributes so we need to check
1760 // if elements attribute is specified using hasAttribute.
1761 else if ( !CKEDITOR.env.ie || this.hasAttribute( attrName ) ) {
1762 attrValue = this.getAttribute( attrName );
1763 if ( attrValue === null )
1764 attrValue = attribute.nodeValue;
1765
1766 dest.setAttribute( attrName, attrValue );
1767 }
1768 }
1769
1770 // The style:
1771 if ( this.$.style.cssText !== '' )
1772 dest.$.style.cssText = this.$.style.cssText;
1773 },
1774
1775 /**
1776 * Changes the tag name of the current element.
1777 *
1778 * @param {String} newTag The new tag for the element.
1779 */
1780 renameNode: function( newTag ) {
1781 // If it's already correct exit here.
1782 if ( this.getName() == newTag )
1783 return;
1784
1785 var doc = this.getDocument();
1786
1787 // Create the new node.
1788 var newNode = new CKEDITOR.dom.element( newTag, doc );
1789
1790 // Copy all attributes.
1791 this.copyAttributes( newNode );
1792
1793 // Move children to the new node.
1794 this.moveChildren( newNode );
1795
1796 // Replace the node.
1797 this.getParent( true ) && this.$.parentNode.replaceChild( newNode.$, this.$ );
1798 newNode.$[ 'data-cke-expando' ] = this.$[ 'data-cke-expando' ];
1799 this.$ = newNode.$;
1800 // Bust getName's cache. (#8663)
1801 delete this.getName;
1802 },
1803
1804 /**
1805 * Gets a DOM tree descendant under the current node.
1806 *
1807 * var strong = p.getChild( 0 );
1808 *
1809 * @method
1810 * @param {Array/Number} indices The child index or array of child indices under the node.
1811 * @returns {CKEDITOR.dom.node} The specified DOM child under the current node. Null if child does not exist.
1812 */
1813 getChild: ( function() {
1814 function getChild( rawNode, index ) {
1815 var childNodes = rawNode.childNodes;
1816
1817 if ( index >= 0 && index < childNodes.length )
1818 return childNodes[ index ];
1819 }
1820
1821 return function( indices ) {
1822 var rawNode = this.$;
1823
1824 if ( !indices.slice )
1825 rawNode = getChild( rawNode, indices );
1826 else {
1827 indices = indices.slice();
1828 while ( indices.length > 0 && rawNode )
1829 rawNode = getChild( rawNode, indices.shift() );
1830 }
1831
1832 return rawNode ? new CKEDITOR.dom.node( rawNode ) : null;
1833 };
1834 } )(),
1835
1836 /**
1837 * Gets number of element's children.
1838 *
1839 * @returns {Number}
1840 */
1841 getChildCount: function() {
1842 return this.$.childNodes.length;
1843 },
1844
1845 /**
1846 * Disables browser's context menu in this element.
1847 */
1848 disableContextMenu: function() {
1849 this.on( 'contextmenu', function( evt ) {
1850 // Cancel the browser context menu.
1851 if ( !evt.data.getTarget().getAscendant( enablesContextMenu, true ) )
1852 evt.data.preventDefault();
1853 } );
1854
1855 function enablesContextMenu( node ) {
1856 return node.type == CKEDITOR.NODE_ELEMENT && node.hasClass( 'cke_enable_context_menu' );
1857 }
1858 },
1859
1860 /**
1861 * Gets element's direction. Supports both CSS `direction` prop and `dir` attr.
1862 */
1863 getDirection: function( useComputed ) {
1864 if ( useComputed ) {
1865 return this.getComputedStyle( 'direction' ) ||
1866 this.getDirection() ||
1867 this.getParent() && this.getParent().getDirection( 1 ) ||
1868 this.getDocument().$.dir ||
1869 'ltr';
1870 }
1871 else {
1872 return this.getStyle( 'direction' ) || this.getAttribute( 'dir' );
1873 }
1874 },
1875
1876 /**
1877 * Gets, sets and removes custom data to be stored as HTML5 data-* attributes.
1878 *
1879 * element.data( 'extra-info', 'test' ); // Appended the attribute data-extra-info="test" to the element.
1880 * alert( element.data( 'extra-info' ) ); // 'test'
1881 * element.data( 'extra-info', false ); // Remove the data-extra-info attribute from the element.
1882 *
1883 * @param {String} name The name of the attribute, excluding the `data-` part.
1884 * @param {String} [value] The value to set. If set to false, the attribute will be removed.
1885 */
1886 data: function( name, value ) {
1887 name = 'data-' + name;
1888 if ( value === undefined )
1889 return this.getAttribute( name );
1890 else if ( value === false )
1891 this.removeAttribute( name );
1892 else
1893 this.setAttribute( name, value );
1894
1895 return null;
1896 },
1897
1898 /**
1899 * Retrieves an editor instance which is based on this element (if any).
1900 * It basically loops over {@link CKEDITOR#instances} in search for an instance
1901 * that uses the element.
1902 *
1903 * var element = new CKEDITOR.dom.element( 'div' );
1904 * element.appendTo( CKEDITOR.document.getBody() );
1905 * CKEDITOR.replace( element );
1906 * alert( element.getEditor().name ); // 'editor1'
1907 *
1908 * @returns {CKEDITOR.editor} An editor instance or null if nothing has been found.
1909 */
1910 getEditor: function() {
1911 var instances = CKEDITOR.instances,
1912 name, instance;
1913
1914 for ( name in instances ) {
1915 instance = instances[ name ];
1916
1917 if ( instance.element.equals( this ) && instance.elementMode != CKEDITOR.ELEMENT_MODE_APPENDTO )
1918 return instance;
1919 }
1920
1921 return null;
1922 },
1923
1924 /**
1925 * Returns list of elements within this element that match specified `selector`.
1926 *
1927 * **Notes:**
1928 *
1929 * * Not available in IE7.
1930 * * Returned list is not a live collection (like a result of native `querySelectorAll`).
1931 * * Unlike native `querySelectorAll` this method ensures selector contextualization. This is:
1932 *
1933 * HTML: '<body><div><i>foo</i></div></body>'
1934 * Native: div.querySelectorAll( 'body i' ) // -> [ <i>foo</i> ]
1935 * Method: div.find( 'body i' ) // -> []
1936 * div.find( 'i' ) // -> [ <i>foo</i> ]
1937 *
1938 * @since 4.3
1939 * @param {String} selector
1940 * @returns {CKEDITOR.dom.nodeList}
1941 */
1942 find: function( selector ) {
1943 var removeTmpId = createTmpId( this ),
1944 list = new CKEDITOR.dom.nodeList(
1945 this.$.querySelectorAll( getContextualizedSelector( this, selector ) )
1946 );
1947
1948 removeTmpId();
1949
1950 return list;
1951 },
1952
1953 /**
1954 * Returns first element within this element that matches specified `selector`.
1955 *
1956 * **Notes:**
1957 *
1958 * * Not available in IE7.
1959 * * Unlike native `querySelectorAll` this method ensures selector contextualization. This is:
1960 *
1961 * HTML: '<body><div><i>foo</i></div></body>'
1962 * Native: div.querySelector( 'body i' ) // -> <i>foo</i>
1963 * Method: div.findOne( 'body i' ) // -> null
1964 * div.findOne( 'i' ) // -> <i>foo</i>
1965 *
1966 * @since 4.3
1967 * @param {String} selector
1968 * @returns {CKEDITOR.dom.element}
1969 */
1970 findOne: function( selector ) {
1971 var removeTmpId = createTmpId( this ),
1972 found = this.$.querySelector( getContextualizedSelector( this, selector ) );
1973
1974 removeTmpId();
1975
1976 return found ? new CKEDITOR.dom.element( found ) : null;
1977 },
1978
1979 /**
1980 * Traverse the DOM of this element (inclusive), executing a callback for
1981 * each node.
1982 *
1983 * var element = CKEDITOR.dom.element.createFromHtml( '<div><p>foo<b>bar</b>bom</p></div>' );
1984 * element.forEach( function( node ) {
1985 * console.log( node );
1986 * } );
1987 * // Will log:
1988 * // 1. <div> element,
1989 * // 2. <p> element,
1990 * // 3. "foo" text node,
1991 * // 4. <b> element,
1992 * // 5. "bar" text node,
1993 * // 6. "bom" text node.
1994 *
1995 * @since 4.3
1996 * @param {Function} callback Function to be executed on every node.
1997 * If `callback` returns `false` descendants of the node will be ignored.
1998 * @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument.
1999 * @param {Number} [type] If specified `callback` will be executed only on
2000 * nodes of this type.
2001 * @param {Boolean} [skipRoot] Don't execute `callback` on this element.
2002 */
2003 forEach: function( callback, type, skipRoot ) {
2004 if ( !skipRoot && ( !type || this.type == type ) )
2005 var ret = callback( this );
2006
2007 // Do not filter children if callback returned false.
2008 if ( ret === false )
2009 return;
2010
2011 var children = this.getChildren(),
2012 node,
2013 i = 0;
2014
2015 // We do not cache the size, because the live list of nodes may be changed by the callback.
2016 for ( ; i < children.count(); i++ ) {
2017 node = children.getItem( i );
2018 if ( node.type == CKEDITOR.NODE_ELEMENT )
2019 node.forEach( callback, type );
2020 else if ( !type || node.type == type )
2021 callback( node );
2022 }
2023 }
2024 } );
2025
2026 function createTmpId( element ) {
2027 var hadId = true;
2028
2029 if ( !element.$.id ) {
2030 element.$.id = 'cke_tmp_' + CKEDITOR.tools.getNextNumber();
2031 hadId = false;
2032 }
2033
2034 return function() {
2035 if ( !hadId )
2036 element.removeAttribute( 'id' );
2037 };
2038 }
2039
2040 function getContextualizedSelector( element, selector ) {
2041 return '#' + element.$.id + ' ' + selector.split( /,\s*/ ).join( ', #' + element.$.id + ' ' );
2042 }
2043
2044 var sides = {
2045 width: [ 'border-left-width', 'border-right-width', 'padding-left', 'padding-right' ],
2046 height: [ 'border-top-width', 'border-bottom-width', 'padding-top', 'padding-bottom' ]
2047 };
2048
2049 // Generate list of specific style rules, applicable to margin/padding/border.
2050 function expandedRules( style ) {
2051 var sides = [ 'top', 'left', 'right', 'bottom' ], components;
2052
2053 if ( style == 'border' )
2054 components = [ 'color', 'style', 'width' ];
2055
2056 var styles = [];
2057 for ( var i = 0 ; i < sides.length ; i++ ) {
2058
2059 if ( components ) {
2060 for ( var j = 0 ; j < components.length ; j++ )
2061 styles.push( [ style, sides[ i ], components[ j ] ].join( '-' ) );
2062 } else {
2063 styles.push( [ style, sides[ i ] ].join( '-' ) );
2064 }
2065 }
2066
2067 return styles;
2068 }
2069
2070 function marginAndPaddingSize( type ) {
2071 var adjustment = 0;
2072 for ( var i = 0, len = sides[ type ].length; i < len; i++ )
2073 adjustment += parseInt( this.getComputedStyle( sides[ type ][ i ] ) || 0, 10 ) || 0;
2074 return adjustment;
2075 }
2076
2077 /**
2078 * Sets the element size considering the box model.
2079 *
2080 * @param {'width'/'height'} type The dimension to set.
2081 * @param {Number} size The length unit in px.
2082 * @param {Boolean} isBorderBox Apply the size based on the border box model.
2083 */
2084 CKEDITOR.dom.element.prototype.setSize = function( type, size, isBorderBox ) {
2085 if ( typeof size == 'number' ) {
2086 if ( isBorderBox && !( CKEDITOR.env.ie && CKEDITOR.env.quirks ) )
2087 size -= marginAndPaddingSize.call( this, type );
2088
2089 this.setStyle( type, size + 'px' );
2090 }
2091 };
2092
2093 /**
2094 * Gets the element size, possibly considering the box model.
2095 *
2096 * @param {'width'/'height'} type The dimension to get.
2097 * @param {Boolean} isBorderBox Get the size based on the border box model.
2098 */
2099 CKEDITOR.dom.element.prototype.getSize = function( type, isBorderBox ) {
2100 var size = Math.max( this.$[ 'offset' + CKEDITOR.tools.capitalize( type ) ], this.$[ 'client' + CKEDITOR.tools.capitalize( type ) ] ) || 0;
2101
2102 if ( isBorderBox )
2103 size -= marginAndPaddingSize.call( this, type );
2104
2105 return size;
2106 };
2107} )();
diff --git a/sources/core/dom/elementpath.js b/sources/core/dom/elementpath.js
new file mode 100644
index 00000000..55b776d5
--- /dev/null
+++ b/sources/core/dom/elementpath.js
@@ -0,0 +1,251 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8( function() {
9
10 var pathBlockLimitElements = {},
11 pathBlockElements = {},
12 tag;
13
14 // Elements that are considered the "Block limit" in an element path.
15 for ( tag in CKEDITOR.dtd.$blockLimit ) {
16 // Exclude from list roots.
17 if ( !( tag in CKEDITOR.dtd.$list ) )
18 pathBlockLimitElements[ tag ] = 1;
19 }
20
21 // Elements that are considered the "End level Block" in an element path.
22 for ( tag in CKEDITOR.dtd.$block ) {
23 // Exclude block limits, and empty block element, e.g. hr.
24 if ( !( tag in CKEDITOR.dtd.$blockLimit || tag in CKEDITOR.dtd.$empty ) )
25 pathBlockElements[ tag ] = 1;
26 }
27
28 // Check if an element contains any block element.
29 function checkHasBlock( element ) {
30 var childNodes = element.getChildren();
31
32 for ( var i = 0, count = childNodes.count(); i < count; i++ ) {
33 var child = childNodes.getItem( i );
34
35 if ( child.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$block[ child.getName() ] )
36 return true;
37 }
38
39 return false;
40 }
41
42 /**
43 * Retrieve the list of nodes walked from the start node up to the editable element of the editor.
44 *
45 * @class
46 * @constructor Creates an element path class instance.
47 * @param {CKEDITOR.dom.element} startNode From which the path should start.
48 * @param {CKEDITOR.dom.element} root To which element the path should stop, defaults to the `body` element.
49 */
50 CKEDITOR.dom.elementPath = function( startNode, root ) {
51 var block = null,
52 blockLimit = null,
53 elements = [],
54 e = startNode,
55 elementName;
56
57 // Backward compact.
58 root = root || startNode.getDocument().getBody();
59
60 do {
61 if ( e.type == CKEDITOR.NODE_ELEMENT ) {
62 elements.push( e );
63
64 if ( !this.lastElement ) {
65 this.lastElement = e;
66
67 // If an object or non-editable element is fully selected at the end of the element path,
68 // it must not become the block limit.
69 if ( e.is( CKEDITOR.dtd.$object ) || e.getAttribute( 'contenteditable' ) == 'false' )
70 continue;
71 }
72
73 if ( e.equals( root ) )
74 break;
75
76 if ( !blockLimit ) {
77 elementName = e.getName();
78
79 // First editable element becomes a block limit, because it cannot be split.
80 if ( e.getAttribute( 'contenteditable' ) == 'true' )
81 blockLimit = e;
82 // "Else" because element cannot be both - block and block levelimit.
83 else if ( !block && pathBlockElements[ elementName ] )
84 block = e;
85
86 if ( pathBlockLimitElements[ elementName ] ) {
87 // End level DIV is considered as the block, if no block is available. (#525)
88 // But it must NOT be the root element (checked above).
89 if ( !block && elementName == 'div' && !checkHasBlock( e ) )
90 block = e;
91 else
92 blockLimit = e;
93 }
94 }
95 }
96 }
97 while ( ( e = e.getParent() ) );
98
99 // Block limit defaults to root.
100 if ( !blockLimit )
101 blockLimit = root;
102
103 /**
104 * First non-empty block element which:
105 *
106 * * is not a {@link CKEDITOR.dtd#$blockLimit},
107 * * or is a `div` which does not contain block elements and is not a `root`.
108 *
109 * This means a first, splittable block in elements path.
110 *
111 * @readonly
112 * @property {CKEDITOR.dom.element}
113 */
114 this.block = block;
115
116 /**
117 * See the {@link CKEDITOR.dtd#$blockLimit} description.
118 *
119 * @readonly
120 * @property {CKEDITOR.dom.element}
121 */
122 this.blockLimit = blockLimit;
123
124 /**
125 * The root of the elements path - `root` argument passed to class constructor or a `body` element.
126 *
127 * @readonly
128 * @property {CKEDITOR.dom.element}
129 */
130 this.root = root;
131
132 /**
133 * An array of elements (from `startNode` to `root`) in the path.
134 *
135 * @readonly
136 * @property {CKEDITOR.dom.element[]}
137 */
138 this.elements = elements;
139
140 /**
141 * The last element of the elements path - `startNode` or its parent.
142 *
143 * @readonly
144 * @property {CKEDITOR.dom.element} lastElement
145 */
146 };
147
148} )();
149
150CKEDITOR.dom.elementPath.prototype = {
151 /**
152 * Compares this element path with another one.
153 *
154 * @param {CKEDITOR.dom.elementPath} otherPath The elementPath object to be
155 * compared with this one.
156 * @returns {Boolean} `true` if the paths are equal, containing the same
157 * number of elements and the same elements in the same order.
158 */
159 compare: function( otherPath ) {
160 var thisElements = this.elements;
161 var otherElements = otherPath && otherPath.elements;
162
163 if ( !otherElements || thisElements.length != otherElements.length )
164 return false;
165
166 for ( var i = 0; i < thisElements.length; i++ ) {
167 if ( !thisElements[ i ].equals( otherElements[ i ] ) )
168 return false;
169 }
170
171 return true;
172 },
173
174 /**
175 * Search the path elements that meets the specified criteria.
176 *
177 * @param {String/Array/Function/Object/CKEDITOR.dom.element} query The criteria that can be
178 * either a tag name, list (array and object) of tag names, element or an node evaluator function.
179 * @param {Boolean} [excludeRoot] Not taking path root element into consideration.
180 * @param {Boolean} [fromTop] Search start from the topmost element instead of bottom.
181 * @returns {CKEDITOR.dom.element} The first matched dom element or `null`.
182 */
183 contains: function( query, excludeRoot, fromTop ) {
184 var evaluator;
185 if ( typeof query == 'string' )
186 evaluator = function( node ) {
187 return node.getName() == query;
188 };
189 if ( query instanceof CKEDITOR.dom.element )
190 evaluator = function( node ) {
191 return node.equals( query );
192 };
193 else if ( CKEDITOR.tools.isArray( query ) )
194 evaluator = function( node ) {
195 return CKEDITOR.tools.indexOf( query, node.getName() ) > -1;
196 };
197 else if ( typeof query == 'function' )
198 evaluator = query;
199 else if ( typeof query == 'object' )
200 evaluator = function( node ) {
201 return node.getName() in query;
202 };
203
204 var elements = this.elements,
205 length = elements.length;
206 excludeRoot && length--;
207
208 if ( fromTop ) {
209 elements = Array.prototype.slice.call( elements, 0 );
210 elements.reverse();
211 }
212
213 for ( var i = 0; i < length; i++ ) {
214 if ( evaluator( elements[ i ] ) )
215 return elements[ i ];
216 }
217
218 return null;
219 },
220
221 /**
222 * Check whether the elements path is the proper context for the specified
223 * tag name in the DTD.
224 *
225 * @param {String} tag The tag name.
226 * @returns {Boolean}
227 */
228 isContextFor: function( tag ) {
229 var holder;
230
231 // Check for block context.
232 if ( tag in CKEDITOR.dtd.$block ) {
233 // Indeterminate elements which are not subjected to be splitted or surrounded must be checked first.
234 var inter = this.contains( CKEDITOR.dtd.$intermediate );
235 holder = inter || ( this.root.equals( this.block ) && this.block ) || this.blockLimit;
236 return !!holder.getDtd()[ tag ];
237 }
238
239 return true;
240 },
241
242 /**
243 * Retrieve the text direction for this elements path.
244 *
245 * @returns {'ltr'/'rtl'}
246 */
247 direction: function() {
248 var directionNode = this.block || this.blockLimit || this.root;
249 return directionNode.getDirection( 1 );
250 }
251};
diff --git a/sources/core/dom/event.js b/sources/core/dom/event.js
new file mode 100644
index 00000000..ee88d78c
--- /dev/null
+++ b/sources/core/dom/event.js
@@ -0,0 +1,208 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.event} class, which
8 * represents the a native DOM event object.
9 */
10
11/**
12 * Represents a native DOM event object.
13 *
14 * @class
15 * @constructor Creates an event class instance.
16 * @param {Object} domEvent A native DOM event object.
17 */
18CKEDITOR.dom.event = function( domEvent ) {
19 /**
20 * The native DOM event object represented by this class instance.
21 *
22 * @readonly
23 */
24 this.$ = domEvent;
25};
26
27CKEDITOR.dom.event.prototype = {
28 /**
29 * Gets the key code associated to the event.
30 *
31 * alert( event.getKey() ); // '65' is 'a' has been pressed
32 *
33 * @returns {Number} The key code.
34 */
35 getKey: function() {
36 return this.$.keyCode || this.$.which;
37 },
38
39 /**
40 * Gets a number represeting the combination of the keys pressed during the
41 * event. It is the sum with the current key code and the {@link CKEDITOR#CTRL},
42 * {@link CKEDITOR#SHIFT} and {@link CKEDITOR#ALT} constants.
43 *
44 * alert( event.getKeystroke() == 65 ); // 'a' key
45 * alert( event.getKeystroke() == CKEDITOR.CTRL + 65 ); // CTRL + 'a' key
46 * alert( event.getKeystroke() == CKEDITOR.CTRL + CKEDITOR.SHIFT + 65 ); // CTRL + SHIFT + 'a' key
47 *
48 * @returns {Number} The number representing the keys combination.
49 */
50 getKeystroke: function() {
51 var keystroke = this.getKey();
52
53 if ( this.$.ctrlKey || this.$.metaKey )
54 keystroke += CKEDITOR.CTRL;
55
56 if ( this.$.shiftKey )
57 keystroke += CKEDITOR.SHIFT;
58
59 if ( this.$.altKey )
60 keystroke += CKEDITOR.ALT;
61
62 return keystroke;
63 },
64
65 /**
66 * Prevents the original behavior of the event to happen. It can optionally
67 * stop propagating the event in the event chain.
68 *
69 * var element = CKEDITOR.document.getById( 'myElement' );
70 * element.on( 'click', function( ev ) {
71 * // The DOM event object is passed by the 'data' property.
72 * var domEvent = ev.data;
73 * // Prevent the click to chave any effect in the element.
74 * domEvent.preventDefault();
75 * } );
76 *
77 * @param {Boolean} [stopPropagation=false] Stop propagating this event in the
78 * event chain.
79 */
80 preventDefault: function( stopPropagation ) {
81 var $ = this.$;
82 if ( $.preventDefault )
83 $.preventDefault();
84 else
85 $.returnValue = false;
86
87 if ( stopPropagation )
88 this.stopPropagation();
89 },
90
91 /**
92 * Stops this event propagation in the event chain.
93 */
94 stopPropagation: function() {
95 var $ = this.$;
96 if ( $.stopPropagation )
97 $.stopPropagation();
98 else
99 $.cancelBubble = true;
100 },
101
102 /**
103 * Returns the DOM node where the event was targeted to.
104 *
105 * var element = CKEDITOR.document.getById( 'myElement' );
106 * element.on( 'click', function( ev ) {
107 * // The DOM event object is passed by the 'data' property.
108 * var domEvent = ev.data;
109 * // Add a CSS class to the event target.
110 * domEvent.getTarget().addClass( 'clicked' );
111 * } );
112 *
113 * @returns {CKEDITOR.dom.node} The target DOM node.
114 */
115 getTarget: function() {
116 var rawNode = this.$.target || this.$.srcElement;
117 return rawNode ? new CKEDITOR.dom.node( rawNode ) : null;
118 },
119
120 /**
121 * Returns an integer value that indicates the current processing phase of an event.
122 * For browsers that doesn't support event phase, {@link CKEDITOR#EVENT_PHASE_AT_TARGET} is always returned.
123 *
124 * @returns {Number} One of {@link CKEDITOR#EVENT_PHASE_CAPTURING},
125 * {@link CKEDITOR#EVENT_PHASE_AT_TARGET}, or {@link CKEDITOR#EVENT_PHASE_BUBBLING}.
126 */
127 getPhase: function() {
128 return this.$.eventPhase || 2;
129 },
130
131 /**
132 * Retrieves the coordinates of the mouse pointer relative to the top-left
133 * corner of the document, in mouse related event.
134 *
135 * element.on( 'mousemouse', function( ev ) {
136 * var pageOffset = ev.data.getPageOffset();
137 * alert( pageOffset.x ); // page offset X
138 * alert( pageOffset.y ); // page offset Y
139 * } );
140 *
141 * @returns {Object} The object contains the position.
142 * @returns {Number} return.x
143 * @returns {Number} return.y
144 */
145 getPageOffset: function() {
146 var doc = this.getTarget().getDocument().$;
147 var pageX = this.$.pageX || this.$.clientX + ( doc.documentElement.scrollLeft || doc.body.scrollLeft );
148 var pageY = this.$.pageY || this.$.clientY + ( doc.documentElement.scrollTop || doc.body.scrollTop );
149 return { x: pageX, y: pageY };
150 }
151};
152
153// For the followind constants, we need to go over the Unicode boundaries
154// (0x10FFFF) to avoid collision.
155
156/**
157 * CTRL key (0x110000).
158 *
159 * @readonly
160 * @property {Number} [=0x110000]
161 * @member CKEDITOR
162 */
163CKEDITOR.CTRL = 0x110000;
164
165/**
166 * SHIFT key (0x220000).
167 *
168 * @readonly
169 * @property {Number} [=0x220000]
170 * @member CKEDITOR
171 */
172CKEDITOR.SHIFT = 0x220000;
173
174/**
175 * ALT key (0x440000).
176 *
177 * @readonly
178 * @property {Number} [=0x440000]
179 * @member CKEDITOR
180 */
181CKEDITOR.ALT = 0x440000;
182
183/**
184 * Capturing phase.
185 *
186 * @readonly
187 * @property {Number} [=1]
188 * @member CKEDITOR
189 */
190CKEDITOR.EVENT_PHASE_CAPTURING = 1;
191
192/**
193 * Event at target.
194 *
195 * @readonly
196 * @property {Number} [=2]
197 * @member CKEDITOR
198 */
199CKEDITOR.EVENT_PHASE_AT_TARGET = 2;
200
201/**
202 * Bubbling phase.
203 *
204 * @readonly
205 * @property {Number} [=3]
206 * @member CKEDITOR
207 */
208CKEDITOR.EVENT_PHASE_BUBBLING = 3;
diff --git a/sources/core/dom/iterator.js b/sources/core/dom/iterator.js
new file mode 100644
index 00000000..99491218
--- /dev/null
+++ b/sources/core/dom/iterator.js
@@ -0,0 +1,565 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @ignore
8 * File overview: DOM iterator which iterates over list items, lines and paragraphs.
9 */
10
11'use strict';
12
13( function() {
14 /**
15 * Represents the iterator class. It can be used to iterate
16 * over all elements (or even text nodes in case of {@link #enlargeBr} set to `false`)
17 * which establish "paragraph-like" spaces within the passed range.
18 *
19 * // <h1>[foo</h1><p>bar]</p>
20 * var iterator = range.createIterator();
21 * iterator.getNextParagraph(); // h1 element
22 * iterator.getNextParagraph(); // p element
23 *
24 * // <ul><li>[foo</li><li>bar]</li>
25 * // With enforceRealBlocks set to false the iterator will return two list item elements.
26 * // With enforceRealBlocks set to true the iterator will return two paragraphs and the DOM will be changed to:
27 * // <ul><li><p>foo</p></li><li><p>bar</p></li>
28 *
29 * @class CKEDITOR.dom.iterator
30 * @constructor Creates an iterator class instance.
31 * @param {CKEDITOR.dom.range} range
32 */
33 function iterator( range ) {
34 if ( arguments.length < 1 )
35 return;
36
37 /**
38 * @readonly
39 * @property {CKEDITOR.dom.range}
40 */
41 this.range = range;
42
43 /**
44 * @property {Boolean} [forceBrBreak=false]
45 */
46 this.forceBrBreak = 0;
47
48 // (#3730).
49 /**
50 * Whether to include `<br>` elements in the enlarged range. Should be
51 * set to `false` when using the iterator in the {@link CKEDITOR#ENTER_BR} mode.
52 *
53 * @property {Boolean} [enlargeBr=true]
54 */
55 this.enlargeBr = 1;
56
57 /**
58 * Whether the iterator should create a transformable block
59 * if the current one contains text and cannot be transformed.
60 * For example new blocks will be established in elements like
61 * `<li>` or `<td>`.
62 *
63 * @property {Boolean} [enforceRealBlocks=false]
64 */
65 this.enforceRealBlocks = 0;
66
67 this._ || ( this._ = {} );
68 }
69
70 /**
71 * Default iterator's filter. It is set only for nested iterators.
72 *
73 * @since 4.3
74 * @readonly
75 * @property {CKEDITOR.filter} filter
76 */
77
78 /**
79 * Iterator's active filter. It is set by the {@link #getNextParagraph} method
80 * when it enters a nested editable.
81 *
82 * @since 4.3
83 * @readonly
84 * @property {CKEDITOR.filter} activeFilter
85 */
86
87 var beginWhitespaceRegex = /^[\r\n\t ]+$/,
88 // Ignore bookmark nodes.(#3783)
89 bookmarkGuard = CKEDITOR.dom.walker.bookmark( false, true ),
90 whitespacesGuard = CKEDITOR.dom.walker.whitespaces( true ),
91 skipGuard = function( node ) {
92 return bookmarkGuard( node ) && whitespacesGuard( node );
93 },
94 listItemNames = { dd: 1, dt: 1, li: 1 };
95
96 iterator.prototype = {
97 /**
98 * Returns the next paragraph-like element or `null` if the end of a range is reached.
99 *
100 * @param {String} [blockTag='p'] Name of a block element which will be established by
101 * the iterator in block-less elements (see {@link #enforceRealBlocks}).
102 */
103 getNextParagraph: function( blockTag ) {
104 // The block element to be returned.
105 var block;
106
107 // The range object used to identify the paragraph contents.
108 var range;
109
110 // Indicats that the current element in the loop is the last one.
111 var isLast;
112
113 // Instructs to cleanup remaining BRs.
114 var removePreviousBr, removeLastBr;
115
116 blockTag = blockTag || 'p';
117
118 // We're iterating over nested editable.
119 if ( this._.nestedEditable ) {
120 // Get next block from nested iterator and returns it if was found.
121 block = this._.nestedEditable.iterator.getNextParagraph( blockTag );
122 if ( block ) {
123 // Inherit activeFilter from the nested iterator.
124 this.activeFilter = this._.nestedEditable.iterator.activeFilter;
125 return block;
126 }
127
128 // No block in nested iterator means that we reached the end of the nested editable.
129 // Reset the active filter to the default filter (or undefined if this iterator didn't have it).
130 this.activeFilter = this.filter;
131
132 // Try to find next nested editable or get back to parent (this) iterator.
133 if ( startNestedEditableIterator( this, blockTag, this._.nestedEditable.container, this._.nestedEditable.remaining ) ) {
134 // Inherit activeFilter from the nested iterator.
135 this.activeFilter = this._.nestedEditable.iterator.activeFilter;
136 return this._.nestedEditable.iterator.getNextParagraph( blockTag );
137 } else {
138 this._.nestedEditable = null;
139 }
140 }
141
142 // Block-less range should be checked first.
143 if ( !this.range.root.getDtd()[ blockTag ] )
144 return null;
145
146 // This is the first iteration. Let's initialize it.
147 if ( !this._.started )
148 range = startIterator.call( this );
149
150 var currentNode = this._.nextNode,
151 lastNode = this._.lastNode;
152
153 this._.nextNode = null;
154 while ( currentNode ) {
155 // closeRange indicates that a paragraph boundary has been found,
156 // so the range can be closed.
157 var closeRange = 0,
158 parentPre = currentNode.hasAscendant( 'pre' );
159
160 // includeNode indicates that the current node is good to be part
161 // of the range. By default, any non-element node is ok for it.
162 var includeNode = ( currentNode.type != CKEDITOR.NODE_ELEMENT ),
163 continueFromSibling = 0;
164
165 // If it is an element node, let's check if it can be part of the range.
166 if ( !includeNode ) {
167 var nodeName = currentNode.getName();
168
169 // Non-editable block was found - return it and move to processing
170 // its nested editables if they exist.
171 if ( CKEDITOR.dtd.$block[ nodeName ] && currentNode.getAttribute( 'contenteditable' ) == 'false' ) {
172 block = currentNode;
173
174 // Setup iterator for first of nested editables.
175 // If there's no editable, then algorithm will move to next element after current block.
176 startNestedEditableIterator( this, blockTag, block );
177
178 // Gets us straight to the end of getParagraph() because block variable is set.
179 break;
180 } else if ( currentNode.isBlockBoundary( this.forceBrBreak && !parentPre && { br: 1 } ) ) {
181 // <br> boundaries must be part of the range. It will
182 // happen only if ForceBrBreak.
183 if ( nodeName == 'br' )
184 includeNode = 1;
185 else if ( !range && !currentNode.getChildCount() && nodeName != 'hr' ) {
186 // If we have found an empty block, and haven't started
187 // the range yet, it means we must return this block.
188 block = currentNode;
189 isLast = currentNode.equals( lastNode );
190 break;
191 }
192
193 // The range must finish right before the boundary,
194 // including possibly skipped empty spaces. (#1603)
195 if ( range ) {
196 range.setEndAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
197
198 // The found boundary must be set as the next one at this
199 // point. (#1717)
200 if ( nodeName != 'br' ) {
201 this._.nextNode = currentNode;
202 }
203 }
204
205 closeRange = 1;
206 } else {
207 // If we have child nodes, let's check them.
208 if ( currentNode.getFirst() ) {
209 // If we don't have a range yet, let's start it.
210 if ( !range ) {
211 range = this.range.clone();
212 range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
213 }
214
215 currentNode = currentNode.getFirst();
216 continue;
217 }
218 includeNode = 1;
219 }
220 } else if ( currentNode.type == CKEDITOR.NODE_TEXT ) {
221 // Ignore normal whitespaces (i.e. not including &nbsp; or
222 // other unicode whitespaces) before/after a block node.
223 if ( beginWhitespaceRegex.test( currentNode.getText() ) )
224 includeNode = 0;
225 }
226
227 // The current node is good to be part of the range and we are
228 // starting a new range, initialize it first.
229 if ( includeNode && !range ) {
230 range = this.range.clone();
231 range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
232 }
233
234 // The last node has been found.
235 isLast = ( ( !closeRange || includeNode ) && currentNode.equals( lastNode ) );
236
237 // If we are in an element boundary, let's check if it is time
238 // to close the range, otherwise we include the parent within it.
239 if ( range && !closeRange ) {
240 while ( !currentNode.getNext( skipGuard ) && !isLast ) {
241 var parentNode = currentNode.getParent();
242
243 if ( parentNode.isBlockBoundary( this.forceBrBreak && !parentPre && { br: 1 } ) ) {
244 closeRange = 1;
245 includeNode = 0;
246 isLast = isLast || ( parentNode.equals( lastNode ) );
247 // Make sure range includes bookmarks at the end of the block. (#7359)
248 range.setEndAt( parentNode, CKEDITOR.POSITION_BEFORE_END );
249 break;
250 }
251
252 currentNode = parentNode;
253 includeNode = 1;
254 isLast = ( currentNode.equals( lastNode ) );
255 continueFromSibling = 1;
256 }
257 }
258
259 // Now finally include the node.
260 if ( includeNode )
261 range.setEndAt( currentNode, CKEDITOR.POSITION_AFTER_END );
262
263 currentNode = this._getNextSourceNode( currentNode, continueFromSibling, lastNode );
264 isLast = !currentNode;
265
266 // We have found a block boundary. Let's close the range and move out of the
267 // loop.
268 if ( isLast || ( closeRange && range ) )
269 break;
270 }
271
272 // Now, based on the processed range, look for (or create) the block to be returned.
273 if ( !block ) {
274 // If no range has been found, this is the end.
275 if ( !range ) {
276 this._.docEndMarker && this._.docEndMarker.remove();
277 this._.nextNode = null;
278 return null;
279 }
280
281 var startPath = new CKEDITOR.dom.elementPath( range.startContainer, range.root );
282 var startBlockLimit = startPath.blockLimit,
283 checkLimits = { div: 1, th: 1, td: 1 };
284 block = startPath.block;
285
286 if ( !block && startBlockLimit && !this.enforceRealBlocks && checkLimits[ startBlockLimit.getName() ] &&
287 range.checkStartOfBlock() && range.checkEndOfBlock() && !startBlockLimit.equals( range.root ) ) {
288 block = startBlockLimit;
289 } else if ( !block || ( this.enforceRealBlocks && block.is( listItemNames ) ) ) {
290 // Create the fixed block.
291 block = this.range.document.createElement( blockTag );
292
293 // Move the contents of the temporary range to the fixed block.
294 range.extractContents().appendTo( block );
295 block.trim();
296
297 // Insert the fixed block into the DOM.
298 range.insertNode( block );
299
300 removePreviousBr = removeLastBr = true;
301 } else if ( block.getName() != 'li' ) {
302 // If the range doesn't includes the entire contents of the
303 // block, we must split it, isolating the range in a dedicated
304 // block.
305 if ( !range.checkStartOfBlock() || !range.checkEndOfBlock() ) {
306 // The resulting block will be a clone of the current one.
307 block = block.clone( false );
308
309 // Extract the range contents, moving it to the new block.
310 range.extractContents().appendTo( block );
311 block.trim();
312
313 // Split the block. At this point, the range will be in the
314 // right position for our intents.
315 var splitInfo = range.splitBlock();
316
317 removePreviousBr = !splitInfo.wasStartOfBlock;
318 removeLastBr = !splitInfo.wasEndOfBlock;
319
320 // Insert the new block into the DOM.
321 range.insertNode( block );
322 }
323 } else if ( !isLast ) {
324 // LIs are returned as is, with all their children (due to the
325 // nested lists). But, the next node is the node right after
326 // the current range, which could be an <li> child (nested
327 // lists) or the next sibling <li>.
328
329 this._.nextNode = ( block.equals( lastNode ) ? null : this._getNextSourceNode( range.getBoundaryNodes().endNode, 1, lastNode ) );
330 }
331 }
332
333 if ( removePreviousBr ) {
334 var previousSibling = block.getPrevious();
335 if ( previousSibling && previousSibling.type == CKEDITOR.NODE_ELEMENT ) {
336 if ( previousSibling.getName() == 'br' )
337 previousSibling.remove();
338 else if ( previousSibling.getLast() && previousSibling.getLast().$.nodeName.toLowerCase() == 'br' )
339 previousSibling.getLast().remove();
340 }
341 }
342
343 if ( removeLastBr ) {
344 var lastChild = block.getLast();
345 if ( lastChild && lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.getName() == 'br' ) {
346 // Remove br filler on browser which do not need it.
347 if ( !CKEDITOR.env.needsBrFiller || lastChild.getPrevious( bookmarkGuard ) || lastChild.getNext( bookmarkGuard ) )
348 lastChild.remove();
349 }
350 }
351
352 // Get a reference for the next element. This is important because the
353 // above block can be removed or changed, so we can rely on it for the
354 // next interation.
355 if ( !this._.nextNode ) {
356 this._.nextNode = ( isLast || block.equals( lastNode ) || !lastNode ) ? null : this._getNextSourceNode( block, 1, lastNode );
357 }
358
359 return block;
360 },
361
362 /**
363 * Gets the next element to check or `null` when the `lastNode` or the
364 * {@link #range}'s {@link CKEDITOR.dom.range#root root} is reached. Bookmarks are skipped.
365 *
366 * @since 4.4.6
367 * @private
368 * @param {CKEDITOR.dom.node} node
369 * @param {Boolean} startFromSibling
370 * @param {CKEDITOR.dom.node} lastNode
371 * @returns {CKEDITOR.dom.node}
372 */
373 _getNextSourceNode: function( node, startFromSibling, lastNode ) {
374 var rootNode = this.range.root,
375 next;
376
377 // Here we are checking in guard function whether current element
378 // reach lastNode(default behaviour) and root node to prevent against
379 // getting out of editor instance root DOM object.
380 // #12484
381 function guardFunction( node ) {
382 return !( node.equals( lastNode ) || node.equals( rootNode ) );
383 }
384
385 next = node.getNextSourceNode( startFromSibling, null, guardFunction );
386 while ( !bookmarkGuard( next ) ) {
387 next = next.getNextSourceNode( startFromSibling, null, guardFunction );
388 }
389 return next;
390 }
391 };
392
393 // @context CKEDITOR.dom.iterator
394 // @returns Collapsed range which will be reused when during furter processing.
395 function startIterator() {
396 var range = this.range.clone(),
397 // Indicate at least one of the range boundaries is inside a preformat block.
398 touchPre,
399
400 // (#12178)
401 // Remember if following situation takes place:
402 // * startAtInnerBoundary: <p>foo[</p>...
403 // * endAtInnerBoundary: ...<p>]bar</p>
404 // Because information about line break will be lost when shrinking range.
405 // Note that we test only if path block exist, because we must properly shrink
406 // range containing table and/or table cells.
407 // Note: When range is collapsed there's no way it can be shrinked.
408 // By checking if range is collapsed we also prevent #12308.
409 startPath = range.startPath(),
410 endPath = range.endPath(),
411 startAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, startPath.block ),
412 endAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, endPath.block, 1 );
413
414 // Shrink the range to exclude harmful "noises" (#4087, #4450, #5435).
415 range.shrink( CKEDITOR.SHRINK_ELEMENT, true );
416
417 if ( startAtInnerBoundary )
418 range.setStartAt( startPath.block, CKEDITOR.POSITION_BEFORE_END );
419 if ( endAtInnerBoundary )
420 range.setEndAt( endPath.block, CKEDITOR.POSITION_AFTER_START );
421
422 touchPre = range.endContainer.hasAscendant( 'pre', true ) || range.startContainer.hasAscendant( 'pre', true );
423
424 range.enlarge( this.forceBrBreak && !touchPre || !this.enlargeBr ? CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR.ENLARGE_BLOCK_CONTENTS );
425
426 if ( !range.collapsed ) {
427 var walker = new CKEDITOR.dom.walker( range.clone() ),
428 ignoreBookmarkTextEvaluator = CKEDITOR.dom.walker.bookmark( true, true );
429 // Avoid anchor inside bookmark inner text.
430 walker.evaluator = ignoreBookmarkTextEvaluator;
431 this._.nextNode = walker.next();
432 // TODO: It's better to have walker.reset() used here.
433 walker = new CKEDITOR.dom.walker( range.clone() );
434 walker.evaluator = ignoreBookmarkTextEvaluator;
435 var lastNode = walker.previous();
436 this._.lastNode = lastNode.getNextSourceNode( true, null, range.root );
437
438 // We may have an empty text node at the end of block due to [3770].
439 // If that node is the lastNode, it would cause our logic to leak to the
440 // next block.(#3887)
441 if ( this._.lastNode && this._.lastNode.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.trim( this._.lastNode.getText() ) && this._.lastNode.getParent().isBlockBoundary() ) {
442 var testRange = this.range.clone();
443 testRange.moveToPosition( this._.lastNode, CKEDITOR.POSITION_AFTER_END );
444 if ( testRange.checkEndOfBlock() ) {
445 var path = new CKEDITOR.dom.elementPath( testRange.endContainer, testRange.root ),
446 lastBlock = path.block || path.blockLimit;
447 this._.lastNode = lastBlock.getNextSourceNode( true );
448 }
449 }
450
451 // The end of document or range.root was reached, so we need a marker node inside.
452 if ( !this._.lastNode || !range.root.contains( this._.lastNode ) ) {
453 this._.lastNode = this._.docEndMarker = range.document.createText( '' );
454 this._.lastNode.insertAfter( lastNode );
455 }
456
457 // Let's reuse this variable.
458 range = null;
459 }
460
461 this._.started = 1;
462
463 return range;
464 }
465
466 // Does a nested editables lookup inside editablesContainer.
467 // If remainingEditables is set will lookup inside this array.
468 // @param {CKEDITOR.dom.element} editablesContainer
469 // @param {CKEDITOR.dom.element[]} [remainingEditables]
470 function getNestedEditableIn( editablesContainer, remainingEditables ) {
471 if ( remainingEditables == null )
472 remainingEditables = findNestedEditables( editablesContainer );
473
474 var editable;
475
476 while ( ( editable = remainingEditables.shift() ) ) {
477 if ( isIterableEditable( editable ) )
478 return { element: editable, remaining: remainingEditables };
479 }
480
481 return null;
482 }
483
484 // Checkes whether we can iterate over this editable.
485 function isIterableEditable( editable ) {
486 // Reject blockless editables.
487 return editable.getDtd().p;
488 }
489
490 // Finds nested editables within container. Does not return
491 // editables nested in another editable (twice).
492 function findNestedEditables( container ) {
493 var editables = [];
494
495 container.forEach( function( element ) {
496 if ( element.getAttribute( 'contenteditable' ) == 'true' ) {
497 editables.push( element );
498 return false; // Skip children.
499 }
500 }, CKEDITOR.NODE_ELEMENT, true );
501
502 return editables;
503 }
504
505 // Looks for a first nested editable after previousEditable (if passed) and creates
506 // nested iterator for it.
507 function startNestedEditableIterator( parentIterator, blockTag, editablesContainer, remainingEditables ) {
508 var editable = getNestedEditableIn( editablesContainer, remainingEditables );
509
510 if ( !editable )
511 return 0;
512
513 var filter = CKEDITOR.filter.instances[ editable.element.data( 'cke-filter' ) ];
514
515 // If current editable has a filter and this filter does not allow for block tag,
516 // search for next nested editable in remaining ones.
517 if ( filter && !filter.check( blockTag ) )
518 return startNestedEditableIterator( parentIterator, blockTag, editablesContainer, editable.remaining );
519
520 var range = new CKEDITOR.dom.range( editable.element );
521 range.selectNodeContents( editable.element );
522
523 var iterator = range.createIterator();
524 // This setting actually does not change anything in this case,
525 // because entire range contents is selected, so there're no <br>s to be included.
526 // But it seems right to copy it too.
527 iterator.enlargeBr = parentIterator.enlargeBr;
528 // Inherit configuration from parent iterator.
529 iterator.enforceRealBlocks = parentIterator.enforceRealBlocks;
530 // Set the activeFilter (which can be overriden when this iteator will start nested iterator)
531 // and the default filter, which will make it possible to reset to
532 // current iterator's activeFilter after leaving nested editable.
533 iterator.activeFilter = iterator.filter = filter;
534
535 parentIterator._.nestedEditable = {
536 element: editable.element,
537 container: editablesContainer,
538 remaining: editable.remaining,
539 iterator: iterator
540 };
541
542 return 1;
543 }
544
545 // Checks whether range starts or ends at inner block boundary.
546 // See usage comments to learn more.
547 function rangeAtInnerBlockBoundary( range, block, checkEnd ) {
548 if ( !block )
549 return false;
550
551 var testRange = range.clone();
552 testRange.collapse( !checkEnd );
553 return testRange.checkBoundaryOfElement( block, checkEnd ? CKEDITOR.START : CKEDITOR.END );
554 }
555
556 /**
557 * Creates a {CKEDITOR.dom.iterator} instance for this range.
558 *
559 * @member CKEDITOR.dom.range
560 * @returns {CKEDITOR.dom.iterator}
561 */
562 CKEDITOR.dom.range.prototype.createIterator = function() {
563 return new iterator( this );
564 };
565} )();
diff --git a/sources/core/dom/node.js b/sources/core/dom/node.js
new file mode 100644
index 00000000..5d791319
--- /dev/null
+++ b/sources/core/dom/node.js
@@ -0,0 +1,897 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.node} class which is the base
8 * class for classes that represent DOM nodes.
9 */
10
11/**
12 * Base class for classes representing DOM nodes. This constructor may return
13 * an instance of a class that inherits from this class, like
14 * {@link CKEDITOR.dom.element} or {@link CKEDITOR.dom.text}.
15 *
16 * @class
17 * @extends CKEDITOR.dom.domObject
18 * @constructor Creates a node class instance.
19 * @param {Object} domNode A native DOM node.
20 * @see CKEDITOR.dom.element
21 * @see CKEDITOR.dom.text
22 */
23CKEDITOR.dom.node = function( domNode ) {
24 if ( domNode ) {
25 var type =
26 domNode.nodeType == CKEDITOR.NODE_DOCUMENT ? 'document' :
27 domNode.nodeType == CKEDITOR.NODE_ELEMENT ? 'element' :
28 domNode.nodeType == CKEDITOR.NODE_TEXT ? 'text' :
29 domNode.nodeType == CKEDITOR.NODE_COMMENT ? 'comment' :
30 domNode.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ? 'documentFragment' :
31 'domObject'; // Call the base constructor otherwise.
32
33 return new CKEDITOR.dom[ type ]( domNode );
34 }
35
36 return this;
37};
38
39CKEDITOR.dom.node.prototype = new CKEDITOR.dom.domObject();
40
41/**
42 * Element node type.
43 *
44 * @readonly
45 * @property {Number} [=1]
46 * @member CKEDITOR
47 */
48CKEDITOR.NODE_ELEMENT = 1;
49
50/**
51 * Document node type.
52 *
53 * @readonly
54 * @property {Number} [=9]
55 * @member CKEDITOR
56 */
57CKEDITOR.NODE_DOCUMENT = 9;
58
59/**
60 * Text node type.
61 *
62 * @readonly
63 * @property {Number} [=3]
64 * @member CKEDITOR
65 */
66CKEDITOR.NODE_TEXT = 3;
67
68/**
69 * Comment node type.
70 *
71 * @readonly
72 * @property {Number} [=8]
73 * @member CKEDITOR
74 */
75CKEDITOR.NODE_COMMENT = 8;
76
77/**
78 * Document fragment node type.
79 *
80 * @readonly
81 * @property {Number} [=11]
82 * @member CKEDITOR
83 */
84CKEDITOR.NODE_DOCUMENT_FRAGMENT = 11;
85
86/**
87 * Indicates that positions of both nodes are identical (this is the same node). See {@link CKEDITOR.dom.node#getPosition}.
88 *
89 * @readonly
90 * @property {Number} [=0]
91 * @member CKEDITOR
92 */
93CKEDITOR.POSITION_IDENTICAL = 0;
94
95/**
96 * Indicates that nodes are in different (detached) trees. See {@link CKEDITOR.dom.node#getPosition}.
97 *
98 * @readonly
99 * @property {Number} [=1]
100 * @member CKEDITOR
101 */
102CKEDITOR.POSITION_DISCONNECTED = 1;
103
104/**
105 * Indicates that the context node follows the other node. See {@link CKEDITOR.dom.node#getPosition}.
106 *
107 * @readonly
108 * @property {Number} [=2]
109 * @member CKEDITOR
110 */
111CKEDITOR.POSITION_FOLLOWING = 2;
112
113/**
114 * Indicates that the context node precedes the other node. See {@link CKEDITOR.dom.node#getPosition}.
115 *
116 * @readonly
117 * @property {Number} [=4]
118 * @member CKEDITOR
119 */
120CKEDITOR.POSITION_PRECEDING = 4;
121
122/**
123 * Indicates that the context node is a descendant of the other node. See {@link CKEDITOR.dom.node#getPosition}.
124 *
125 * @readonly
126 * @property {Number} [=8]
127 * @member CKEDITOR
128 */
129CKEDITOR.POSITION_IS_CONTAINED = 8;
130
131/**
132 * Indicates that the context node contains the other node. See {@link CKEDITOR.dom.node#getPosition}.
133 *
134 * @readonly
135 * @property {Number} [=16]
136 * @member CKEDITOR
137 */
138CKEDITOR.POSITION_CONTAINS = 16;
139
140CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, {
141 /**
142 * Makes this node a child of another element.
143 *
144 * var p = new CKEDITOR.dom.element( 'p' );
145 * var strong = new CKEDITOR.dom.element( 'strong' );
146 * strong.appendTo( p );
147 *
148 * // Result: '<p><strong></strong></p>'.
149 *
150 * @param {CKEDITOR.dom.element} element The target element to which this node will be appended.
151 * @returns {CKEDITOR.dom.element} The target element.
152 */
153 appendTo: function( element, toStart ) {
154 element.append( this, toStart );
155 return element;
156 },
157
158 /**
159 * Clones this node.
160 *
161 * **Note**: Values set by {#setCustomData} will not be available in the clone.
162 *
163 * @param {Boolean} [includeChildren=false] If `true` then all node's
164 * children will be cloned recursively.
165 * @param {Boolean} [cloneId=false] Whether ID attributes should be cloned, too.
166 * @returns {CKEDITOR.dom.node} Clone of this node.
167 */
168 clone: function( includeChildren, cloneId ) {
169 var $clone = this.$.cloneNode( includeChildren );
170
171 // The "id" attribute should never be cloned to avoid duplication.
172 removeIds( $clone );
173
174 var node = new CKEDITOR.dom.node( $clone );
175
176 // On IE8 we need to fixed HTML5 node name, see details below.
177 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 &&
178 ( this.type == CKEDITOR.NODE_ELEMENT || this.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) {
179 renameNodes( node );
180 }
181
182 return node;
183
184 function removeIds( node ) {
185 // Reset data-cke-expando only when has been cloned (IE and only for some types of objects).
186 if ( node[ 'data-cke-expando' ] )
187 node[ 'data-cke-expando' ] = false;
188
189 if ( node.nodeType != CKEDITOR.NODE_ELEMENT && node.nodeType != CKEDITOR.NODE_DOCUMENT_FRAGMENT )
190 return;
191
192 if ( !cloneId && node.nodeType == CKEDITOR.NODE_ELEMENT )
193 node.removeAttribute( 'id', false );
194
195 if ( includeChildren ) {
196 var childs = node.childNodes;
197 for ( var i = 0; i < childs.length; i++ )
198 removeIds( childs[ i ] );
199 }
200 }
201
202 // IE8 rename HTML5 nodes by adding `:` at the begging of the tag name when the node is cloned,
203 // so `<figure>` will be `<:figure>` after 'cloneNode'. We need to fix it (#13101).
204 function renameNodes( node ) {
205 if ( node.type != CKEDITOR.NODE_ELEMENT && node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT )
206 return;
207
208 if ( node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) {
209 var name = node.getName();
210 if ( name[ 0 ] == ':' ) {
211 node.renameNode( name.substring( 1 ) );
212 }
213 }
214
215 if ( includeChildren ) {
216 for ( var i = 0; i < node.getChildCount(); i++ )
217 renameNodes( node.getChild( i ) );
218 }
219 }
220 },
221
222 /**
223 * Checks if the node is preceded by any sibling.
224 *
225 * @returns {Boolean}
226 */
227 hasPrevious: function() {
228 return !!this.$.previousSibling;
229 },
230
231 /**
232 * Checks if the node is succeeded by any sibling.
233 *
234 * @returns {Boolean}
235 */
236 hasNext: function() {
237 return !!this.$.nextSibling;
238 },
239
240 /**
241 * Inserts this element after a node.
242 *
243 * var em = new CKEDITOR.dom.element( 'em' );
244 * var strong = new CKEDITOR.dom.element( 'strong' );
245 * strong.insertAfter( em );
246 *
247 * // Result: '<em></em><strong></strong>'
248 *
249 * @param {CKEDITOR.dom.node} node The node that will precede this element.
250 * @returns {CKEDITOR.dom.node} The node preceding this one after insertion.
251 */
252 insertAfter: function( node ) {
253 node.$.parentNode.insertBefore( this.$, node.$.nextSibling );
254 return node;
255 },
256
257 /**
258 * Inserts this element before a node.
259 *
260 * var em = new CKEDITOR.dom.element( 'em' );
261 * var strong = new CKEDITOR.dom.element( 'strong' );
262 * strong.insertBefore( em );
263 *
264 * // result: '<strong></strong><em></em>'
265 *
266 * @param {CKEDITOR.dom.node} node The node that will succeed this element.
267 * @returns {CKEDITOR.dom.node} The node being inserted.
268 */
269 insertBefore: function( node ) {
270 node.$.parentNode.insertBefore( this.$, node.$ );
271 return node;
272 },
273
274 /**
275 * Inserts a node before this node.
276 *
277 * var em = new CKEDITOR.dom.element( 'em' );
278 * var strong = new CKEDITOR.dom.element( 'strong' );
279 * strong.insertBeforeMe( em );
280 *
281 * // result: '<em></em><strong></strong>'
282 *
283 * @param {CKEDITOR.dom.node} node The node that will preceed this element.
284 * @returns {CKEDITOR.dom.node} The node being inserted.
285 */
286 insertBeforeMe: function( node ) {
287 this.$.parentNode.insertBefore( node.$, this.$ );
288 return node;
289 },
290
291 /**
292 * Retrieves a uniquely identifiable tree address for this node.
293 * The tree address returned is an array of integers, with each integer
294 * indicating a child index of a DOM node, starting from
295 * `document.documentElement`.
296 *
297 * For example, assuming `<body>` is the second child
298 * of `<html>` (`<head>` being the first),
299 * and we would like to address the third child under the
300 * fourth child of `<body>`, the tree address returned would be:
301 * `[1, 3, 2]`.
302 *
303 * The tree address cannot be used for finding back the DOM tree node once
304 * the DOM tree structure has been modified.
305 *
306 * @param {Boolean} [normalized=false] See {@link #getIndex}.
307 * @returns {Array} The address.
308 */
309 getAddress: function( normalized ) {
310 var address = [];
311 var $documentElement = this.getDocument().$.documentElement;
312 var node = this.$;
313
314 while ( node && node != $documentElement ) {
315 var parentNode = node.parentNode;
316
317 if ( parentNode ) {
318 // Get the node index. For performance, call getIndex
319 // directly, instead of creating a new node object.
320 address.unshift( this.getIndex.call( { $: node }, normalized ) );
321 }
322
323 node = parentNode;
324 }
325
326 return address;
327 },
328
329 /**
330 * Gets the document containing this element.
331 *
332 * var element = CKEDITOR.document.getById( 'example' );
333 * alert( element.getDocument().equals( CKEDITOR.document ) ); // true
334 *
335 * @returns {CKEDITOR.dom.document} The document.
336 */
337 getDocument: function() {
338 return new CKEDITOR.dom.document( this.$.ownerDocument || this.$.parentNode.ownerDocument );
339 },
340
341 /**
342 * Gets the index of a node in an array of its `parent.childNodes`.
343 * Returns `-1` if a node does not have a parent or when the `normalized` argument is set to `true`
344 * and the text node is empty and will be removed during the normalization.
345 *
346 * Let us assume having the following `childNodes` array:
347 *
348 * [ emptyText, element1, text, text, element2, emptyText2 ]
349 *
350 * emptyText.getIndex() // 0
351 * emptyText.getIndex( true ) // -1
352 * element1.getIndex(); // 1
353 * element1.getIndex( true ); // 0
354 * element2.getIndex(); // 4
355 * element2.getIndex( true ); // 2
356 * emptyText2.getIndex(); // 5
357 * emptyText2.getIndex( true ); // -1
358 *
359 * @param {Boolean} normalized When `true`, adjacent text nodes are merged and empty text nodes are removed.
360 * @returns {Number} Index of a node or `-1` if a node does not have a parent or is removed during the normalization.
361 */
362 getIndex: function( normalized ) {
363 // Attention: getAddress depends on this.$
364 // getIndex is called on a plain object: { $ : node }
365
366 var current = this.$,
367 index = -1,
368 isNormalizing;
369
370 if ( !this.$.parentNode )
371 return -1;
372
373 // The idea is - all empty text nodes will be virtually merged into their adjacent text nodes.
374 // If an empty text node does not have an adjacent non-empty text node we can return -1 straight away,
375 // because it and all its sibling text nodes will be merged into an empty text node and then totally ignored.
376 if ( normalized && current.nodeType == CKEDITOR.NODE_TEXT && !current.nodeValue ) {
377 var adjacent = getAdjacentNonEmptyTextNode( current ) || getAdjacentNonEmptyTextNode( current, true );
378
379 if ( !adjacent )
380 return -1;
381 }
382
383 do {
384 // Bypass blank node and adjacent text nodes.
385 if ( normalized && current != this.$ && current.nodeType == CKEDITOR.NODE_TEXT && ( isNormalizing || !current.nodeValue ) )
386 continue;
387
388 index++;
389 isNormalizing = current.nodeType == CKEDITOR.NODE_TEXT;
390 }
391 while ( ( current = current.previousSibling ) );
392
393 return index;
394
395 function getAdjacentNonEmptyTextNode( node, lookForward ) {
396 var sibling = lookForward ? node.nextSibling : node.previousSibling;
397
398 if ( !sibling || sibling.nodeType != CKEDITOR.NODE_TEXT ) {
399 return null;
400 }
401
402 // If found a non-empty text node, then return it.
403 // If not, then continue search.
404 return sibling.nodeValue ? sibling : getAdjacentNonEmptyTextNode( sibling, lookForward );
405 }
406 },
407
408 /**
409 * @todo
410 */
411 getNextSourceNode: function( startFromSibling, nodeType, guard ) {
412 // If "guard" is a node, transform it in a function.
413 if ( guard && !guard.call ) {
414 var guardNode = guard;
415 guard = function( node ) {
416 return !node.equals( guardNode );
417 };
418 }
419
420 var node = ( !startFromSibling && this.getFirst && this.getFirst() ),
421 parent;
422
423 // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
424 // send the 'moving out' signal even we don't actually dive into.
425 if ( !node ) {
426 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
427 return null;
428 node = this.getNext();
429 }
430
431 while ( !node && ( parent = ( parent || this ).getParent() ) ) {
432 // The guard check sends the "true" paramenter to indicate that
433 // we are moving "out" of the element.
434 if ( guard && guard( parent, true ) === false )
435 return null;
436
437 node = parent.getNext();
438 }
439
440 if ( !node )
441 return null;
442
443 if ( guard && guard( node ) === false )
444 return null;
445
446 if ( nodeType && nodeType != node.type )
447 return node.getNextSourceNode( false, nodeType, guard );
448
449 return node;
450 },
451
452 /**
453 * @todo
454 */
455 getPreviousSourceNode: function( startFromSibling, nodeType, guard ) {
456 if ( guard && !guard.call ) {
457 var guardNode = guard;
458 guard = function( node ) {
459 return !node.equals( guardNode );
460 };
461 }
462
463 var node = ( !startFromSibling && this.getLast && this.getLast() ),
464 parent;
465
466 // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
467 // send the 'moving out' signal even we don't actually dive into.
468 if ( !node ) {
469 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
470 return null;
471 node = this.getPrevious();
472 }
473
474 while ( !node && ( parent = ( parent || this ).getParent() ) ) {
475 // The guard check sends the "true" paramenter to indicate that
476 // we are moving "out" of the element.
477 if ( guard && guard( parent, true ) === false )
478 return null;
479
480 node = parent.getPrevious();
481 }
482
483 if ( !node )
484 return null;
485
486 if ( guard && guard( node ) === false )
487 return null;
488
489 if ( nodeType && node.type != nodeType )
490 return node.getPreviousSourceNode( false, nodeType, guard );
491
492 return node;
493 },
494
495 /**
496 * Gets the node that preceeds this element in its parent's child list.
497 *
498 * var element = CKEDITOR.dom.element.createFromHtml( '<div><i>prev</i><b>Example</b></div>' );
499 * var first = element.getLast().getPrev();
500 * alert( first.getName() ); // 'i'
501 *
502 * @param {Function} [evaluator] Filtering the result node.
503 * @returns {CKEDITOR.dom.node} The previous node or null if not available.
504 */
505 getPrevious: function( evaluator ) {
506 var previous = this.$,
507 retval;
508 do {
509 previous = previous.previousSibling;
510
511 // Avoid returning the doc type node.
512 // http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927
513 retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous );
514 }
515 while ( retval && evaluator && !evaluator( retval ) );
516 return retval;
517 },
518
519 /**
520 * Gets the node that follows this element in its parent's child list.
521 *
522 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b><i>next</i></div>' );
523 * var last = element.getFirst().getNext();
524 * alert( last.getName() ); // 'i'
525 *
526 * @param {Function} [evaluator] Filtering the result node.
527 * @returns {CKEDITOR.dom.node} The next node or null if not available.
528 */
529 getNext: function( evaluator ) {
530 var next = this.$,
531 retval;
532 do {
533 next = next.nextSibling;
534 retval = next && new CKEDITOR.dom.node( next );
535 }
536 while ( retval && evaluator && !evaluator( retval ) );
537 return retval;
538 },
539
540 /**
541 * Gets the parent element for this node.
542 *
543 * var node = editor.document.getBody().getFirst();
544 * var parent = node.getParent();
545 * alert( parent.getName() ); // 'body'
546 *
547 * @param {Boolean} [allowFragmentParent=false] Consider also parent node that is of
548 * fragment type {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
549 * @returns {CKEDITOR.dom.element} The parent element.
550 */
551 getParent: function( allowFragmentParent ) {
552 var parent = this.$.parentNode;
553 return ( parent && ( parent.nodeType == CKEDITOR.NODE_ELEMENT || allowFragmentParent && parent.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) ? new CKEDITOR.dom.node( parent ) : null;
554 },
555
556 /**
557 * Returns an array containing node parents and the node itself. By default nodes are in _descending_ order.
558 *
559 * // Assuming that body has paragraph as the first child.
560 * var node = editor.document.getBody().getFirst();
561 * var parents = node.getParents();
562 * alert( parents[ 0 ].getName() + ',' + parents[ 2 ].getName() ); // 'html,p'
563 *
564 * @param {Boolean} [closerFirst=false] Determines the order of returned nodes.
565 * @returns {Array} Returns an array of {@link CKEDITOR.dom.node}.
566 */
567 getParents: function( closerFirst ) {
568 var node = this;
569 var parents = [];
570
571 do {
572 parents[ closerFirst ? 'push' : 'unshift' ]( node );
573 }
574 while ( ( node = node.getParent() ) );
575
576 return parents;
577 },
578
579 /**
580 * @todo
581 */
582 getCommonAncestor: function( node ) {
583 if ( node.equals( this ) )
584 return this;
585
586 if ( node.contains && node.contains( this ) )
587 return node;
588
589 var start = this.contains ? this : this.getParent();
590
591 do {
592 if ( start.contains( node ) ) return start;
593 }
594 while ( ( start = start.getParent() ) );
595
596 return null;
597 },
598
599 /**
600 * Determines the position relation between this node and the given {@link CKEDITOR.dom.node} in the document.
601 * This node can be preceding ({@link CKEDITOR#POSITION_PRECEDING}) or following ({@link CKEDITOR#POSITION_FOLLOWING})
602 * the given node. This node can also contain ({@link CKEDITOR#POSITION_CONTAINS}) or be contained by
603 * ({@link CKEDITOR#POSITION_IS_CONTAINED}) the given node. The function returns a bitmask of constants
604 * listed above or {@link CKEDITOR#POSITION_IDENTICAL} if the given node is the same as this node.
605 *
606 * @param {CKEDITOR.dom.node} otherNode A node to check relation with.
607 * @returns {Number} Position relation between this node and given node.
608 */
609 getPosition: function( otherNode ) {
610 var $ = this.$;
611 var $other = otherNode.$;
612
613 if ( $.compareDocumentPosition )
614 return $.compareDocumentPosition( $other );
615
616 // IE and Safari have no support for compareDocumentPosition.
617
618 if ( $ == $other )
619 return CKEDITOR.POSITION_IDENTICAL;
620
621 // Only element nodes support contains and sourceIndex.
622 if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) {
623 if ( $.contains ) {
624 if ( $.contains( $other ) )
625 return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING;
626
627 if ( $other.contains( $ ) )
628 return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
629 }
630
631 if ( 'sourceIndex' in $ )
632 return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
633
634 }
635
636 // For nodes that don't support compareDocumentPosition, contains
637 // or sourceIndex, their "address" is compared.
638
639 var addressOfThis = this.getAddress(),
640 addressOfOther = otherNode.getAddress(),
641 minLevel = Math.min( addressOfThis.length, addressOfOther.length );
642
643 // Determinate preceding/following relationship.
644 for ( var i = 0; i < minLevel; i++ ) {
645 if ( addressOfThis[ i ] != addressOfOther[ i ] ) {
646 return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
647 }
648 }
649
650 // Determinate contains/contained relationship.
651 return ( addressOfThis.length < addressOfOther.length ) ? CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
652 },
653
654 /**
655 * Gets the closest ancestor node of this node, specified by its name or using an evaluator function.
656 *
657 * // Suppose we have the following HTML structure:
658 * // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div>
659 * // If node == <b>
660 * ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner">
661 * ascendant = node.getAscendant( 'b' ); // ascendant == null
662 * ascendant = node.getAscendant( 'b', true ); // ascendant == <b>
663 * ascendant = node.getAscendant( { div:1, p:1 } ); // Searches for the first 'div' or 'p': ascendant == <div id="inner">
664 *
665 * // Using custom evaluator:
666 * ascendant = node.getAscendant( function( el ) {
667 * return el.getId() == 'inner';
668 * } );
669 * // ascendant == <div id="inner">
670 *
671 * @since 3.6.1
672 * @param {String/Function/Object} query The name of the ancestor node to search or
673 * an object with the node names to search for or an evaluator function.
674 * @param {Boolean} [includeSelf] Whether to include the current
675 * node in the search.
676 * @returns {CKEDITOR.dom.node} The located ancestor node or `null` if not found.
677 */
678 getAscendant: function( query, includeSelf ) {
679 var $ = this.$,
680 evaluator,
681 isCustomEvaluator;
682
683 if ( !includeSelf ) {
684 $ = $.parentNode;
685 }
686
687 // Custom checker provided in an argument.
688 if ( typeof query == 'function' ) {
689 isCustomEvaluator = true;
690 evaluator = query;
691 } else {
692 // Predefined tag name checker.
693 isCustomEvaluator = false;
694 evaluator = function( $ ) {
695 var name = ( typeof $.nodeName == 'string' ? $.nodeName.toLowerCase() : '' );
696
697 return ( typeof query == 'string' ? name == query : name in query );
698 };
699 }
700
701 while ( $ ) {
702 // For user provided checker we use CKEDITOR.dom.node.
703 if ( evaluator( isCustomEvaluator ? new CKEDITOR.dom.node( $ ) : $ ) ) {
704 return new CKEDITOR.dom.node( $ );
705 }
706
707 try {
708 $ = $.parentNode;
709 } catch ( e ) {
710 $ = null;
711 }
712 }
713
714 return null;
715 },
716
717 /**
718 * @todo
719 */
720 hasAscendant: function( name, includeSelf ) {
721 var $ = this.$;
722
723 if ( !includeSelf )
724 $ = $.parentNode;
725
726 while ( $ ) {
727 if ( $.nodeName && $.nodeName.toLowerCase() == name )
728 return true;
729
730 $ = $.parentNode;
731 }
732 return false;
733 },
734
735 /**
736 * @todo
737 */
738 move: function( target, toStart ) {
739 target.append( this.remove(), toStart );
740 },
741
742 /**
743 * Removes this node from the document DOM.
744 *
745 * var element = CKEDITOR.document.getById( 'MyElement' );
746 * element.remove();
747 *
748 * @param {Boolean} [preserveChildren=false] Indicates that the children
749 * elements must remain in the document, removing only the outer tags.
750 */
751 remove: function( preserveChildren ) {
752 var $ = this.$;
753 var parent = $.parentNode;
754
755 if ( parent ) {
756 if ( preserveChildren ) {
757 // Move all children before the node.
758 for ( var child;
759 ( child = $.firstChild ); ) {
760 parent.insertBefore( $.removeChild( child ), $ );
761 }
762 }
763
764 parent.removeChild( $ );
765 }
766
767 return this;
768 },
769
770 /**
771 * @todo
772 */
773 replace: function( nodeToReplace ) {
774 this.insertBefore( nodeToReplace );
775 nodeToReplace.remove();
776 },
777
778 /**
779 * @todo
780 */
781 trim: function() {
782 this.ltrim();
783 this.rtrim();
784 },
785
786 /**
787 * @todo
788 */
789 ltrim: function() {
790 var child;
791 while ( this.getFirst && ( child = this.getFirst() ) ) {
792 if ( child.type == CKEDITOR.NODE_TEXT ) {
793 var trimmed = CKEDITOR.tools.ltrim( child.getText() ),
794 originalLength = child.getLength();
795
796 if ( !trimmed ) {
797 child.remove();
798 continue;
799 } else if ( trimmed.length < originalLength ) {
800 child.split( originalLength - trimmed.length );
801
802 // IE BUG: child.remove() may raise JavaScript errors here. (#81)
803 this.$.removeChild( this.$.firstChild );
804 }
805 }
806 break;
807 }
808 },
809
810 /**
811 * @todo
812 */
813 rtrim: function() {
814 var child;
815 while ( this.getLast && ( child = this.getLast() ) ) {
816 if ( child.type == CKEDITOR.NODE_TEXT ) {
817 var trimmed = CKEDITOR.tools.rtrim( child.getText() ),
818 originalLength = child.getLength();
819
820 if ( !trimmed ) {
821 child.remove();
822 continue;
823 } else if ( trimmed.length < originalLength ) {
824 child.split( trimmed.length );
825
826 // IE BUG: child.getNext().remove() may raise JavaScript errors here.
827 // (#81)
828 this.$.lastChild.parentNode.removeChild( this.$.lastChild );
829 }
830 }
831 break;
832 }
833
834 if ( CKEDITOR.env.needsBrFiller ) {
835 child = this.$.lastChild;
836
837 if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) {
838 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324).
839 child.parentNode.removeChild( child );
840 }
841 }
842 },
843
844 /**
845 * Checks if this node is read-only (should not be changed).
846 *
847 * // For the following HTML:
848 * // <b>foo</b><div contenteditable="false"><i>bar</i></div>
849 *
850 * elB.isReadOnly(); // -> false
851 * foo.isReadOnly(); // -> false
852 * elDiv.isReadOnly(); // -> true
853 * elI.isReadOnly(); // -> true
854 *
855 * This method works in two modes depending on browser support for the `element.isContentEditable` property and
856 * the value of the `checkOnlyAttributes` parameter. The `element.isContentEditable` check is faster, but it is known
857 * to malfunction in hidden or detached nodes. Additionally, when processing some detached DOM tree you may want to imitate
858 * that this happens inside an editable container (like it would happen inside the {@link CKEDITOR.editable}). To do so,
859 * you can temporarily attach this tree to an element with the `data-cke-editable` attribute and use the
860 * `checkOnlyAttributes` mode.
861 *
862 * @since 3.5
863 * @param {Boolean} [checkOnlyAttributes=false] If `true`, only attributes will be checked, native methods will not
864 * be used. This parameter needs to be `true` to check hidden or detached elements. Introduced in 4.5.
865 * @returns {Boolean}
866 */
867 isReadOnly: function( checkOnlyAttributes ) {
868 var element = this;
869 if ( this.type != CKEDITOR.NODE_ELEMENT )
870 element = this.getParent();
871
872 // Prevent Edge crash (#13609, #13919).
873 if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) {
874 checkOnlyAttributes = true;
875 }
876
877 if ( !checkOnlyAttributes && element && typeof element.$.isContentEditable != 'undefined' ) {
878 return !( element.$.isContentEditable || element.data( 'cke-editable' ) );
879 }
880 else {
881 // Degrade for old browsers which don't support "isContentEditable", e.g. FF3
882
883 while ( element ) {
884 if ( element.data( 'cke-editable' ) ) {
885 return false;
886 } else if ( element.hasAttribute( 'contenteditable' ) ) {
887 return element.getAttribute( 'contenteditable' ) == 'false';
888 }
889
890 element = element.getParent();
891 }
892
893 // Reached the root of DOM tree, no editable found.
894 return true;
895 }
896 }
897} );
diff --git a/sources/core/dom/nodelist.js b/sources/core/dom/nodelist.js
new file mode 100644
index 00000000..0bbe3ee6
--- /dev/null
+++ b/sources/core/dom/nodelist.js
@@ -0,0 +1,43 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * Represents a list of {@link CKEDITOR.dom.node} objects.
8 * It's a wrapper for native nodes list.
9 *
10 * var nodeList = CKEDITOR.document.getBody().getChildren();
11 * alert( nodeList.count() ); // number [0;N]
12 *
13 * @class
14 * @constructor Creates a document class instance.
15 * @param {Object} nativeList
16 */
17CKEDITOR.dom.nodeList = function( nativeList ) {
18 this.$ = nativeList;
19};
20
21CKEDITOR.dom.nodeList.prototype = {
22 /**
23 * Get count of nodes in this list.
24 *
25 * @returns {Number}
26 */
27 count: function() {
28 return this.$.length;
29 },
30
31 /**
32 * Get node from the list.
33 *
34 * @returns {CKEDITOR.dom.node}
35 */
36 getItem: function( index ) {
37 if ( index < 0 || index >= this.$.length )
38 return null;
39
40 var $node = this.$[ index ];
41 return $node ? new CKEDITOR.dom.node( $node ) : null;
42 }
43};
diff --git a/sources/core/dom/range.js b/sources/core/dom/range.js
new file mode 100644
index 00000000..fe75c550
--- /dev/null
+++ b/sources/core/dom/range.js
@@ -0,0 +1,2860 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * Represents a delimited piece of content in a DOM Document.
8 * It is contiguous in the sense that it can be characterized as selecting all
9 * of the content between a pair of boundary-points.
10 *
11 * This class shares much of the W3C
12 * [Document Object Model Range](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html)
13 * ideas and features, adding several range manipulation tools to it, but it's
14 * not intended to be compatible with it.
15 *
16 * // Create a range for the entire contents of the editor document body.
17 * var range = new CKEDITOR.dom.range( editor.document );
18 * range.selectNodeContents( editor.document.getBody() );
19 * // Delete the contents.
20 * range.deleteContents();
21 *
22 * Usually you will want to work on a ranges rooted in the editor's {@link CKEDITOR.editable editable}
23 * element. Such ranges can be created with a shorthand method &ndash; {@link CKEDITOR.editor#createRange editor.createRange}.
24 *
25 * var range = editor.createRange();
26 * range.root.equals( editor.editable() ); // -> true
27 *
28 * Note that the {@link #root} of a range is an important property, which limits many
29 * algorithms implemented in range's methods. Therefore it is crucial, especially
30 * when using ranges inside inline editors, to specify correct root, so using
31 * the {@link CKEDITOR.editor#createRange} method is highly recommended.
32 *
33 * ### Selection
34 *
35 * Range is only a logical representation of a piece of content in a DOM. It should not
36 * be confused with a {@link CKEDITOR.dom.selection selection} which represents "physically
37 * marked" content. It is possible to create unlimited number of various ranges, when
38 * only one real selection may exist at a time in a document. Ranges are used to read position
39 * of selection in the DOM and to move selection to new positions.
40 *
41 * The editor selection may be retrieved using the {@link CKEDITOR.editor#getSelection} method:
42 *
43 * var sel = editor.getSelection(),
44 * ranges = sel.getRanges(); // CKEDITOR.dom.rangeList instance.
45 *
46 * var range = ranges[ 0 ];
47 * range.root; // -> editor's editable element.
48 *
49 * A range can also be selected:
50 *
51 * var range = editor.createRange();
52 * range.selectNodeContents( editor.editable() );
53 * sel.selectRanges( [ range ] );
54 *
55 * @class
56 * @constructor Creates a {@link CKEDITOR.dom.range} instance that can be used inside a specific DOM Document.
57 * @param {CKEDITOR.dom.document/CKEDITOR.dom.element} root The document or element
58 * within which the range will be scoped.
59 * @todo global "TODO" - precise algorithms descriptions needed for the most complex methods like #enlarge.
60 */
61CKEDITOR.dom.range = function( root ) {
62 /**
63 * Node within which the range begins.
64 *
65 * var range = new CKEDITOR.dom.range( editor.document );
66 * range.selectNodeContents( editor.document.getBody() );
67 * alert( range.startContainer.getName() ); // 'body'
68 *
69 * @readonly
70 * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
71 */
72 this.startContainer = null;
73
74 /**
75 * Offset within the starting node of the range.
76 *
77 * var range = new CKEDITOR.dom.range( editor.document );
78 * range.selectNodeContents( editor.document.getBody() );
79 * alert( range.startOffset ); // 0
80 *
81 * @readonly
82 * @property {Number}
83 */
84 this.startOffset = null;
85
86 /**
87 * Node within which the range ends.
88 *
89 * var range = new CKEDITOR.dom.range( editor.document );
90 * range.selectNodeContents( editor.document.getBody() );
91 * alert( range.endContainer.getName() ); // 'body'
92 *
93 * @readonly
94 * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
95 */
96 this.endContainer = null;
97
98 /**
99 * Offset within the ending node of the range.
100 *
101 * var range = new CKEDITOR.dom.range( editor.document );
102 * range.selectNodeContents( editor.document.getBody() );
103 * alert( range.endOffset ); // == editor.document.getBody().getChildCount()
104 *
105 * @readonly
106 * @property {Number}
107 */
108 this.endOffset = null;
109
110 /**
111 * Indicates that this is a collapsed range. A collapsed range has its
112 * start and end boundaries at the very same point so nothing is contained
113 * in it.
114 *
115 * var range = new CKEDITOR.dom.range( editor.document );
116 * range.selectNodeContents( editor.document.getBody() );
117 * alert( range.collapsed ); // false
118 * range.collapse();
119 * alert( range.collapsed ); // true
120 *
121 * @readonly
122 */
123 this.collapsed = true;
124
125 var isDocRoot = root instanceof CKEDITOR.dom.document;
126 /**
127 * The document within which the range can be used.
128 *
129 * // Selects the body contents of the range document.
130 * range.selectNodeContents( range.document.getBody() );
131 *
132 * @readonly
133 * @property {CKEDITOR.dom.document}
134 */
135 this.document = isDocRoot ? root : root.getDocument();
136
137 /**
138 * The ancestor DOM element within which the range manipulation are limited.
139 *
140 * @readonly
141 * @property {CKEDITOR.dom.element}
142 */
143 this.root = isDocRoot ? root.getBody() : root;
144};
145
146( function() {
147 // Updates the "collapsed" property for the given range object.
148 function updateCollapsed( range ) {
149 range.collapsed = ( range.startContainer && range.endContainer && range.startContainer.equals( range.endContainer ) && range.startOffset == range.endOffset );
150 }
151
152 // This is a shared function used to delete, extract and clone the range content.
153 //
154 // The outline of the algorithm:
155 //
156 // 1. Normalization. We handle special cases, split text nodes if we can, find boundary nodes (startNode and endNode).
157 // 2. Gathering data.
158 // * We start by creating two arrays of boundary nodes parents. You can imagine these arrays as lines limiting
159 // the tree from the left and right and thus marking the part which is selected by the range. The both lines
160 // start in the same node which is the range.root and end in startNode and endNode.
161 // * Then we find min level and max levels. Level represents all nodes which are equally far from the range.root.
162 // Min level is the level at which the left and right boundaries diverged (the first diverged level). And max levels
163 // are how deep the start and end nodes are nested.
164 // 3. Cloning/extraction.
165 // * We start iterating over start node parents (left branch) from min level and clone the parent (usually shallow clone,
166 // because we know that it's not fully selected) and its right siblings (deep clone, because they are fully selected).
167 // We iterate over siblings up to meeting end node parent or end of the siblings chain.
168 // * We clone level after level down to the startNode.
169 // * Then we do the same with end node parents (right branch), because it may contains notes we omit during the previous
170 // step, for example if the right branch is deeper then left branch. Things are more complicated here because we have to
171 // watch out for nodes that were already cloned.
172 // * ***Note:** Setting `cloneId` option to `false` for **extraction** works for partially selected elements only.
173 // See range.extractContents to know more.
174 // 4. Clean up.
175 // * There are two things we need to do - updating the range position and perform the action of the "mergeThen"
176 // param (see range.deleteContents or range.extractContents).
177 // See comments in mergeAndUpdate because this is lots of fun too.
178 function execContentsAction( range, action, docFrag, mergeThen, cloneId ) {
179 'use strict';
180
181 range.optimizeBookmark();
182
183 var isDelete = action === 0;
184 var isExtract = action == 1;
185 var isClone = action == 2;
186 var doClone = isClone || isExtract;
187
188 var startNode = range.startContainer;
189 var endNode = range.endContainer;
190
191 var startOffset = range.startOffset;
192 var endOffset = range.endOffset;
193
194 var cloneStartNode;
195 var cloneEndNode;
196
197 var doNotRemoveStartNode;
198 var doNotRemoveEndNode;
199
200 var cloneStartText;
201 var cloneEndText;
202
203 // Handle here an edge case where we clone a range which is located in one text node.
204 // This allows us to not think about startNode == endNode case later on.
205 // We do that only when cloning, because in other cases we can safely split this text node
206 // and hence we can easily handle this case as many others.
207 if ( isClone && endNode.type == CKEDITOR.NODE_TEXT && startNode.equals( endNode ) ) {
208 startNode = range.document.createText( startNode.substring( startOffset, endOffset ) );
209 docFrag.append( startNode );
210 return;
211 }
212
213 // For text containers, we must simply split the node and point to the
214 // second part. The removal will be handled by the rest of the code.
215 if ( endNode.type == CKEDITOR.NODE_TEXT ) {
216 // If Extract or Delete we can split the text node,
217 // but if Clone (2), then we cannot modify the DOM (#11586) so we mark the text node for cloning.
218 if ( !isClone ) {
219 endNode = endNode.split( endOffset );
220 } else {
221 cloneEndText = true;
222 }
223 } else {
224 // If there's no node after the range boundary we set endNode to the previous node
225 // and mark it to be cloned.
226 if ( endNode.getChildCount() > 0 ) {
227 // If the offset points after the last node.
228 if ( endOffset >= endNode.getChildCount() ) {
229 endNode = endNode.getChild( endOffset - 1 );
230 cloneEndNode = true;
231 } else {
232 endNode = endNode.getChild( endOffset );
233 }
234 }
235 // The end container is empty (<h1>]</h1>), but we want to clone it, although not remove.
236 else {
237 cloneEndNode = true;
238 doNotRemoveEndNode = true;
239 }
240 }
241
242 // For text containers, we must simply split the node. The removal will
243 // be handled by the rest of the code .
244 if ( startNode.type == CKEDITOR.NODE_TEXT ) {
245 // If Extract or Delete we can split the text node,
246 // but if Clone (2), then we cannot modify the DOM (#11586) so we mark
247 // the text node for cloning.
248 if ( !isClone ) {
249 startNode.split( startOffset );
250 } else {
251 cloneStartText = true;
252 }
253 } else {
254 // If there's no node before the range boundary we set startNode to the next node
255 // and mark it to be cloned.
256 if ( startNode.getChildCount() > 0 ) {
257 if ( startOffset === 0 ) {
258 startNode = startNode.getChild( startOffset );
259 cloneStartNode = true;
260 } else {
261 startNode = startNode.getChild( startOffset - 1 );
262 }
263 }
264 // The start container is empty (<h1>[</h1>), but we want to clone it, although not remove.
265 else {
266 cloneStartNode = true;
267 doNotRemoveStartNode = true;
268 }
269 }
270
271 // Get the parent nodes tree for the start and end boundaries.
272 var startParents = startNode.getParents(),
273 endParents = endNode.getParents(),
274 // Level at which start and end boundaries diverged.
275 minLevel = findMinLevel(),
276 maxLevelLeft = startParents.length - 1,
277 maxLevelRight = endParents.length - 1,
278 // Keeps the frag/element which is parent of the level that we are currently cloning.
279 levelParent = docFrag,
280 nextLevelParent,
281 leftNode,
282 rightNode,
283 nextSibling,
284 // Keeps track of the last connected level (on which left and right branches are connected)
285 // Usually this is minLevel, but not always.
286 lastConnectedLevel = -1;
287
288 // THE LEFT BRANCH.
289 for ( var level = minLevel; level <= maxLevelLeft; level++ ) {
290 leftNode = startParents[ level ];
291 nextSibling = leftNode.getNext();
292
293 // 1.
294 // The first step is to handle partial selection of the left branch.
295
296 // Max depth of the left branch. It means that ( leftSibling == endNode ).
297 // We also check if the leftNode isn't only partially selected, because in this case
298 // we want to make a shallow clone of it (the else part).
299 if ( level == maxLevelLeft && !( leftNode.equals( endParents[ level ] ) && maxLevelLeft < maxLevelRight ) ) {
300 if ( cloneStartNode ) {
301 consume( leftNode, levelParent, false, doNotRemoveStartNode );
302 } else if ( cloneStartText ) {
303 levelParent.append( range.document.createText( leftNode.substring( startOffset ) ) );
304 }
305 } else if ( doClone ) {
306 nextLevelParent = levelParent.append( leftNode.clone( 0, cloneId ) );
307 }
308
309 // 2.
310 // The second step is to handle full selection of the content between the left branch and the right branch.
311
312 while ( nextSibling ) {
313 // We can't clone entire endParent just like we can't clone entire startParent -
314 // - they are not fully selected with the range. Partial endParent selection
315 // will be cloned in the next loop.
316 if ( nextSibling.equals( endParents[ level ] ) ) {
317 lastConnectedLevel = level;
318 break;
319 }
320
321 nextSibling = consume( nextSibling, levelParent );
322 }
323
324 levelParent = nextLevelParent;
325 }
326
327 // Reset levelParent, because we reset the level.
328 levelParent = docFrag;
329
330 // THE RIGHT BRANCH.
331 for ( level = minLevel; level <= maxLevelRight; level++ ) {
332 rightNode = endParents[ level ];
333 nextSibling = rightNode.getPrevious();
334
335 // Do not process this node if it is shared with the left branch
336 // because it was already processed.
337 //
338 // Note: Don't worry about text nodes selection - if the entire range was placed in a single text node
339 // it was handled as a special case at the beginning. In other cases when startNode == endNode
340 // or when on this level leftNode == rightNode (so rightNode.equals( startParents[ level ] ))
341 // this node was handled by the previous loop.
342 if ( !rightNode.equals( startParents[ level ] ) ) {
343 // 1.
344 // The first step is to handle partial selection of the right branch.
345
346 // Max depth of the right branch. It means that ( rightNode == endNode ).
347 // We also check if the rightNode isn't only partially selected, because in this case
348 // we want to make a shallow clone of it (the else part).
349 if ( level == maxLevelRight && !( rightNode.equals( startParents[ level ] ) && maxLevelRight < maxLevelLeft ) ) {
350 if ( cloneEndNode ) {
351 consume( rightNode, levelParent, false, doNotRemoveEndNode );
352 } else if ( cloneEndText ) {
353 levelParent.append( range.document.createText( rightNode.substring( 0, endOffset ) ) );
354 }
355 } else if ( doClone ) {
356 nextLevelParent = levelParent.append( rightNode.clone( 0, cloneId ) );
357 }
358
359 // 2.
360 // The second step is to handle all left (selected) siblings of the rightNode which
361 // have not yet been handled. If the level branches were connected, the previous loop
362 // already copied all siblings (except the current rightNode).
363 if ( level > lastConnectedLevel ) {
364 while ( nextSibling ) {
365 nextSibling = consume( nextSibling, levelParent, true );
366 }
367 }
368
369 levelParent = nextLevelParent;
370 } else if ( doClone ) {
371 // If this is "shared" node and we are in cloning mode we have to update levelParent to
372 // reflect that we visited the node (even though we didn't process it).
373 // If we don't do that, in next iterations nodes will be appended to wrong parent.
374 //
375 // We can just take first child because the algorithm guarantees
376 // that this will be the only child on this level. (#13568)
377 levelParent = levelParent.getChild( 0 );
378 }
379 }
380
381 // Delete or Extract.
382 // We need to update the range and if mergeThen was passed do it.
383 if ( !isClone ) {
384 mergeAndUpdate();
385 }
386
387 // Depending on an action:
388 // * clones node and adds to new parent,
389 // * removes node,
390 // * moves node to the new parent.
391 function consume( node, newParent, toStart, forceClone ) {
392 var nextSibling = toStart ? node.getPrevious() : node.getNext();
393
394 // We do not clone if we are only deleting, so do nothing.
395 if ( forceClone && isDelete ) {
396 return nextSibling;
397 }
398
399 // If cloning, just clone it.
400 if ( isClone || forceClone ) {
401 newParent.append( node.clone( true, cloneId ), toStart );
402 } else {
403 // Both Delete and Extract will remove the node.
404 node.remove();
405
406 // When Extracting, move the removed node to the docFrag.
407 if ( isExtract ) {
408 newParent.append( node );
409 }
410 }
411
412 return nextSibling;
413 }
414
415 // Finds a level number on which both branches starts diverging.
416 // If such level does not exist, return the last on which both branches have nodes.
417 function findMinLevel() {
418 // Compare them, to find the top most siblings.
419 var i, topStart, topEnd,
420 maxLevel = Math.min( startParents.length, endParents.length );
421
422 for ( i = 0; i < maxLevel; i++ ) {
423 topStart = startParents[ i ];
424 topEnd = endParents[ i ];
425
426 // The compared nodes will match until we find the top most siblings (different nodes that have the same parent).
427 // "i" will hold the index in the parents array for the top most element.
428 if ( !topStart.equals( topEnd ) ) {
429 return i;
430 }
431 }
432
433 // When startNode == endNode.
434 return i - 1;
435 }
436
437 // Executed only when deleting or extracting to update range position
438 // and perform the merge operation.
439 function mergeAndUpdate() {
440 var commonLevel = minLevel - 1,
441 boundariesInEmptyNode = doNotRemoveStartNode && doNotRemoveEndNode && !startNode.equals( endNode );
442
443 // If a node has been partially selected, collapse the range between
444 // startParents[ minLevel + 1 ] and endParents[ minLevel + 1 ] (the first diverged elements).
445 // Otherwise, simply collapse it to the start. (W3C specs).
446 //
447 // All clear, right?
448 //
449 // It took me few hours to truly understand a previous version of this condition.
450 // Mine seems to be more straightforward (even if it doesn't look so) and I could leave you here
451 // without additional comments, but I'm not that mean so here goes the explanation.
452 //
453 // We want to know if both ends of the range are anchored in the same element. Really. It's this simple.
454 // But why? Because we need to differentiate situations like:
455 //
456 // <p>foo[<b>x</b>bar]y</p> (commonLevel = p, maxLL = "foo", maxLR = "y")
457 // from:
458 // <p>foo<b>x[</b>bar]y</p> (commonLevel = p, maxLL = "x", maxLR = "y")
459 //
460 // In the first case we can collapse the range to the left, because simply everything between range's
461 // boundaries was removed.
462 // In the second case we must place the range after </b>, because <b> was only **partially selected**.
463 //
464 // * <b> is our startParents[ commonLevel + 1 ]
465 // * "y" is our endParents[ commonLevel + 1 ].
466 //
467 // By now "bar" is removed from the DOM so <b> is a direct sibling of "y":
468 // <p>foo<b>x</b>y</p>
469 //
470 // Therefore it's enough to place the range between <b> and "y".
471 //
472 // Now, what does the comparison mean? Why not just taking startNode and endNode and checking
473 // their parents? Because the tree is already changed and they may be gone. Plus, thanks to
474 // cloneStartNode and cloneEndNode, that would be reaaaaly tricky.
475 //
476 // So we play with levels which can give us the same information:
477 // * commonLevel - the level of common ancestor,
478 // * maxLevel - 1 - the level of range boundary parent (range boundary is here like a bookmark span).
479 // * commonLevel < maxLevel - 1 - whether the range boundary is not a child of common ancestor.
480 //
481 // There's also an edge case in which both range boundaries were placed in empty nodes like:
482 // <p>[</p><p>]</p>
483 // Those boundaries were not removed, but in this case start and end nodes are child of the common ancestor.
484 // We handle this edge case separately.
485 if ( commonLevel < ( maxLevelLeft - 1 ) || commonLevel < ( maxLevelRight - 1 ) || boundariesInEmptyNode ) {
486 if ( boundariesInEmptyNode ) {
487 range.moveToPosition( endNode, CKEDITOR.POSITION_BEFORE_START );
488 } else if ( ( maxLevelRight == commonLevel + 1 ) && cloneEndNode ) {
489 // The maxLevelRight + 1 element could be already removed so we use the fact that
490 // we know that it was the last element in its parent.
491 range.moveToPosition( endParents[ commonLevel ], CKEDITOR.POSITION_BEFORE_END );
492 } else {
493 range.moveToPosition( endParents[ commonLevel + 1 ], CKEDITOR.POSITION_BEFORE_START );
494 }
495
496 // Merge split parents.
497 if ( mergeThen ) {
498 // Find the first diverged node in the left branch.
499 var topLeft = startParents[ commonLevel + 1 ];
500
501 // TopLeft may simply not exist if commonLevel == maxLevel or may be a text node.
502 if ( topLeft && topLeft.type == CKEDITOR.NODE_ELEMENT ) {
503 var span = CKEDITOR.dom.element.createFromHtml( '<span ' +
504 'data-cke-bookmark="1" style="display:none">&nbsp;</span>', range.document );
505 span.insertAfter( topLeft );
506 topLeft.mergeSiblings( false );
507 range.moveToBookmark( { startNode: span } );
508 }
509 }
510 } else {
511 // Collapse it to the start.
512 range.collapse( true );
513 }
514 }
515 }
516
517 var inlineChildReqElements = {
518 abbr: 1, acronym: 1, b: 1, bdo: 1, big: 1, cite: 1, code: 1, del: 1,
519 dfn: 1, em: 1, font: 1, i: 1, ins: 1, label: 1, kbd: 1, q: 1, samp: 1, small: 1, span: 1, strike: 1,
520 strong: 1, sub: 1, sup: 1, tt: 1, u: 1, 'var': 1
521 };
522
523 // Creates the appropriate node evaluator for the dom walker used inside
524 // check(Start|End)OfBlock.
525 function getCheckStartEndBlockEvalFunction() {
526 var skipBogus = false,
527 whitespaces = CKEDITOR.dom.walker.whitespaces(),
528 bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),
529 isBogus = CKEDITOR.dom.walker.bogus();
530
531 return function( node ) {
532 // First skip empty nodes
533 if ( bookmarkEvaluator( node ) || whitespaces( node ) )
534 return true;
535
536 // Skip the bogus node at the end of block.
537 if ( isBogus( node ) && !skipBogus ) {
538 skipBogus = true;
539 return true;
540 }
541
542 // If there's any visible text, then we're not at the start.
543 if ( node.type == CKEDITOR.NODE_TEXT &&
544 ( node.hasAscendant( 'pre' ) ||
545 CKEDITOR.tools.trim( node.getText() ).length ) ) {
546 return false;
547 }
548
549 // If there are non-empty inline elements (e.g. <img />), then we're not
550 // at the start.
551 if ( node.type == CKEDITOR.NODE_ELEMENT && !node.is( inlineChildReqElements ) )
552 return false;
553
554 return true;
555 };
556 }
557
558 var isBogus = CKEDITOR.dom.walker.bogus(),
559 nbspRegExp = /^[\t\r\n ]*(?:&nbsp;|\xa0)$/,
560 editableEval = CKEDITOR.dom.walker.editable(),
561 notIgnoredEval = CKEDITOR.dom.walker.ignored( true );
562
563 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
564 // text node and non-empty elements unless it's being bookmark text.
565 function elementBoundaryEval( checkStart ) {
566 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
567 bookmark = CKEDITOR.dom.walker.bookmark( 1 );
568
569 return function( node ) {
570 // First skip empty nodes.
571 if ( bookmark( node ) || whitespaces( node ) )
572 return true;
573
574 // Tolerant bogus br when checking at the end of block.
575 // Reject any text node unless it's being bookmark
576 // OR it's spaces.
577 // Reject any element unless it's being invisible empty. (#3883)
578 return !checkStart && isBogus( node ) ||
579 node.type == CKEDITOR.NODE_ELEMENT &&
580 node.is( CKEDITOR.dtd.$removeEmpty );
581 };
582 }
583
584 function getNextEditableNode( isPrevious ) {
585 return function() {
586 var first;
587
588 return this[ isPrevious ? 'getPreviousNode' : 'getNextNode' ]( function( node ) {
589 // Cache first not ignorable node.
590 if ( !first && notIgnoredEval( node ) )
591 first = node;
592
593 // Return true if found editable node, but not a bogus next to start of our lookup (first != bogus).
594 return editableEval( node ) && !( isBogus( node ) && node.equals( first ) );
595 } );
596 };
597 }
598
599 CKEDITOR.dom.range.prototype = {
600 /**
601 * Clones this range.
602 *
603 * @returns {CKEDITOR.dom.range}
604 */
605 clone: function() {
606 var clone = new CKEDITOR.dom.range( this.root );
607
608 clone._setStartContainer( this.startContainer );
609 clone.startOffset = this.startOffset;
610 clone._setEndContainer( this.endContainer );
611 clone.endOffset = this.endOffset;
612 clone.collapsed = this.collapsed;
613
614 return clone;
615 },
616
617 /**
618 * Makes the range collapsed by moving its start point (or end point if `toStart==true`)
619 * to the second end.
620 *
621 * @param {Boolean} toStart Collapse range "to start".
622 */
623 collapse: function( toStart ) {
624 if ( toStart ) {
625 this._setEndContainer( this.startContainer );
626 this.endOffset = this.startOffset;
627 } else {
628 this._setStartContainer( this.endContainer );
629 this.startOffset = this.endOffset;
630 }
631
632 this.collapsed = true;
633 },
634
635 /**
636 * Clones content nodes of the range and adds them to a document fragment, which is returned.
637 *
638 * @param {Boolean} [cloneId=true] Whether to preserve ID attributes in the clone.
639 * @returns {CKEDITOR.dom.documentFragment} Document fragment containing a clone of range's content.
640 */
641 cloneContents: function( cloneId ) {
642 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
643
644 cloneId = typeof cloneId == 'undefined' ? true : cloneId;
645
646 if ( !this.collapsed )
647 execContentsAction( this, 2, docFrag, false, cloneId );
648
649 return docFrag;
650 },
651
652 /**
653 * Deletes the content nodes of the range permanently from the DOM tree.
654 *
655 * @param {Boolean} [mergeThen] Merge any split elements result in DOM true due to partial selection.
656 */
657 deleteContents: function( mergeThen ) {
658 if ( this.collapsed )
659 return;
660
661 execContentsAction( this, 0, null, mergeThen );
662 },
663
664 /**
665 * The content nodes of the range are cloned and added to a document fragment,
666 * meanwhile they are removed permanently from the DOM tree.
667 *
668 * **Note:** Setting the `cloneId` parameter to `false` works for **partially** selected elements only.
669 * If an element with an ID attribute is **fully enclosed** in a range, it will keep the ID attribute
670 * regardless of the `cloneId` parameter value, because it is not cloned &mdash; it is moved to the returned
671 * document fragment.
672 *
673 * @param {Boolean} [mergeThen] Merge any split elements result in DOM true due to partial selection.
674 * @param {Boolean} [cloneId=true] Whether to preserve ID attributes in the extracted content.
675 * @returns {CKEDITOR.dom.documentFragment} Document fragment containing extracted content.
676 */
677 extractContents: function( mergeThen, cloneId ) {
678 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
679
680 cloneId = typeof cloneId == 'undefined' ? true : cloneId;
681
682 if ( !this.collapsed )
683 execContentsAction( this, 1, docFrag, mergeThen, cloneId );
684
685 return docFrag;
686 },
687
688 /**
689 * Creates a bookmark object, which can be later used to restore the
690 * range by using the {@link #moveToBookmark} function.
691 *
692 * This is an "intrusive" way to create a bookmark. It includes `<span>` tags
693 * in the range boundaries. The advantage of it is that it is possible to
694 * handle DOM mutations when moving back to the bookmark.
695 *
696 * **Note:** The inclusion of nodes in the DOM is a design choice and
697 * should not be changed as there are other points in the code that may be
698 * using those nodes to perform operations.
699 *
700 * @param {Boolean} [serializable] Indicates that the bookmark nodes
701 * must contain IDs, which can be used to restore the range even
702 * when these nodes suffer mutations (like cloning or `innerHTML` change).
703 * @returns {Object} And object representing a bookmark.
704 * @returns {CKEDITOR.dom.node/String} return.startNode Node or element ID.
705 * @returns {CKEDITOR.dom.node/String} return.endNode Node or element ID.
706 * @returns {Boolean} return.serializable
707 * @returns {Boolean} return.collapsed
708 */
709 createBookmark: function( serializable ) {
710 var startNode, endNode;
711 var baseId;
712 var clone;
713 var collapsed = this.collapsed;
714
715 startNode = this.document.createElement( 'span' );
716 startNode.data( 'cke-bookmark', 1 );
717 startNode.setStyle( 'display', 'none' );
718
719 // For IE, it must have something inside, otherwise it may be
720 // removed during DOM operations.
721 startNode.setHtml( '&nbsp;' );
722
723 if ( serializable ) {
724 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
725 startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );
726 }
727
728 // If collapsed, the endNode will not be created.
729 if ( !collapsed ) {
730 endNode = startNode.clone();
731 endNode.setHtml( '&nbsp;' );
732
733 if ( serializable )
734 endNode.setAttribute( 'id', baseId + 'E' );
735
736 clone = this.clone();
737 clone.collapse();
738 clone.insertNode( endNode );
739 }
740
741 clone = this.clone();
742 clone.collapse( true );
743 clone.insertNode( startNode );
744
745 // Update the range position.
746 if ( endNode ) {
747 this.setStartAfter( startNode );
748 this.setEndBefore( endNode );
749 } else {
750 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
751 }
752
753 return {
754 startNode: serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,
755 endNode: serializable ? baseId + 'E' : endNode,
756 serializable: serializable,
757 collapsed: collapsed
758 };
759 },
760
761 /**
762 * Creates a "non intrusive" and "mutation sensible" bookmark. This
763 * kind of bookmark should be used only when the DOM is supposed to
764 * remain stable after its creation.
765 *
766 * @param {Boolean} [normalized] Indicates that the bookmark must
767 * be normalized. When normalized, the successive text nodes are
768 * considered a single node. To successfully load a normalized
769 * bookmark, the DOM tree must also be normalized before calling
770 * {@link #moveToBookmark}.
771 * @returns {Object} An object representing the bookmark.
772 * @returns {Array} return.start Start container's address (see {@link CKEDITOR.dom.node#getAddress}).
773 * @returns {Array} return.end Start container's address.
774 * @returns {Number} return.startOffset
775 * @returns {Number} return.endOffset
776 * @returns {Boolean} return.collapsed
777 * @returns {Boolean} return.normalized
778 * @returns {Boolean} return.is2 This is "bookmark2".
779 */
780 createBookmark2: ( function() {
781 var isNotText = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_TEXT, true );
782
783 // Returns true for limit anchored in element and placed between text nodes.
784 //
785 // v
786 // <p>[text node] [text node]</p> -> true
787 //
788 // v
789 // <p> [text node]</p> -> false
790 //
791 // v
792 // <p>[text node][text node]</p> -> false (limit is anchored in text node)
793 function betweenTextNodes( container, offset ) {
794 // Not anchored in element or limit is on the edge.
795 if ( container.type != CKEDITOR.NODE_ELEMENT || offset === 0 || offset == container.getChildCount() )
796 return 0;
797
798 return container.getChild( offset - 1 ).type == CKEDITOR.NODE_TEXT &&
799 container.getChild( offset ).type == CKEDITOR.NODE_TEXT;
800 }
801
802 // Sums lengths of all preceding text nodes.
803 function getLengthOfPrecedingTextNodes( node ) {
804 var sum = 0;
805
806 while ( ( node = node.getPrevious() ) && node.type == CKEDITOR.NODE_TEXT )
807 sum += node.getLength();
808
809 return sum;
810 }
811
812 function normalize( limit ) {
813 var container = limit.container,
814 offset = limit.offset;
815
816 // If limit is between text nodes move it to the end of preceding one,
817 // because they will be merged.
818 if ( betweenTextNodes( container, offset ) ) {
819 container = container.getChild( offset - 1 );
820 offset = container.getLength();
821 }
822
823 // Now, if limit is anchored in element and has at least two nodes before it,
824 // it may happen that some of them will be merged. Normalize the offset
825 // by setting it to normalized index of its preceding node.
826 if ( container.type == CKEDITOR.NODE_ELEMENT && offset > 1 )
827 offset = container.getChild( offset - 1 ).getIndex( true ) + 1;
828
829 // The last step - fix the offset inside text node by adding
830 // lengths of preceding text nodes which will be merged with container.
831 if ( container.type == CKEDITOR.NODE_TEXT ) {
832 var precedingLength = getLengthOfPrecedingTextNodes( container );
833
834 // Normal case - text node is not empty.
835 if ( container.getText() ) {
836 offset += precedingLength;
837
838 // Awful case - the text node is empty and thus will be totally lost.
839 // In this case we are trying to normalize the limit to the left:
840 // * either to the preceding text node,
841 // * or to the "gap" after the preceding element.
842 } else {
843 // Find the closest non-text sibling.
844 var precedingContainer = container.getPrevious( isNotText );
845
846 // If there are any characters on the left, that means that we can anchor
847 // there, because this text node will not be lost.
848 if ( precedingLength ) {
849 offset = precedingLength;
850
851 if ( precedingContainer ) {
852 // The text node is the first node after the closest non-text sibling.
853 container = precedingContainer.getNext();
854 } else {
855 // But if there was no non-text sibling, then the text node is the first child.
856 container = container.getParent().getFirst();
857 }
858
859 // If there are no characters on the left, then anchor after the previous non-text node.
860 // E.g. (see tests for a legend :D):
861 // <b>x</b>(foo)({}bar) -> <b>x</b>[](foo)(bar)
862 } else {
863 container = container.getParent();
864 offset = precedingContainer ? ( precedingContainer.getIndex( true ) + 1 ) : 0;
865 }
866 }
867 }
868
869 limit.container = container;
870 limit.offset = offset;
871 }
872
873 return function( normalized ) {
874 var collapsed = this.collapsed,
875 bmStart = {
876 container: this.startContainer,
877 offset: this.startOffset
878 },
879 bmEnd = {
880 container: this.endContainer,
881 offset: this.endOffset
882 };
883
884 if ( normalized ) {
885 normalize( bmStart );
886
887 if ( !collapsed )
888 normalize( bmEnd );
889 }
890
891 return {
892 start: bmStart.container.getAddress( normalized ),
893 end: collapsed ? null : bmEnd.container.getAddress( normalized ),
894 startOffset: bmStart.offset,
895 endOffset: bmEnd.offset,
896 normalized: normalized,
897 collapsed: collapsed,
898 is2: true // It's a createBookmark2 bookmark.
899 };
900 };
901 } )(),
902
903 /**
904 * Moves this range to the given bookmark. See {@link #createBookmark} and {@link #createBookmark2}.
905 *
906 * If serializable bookmark passed, then its `<span>` markers will be removed.
907 *
908 * @param {Object} bookmark
909 */
910 moveToBookmark: function( bookmark ) {
911 // Created with createBookmark2().
912 if ( bookmark.is2 ) {
913 // Get the start information.
914 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
915 startOffset = bookmark.startOffset;
916
917 // Get the end information.
918 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
919 endOffset = bookmark.endOffset;
920
921 // Set the start boundary.
922 this.setStart( startContainer, startOffset );
923
924 // Set the end boundary. If not available, collapse it.
925 if ( endContainer )
926 this.setEnd( endContainer, endOffset );
927 else
928 this.collapse( true );
929 }
930 // Created with createBookmark().
931 else {
932 var serializable = bookmark.serializable,
933 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
934 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
935
936 // Set the range start at the bookmark start node position.
937 this.setStartBefore( startNode );
938
939 // Remove it, because it may interfere in the setEndBefore call.
940 startNode.remove();
941
942 // Set the range end at the bookmark end node position, or simply
943 // collapse it if it is not available.
944 if ( endNode ) {
945 this.setEndBefore( endNode );
946 endNode.remove();
947 } else {
948 this.collapse( true );
949 }
950 }
951 },
952
953 /**
954 * Returns two nodes which are on the boundaries of this range.
955 *
956 * @returns {Object}
957 * @returns {CKEDITOR.dom.node} return.startNode
958 * @returns {CKEDITOR.dom.node} return.endNode
959 * @todo precise desc/algorithm
960 */
961 getBoundaryNodes: function() {
962 var startNode = this.startContainer,
963 endNode = this.endContainer,
964 startOffset = this.startOffset,
965 endOffset = this.endOffset,
966 childCount;
967
968 if ( startNode.type == CKEDITOR.NODE_ELEMENT ) {
969 childCount = startNode.getChildCount();
970 if ( childCount > startOffset ) {
971 startNode = startNode.getChild( startOffset );
972 } else if ( childCount < 1 ) {
973 startNode = startNode.getPreviousSourceNode();
974 }
975 // startOffset > childCount but childCount is not 0
976 else {
977 // Try to take the node just after the current position.
978 startNode = startNode.$;
979 while ( startNode.lastChild )
980 startNode = startNode.lastChild;
981 startNode = new CKEDITOR.dom.node( startNode );
982
983 // Normally we should take the next node in DFS order. But it
984 // is also possible that we've already reached the end of
985 // document.
986 startNode = startNode.getNextSourceNode() || startNode;
987 }
988 }
989
990 if ( endNode.type == CKEDITOR.NODE_ELEMENT ) {
991 childCount = endNode.getChildCount();
992 if ( childCount > endOffset ) {
993 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
994 } else if ( childCount < 1 ) {
995 endNode = endNode.getPreviousSourceNode();
996 }
997 // endOffset > childCount but childCount is not 0.
998 else {
999 // Try to take the node just before the current position.
1000 endNode = endNode.$;
1001 while ( endNode.lastChild )
1002 endNode = endNode.lastChild;
1003 endNode = new CKEDITOR.dom.node( endNode );
1004 }
1005 }
1006
1007 // Sometimes the endNode will come right before startNode for collapsed
1008 // ranges. Fix it. (#3780)
1009 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
1010 startNode = endNode;
1011
1012 return { startNode: startNode, endNode: endNode };
1013 },
1014
1015 /**
1016 * Find the node which fully contains the range.
1017 *
1018 * @param {Boolean} [includeSelf=false]
1019 * @param {Boolean} [ignoreTextNode=false] Whether ignore {@link CKEDITOR#NODE_TEXT} type.
1020 * @returns {CKEDITOR.dom.element}
1021 */
1022 getCommonAncestor: function( includeSelf, ignoreTextNode ) {
1023 var start = this.startContainer,
1024 end = this.endContainer,
1025 ancestor;
1026
1027 if ( start.equals( end ) ) {
1028 if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 )
1029 ancestor = start.getChild( this.startOffset );
1030 else
1031 ancestor = start;
1032 } else {
1033 ancestor = start.getCommonAncestor( end );
1034 }
1035
1036 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
1037 },
1038
1039 /**
1040 * Transforms the {@link #startContainer} and {@link #endContainer} properties from text
1041 * nodes to element nodes, whenever possible. This is actually possible
1042 * if either of the boundary containers point to a text node, and its
1043 * offset is set to zero, or after the last char in the node.
1044 */
1045 optimize: function() {
1046 var container = this.startContainer;
1047 var offset = this.startOffset;
1048
1049 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1050 if ( !offset )
1051 this.setStartBefore( container );
1052 else if ( offset >= container.getLength() )
1053 this.setStartAfter( container );
1054 }
1055
1056 container = this.endContainer;
1057 offset = this.endOffset;
1058
1059 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1060 if ( !offset )
1061 this.setEndBefore( container );
1062 else if ( offset >= container.getLength() )
1063 this.setEndAfter( container );
1064 }
1065 },
1066
1067 /**
1068 * Move the range out of bookmark nodes if they'd been the container.
1069 */
1070 optimizeBookmark: function() {
1071 var startNode = this.startContainer,
1072 endNode = this.endContainer;
1073
1074 if ( startNode.is && startNode.is( 'span' ) && startNode.data( 'cke-bookmark' ) )
1075 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
1076 if ( endNode && endNode.is && endNode.is( 'span' ) && endNode.data( 'cke-bookmark' ) )
1077 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
1078 },
1079
1080 /**
1081 * @param {Boolean} [ignoreStart=false]
1082 * @param {Boolean} [ignoreEnd=false]
1083 * @todo precise desc/algorithm
1084 */
1085 trim: function( ignoreStart, ignoreEnd ) {
1086 var startContainer = this.startContainer,
1087 startOffset = this.startOffset,
1088 collapsed = this.collapsed;
1089 if ( ( !ignoreStart || collapsed ) && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1090 // If the offset is zero, we just insert the new node before
1091 // the start.
1092 if ( !startOffset ) {
1093 startOffset = startContainer.getIndex();
1094 startContainer = startContainer.getParent();
1095 }
1096 // If the offset is at the end, we'll insert it after the text
1097 // node.
1098 else if ( startOffset >= startContainer.getLength() ) {
1099 startOffset = startContainer.getIndex() + 1;
1100 startContainer = startContainer.getParent();
1101 }
1102 // In other case, we split the text node and insert the new
1103 // node at the split point.
1104 else {
1105 var nextText = startContainer.split( startOffset );
1106
1107 startOffset = startContainer.getIndex() + 1;
1108 startContainer = startContainer.getParent();
1109
1110 // Check all necessity of updating the end boundary.
1111 if ( this.startContainer.equals( this.endContainer ) )
1112 this.setEnd( nextText, this.endOffset - this.startOffset );
1113 else if ( startContainer.equals( this.endContainer ) )
1114 this.endOffset += 1;
1115 }
1116
1117 this.setStart( startContainer, startOffset );
1118
1119 if ( collapsed ) {
1120 this.collapse( true );
1121 return;
1122 }
1123 }
1124
1125 var endContainer = this.endContainer;
1126 var endOffset = this.endOffset;
1127
1128 if ( !( ignoreEnd || collapsed ) && endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1129 // If the offset is zero, we just insert the new node before
1130 // the start.
1131 if ( !endOffset ) {
1132 endOffset = endContainer.getIndex();
1133 endContainer = endContainer.getParent();
1134 }
1135 // If the offset is at the end, we'll insert it after the text
1136 // node.
1137 else if ( endOffset >= endContainer.getLength() ) {
1138 endOffset = endContainer.getIndex() + 1;
1139 endContainer = endContainer.getParent();
1140 }
1141 // In other case, we split the text node and insert the new
1142 // node at the split point.
1143 else {
1144 endContainer.split( endOffset );
1145
1146 endOffset = endContainer.getIndex() + 1;
1147 endContainer = endContainer.getParent();
1148 }
1149
1150 this.setEnd( endContainer, endOffset );
1151 }
1152 },
1153
1154 /**
1155 * Expands the range so that partial units are completely contained.
1156 *
1157 * @param unit {Number} The unit type to expand with.
1158 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
1159 */
1160 enlarge: function( unit, excludeBrs ) {
1161 var leadingWhitespaceRegex = new RegExp( /[^\s\ufeff]/ );
1162
1163 switch ( unit ) {
1164 case CKEDITOR.ENLARGE_INLINE:
1165 var enlargeInlineOnly = 1;
1166
1167 /* falls through */
1168 case CKEDITOR.ENLARGE_ELEMENT:
1169
1170 if ( this.collapsed )
1171 return;
1172
1173 // Get the common ancestor.
1174 var commonAncestor = this.getCommonAncestor();
1175
1176 var boundary = this.root;
1177
1178 // For each boundary
1179 // a. Depending on its position, find out the first node to be checked (a sibling) or,
1180 // if not available, to be enlarge.
1181 // b. Go ahead checking siblings and enlarging the boundary as much as possible until the
1182 // common ancestor is not reached. After reaching the common ancestor, just save the
1183 // enlargeable node to be used later.
1184
1185 var startTop, endTop;
1186
1187 var enlargeable, sibling, commonReached;
1188
1189 // Indicates that the node can be added only if whitespace
1190 // is available before it.
1191 var needsWhiteSpace = false;
1192 var isWhiteSpace;
1193 var siblingText;
1194
1195 // Process the start boundary.
1196
1197 var container = this.startContainer;
1198 var offset = this.startOffset;
1199
1200 if ( container.type == CKEDITOR.NODE_TEXT ) {
1201 if ( offset ) {
1202 // Check if there is any non-space text before the
1203 // offset. Otherwise, container is null.
1204 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
1205
1206 // If we found only whitespace in the node, it
1207 // means that we'll need more whitespace to be able
1208 // to expand. For example, <i> can be expanded in
1209 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1210 needsWhiteSpace = !!container;
1211 }
1212
1213 if ( container ) {
1214 if ( !( sibling = container.getPrevious() ) )
1215 enlargeable = container.getParent();
1216 }
1217 } else {
1218 // If we have offset, get the node preceeding it as the
1219 // first sibling to be checked.
1220 if ( offset )
1221 sibling = container.getChild( offset - 1 ) || container.getLast();
1222
1223 // If there is no sibling, mark the container to be
1224 // enlarged.
1225 if ( !sibling )
1226 enlargeable = container;
1227 }
1228
1229 // Ensures that enlargeable can be indeed enlarged, if not it will be nulled.
1230 enlargeable = getValidEnlargeable( enlargeable );
1231
1232 while ( enlargeable || sibling ) {
1233 if ( enlargeable && !sibling ) {
1234 // If we reached the common ancestor, mark the flag
1235 // for it.
1236 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1237 commonReached = true;
1238
1239 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1240 break;
1241
1242 // If we don't need space or this element breaks
1243 // the line, then enlarge it.
1244 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1245 needsWhiteSpace = false;
1246
1247 // If the common ancestor has been reached,
1248 // we'll not enlarge it immediately, but just
1249 // mark it to be enlarged later if the end
1250 // boundary also enlarges it.
1251 if ( commonReached )
1252 startTop = enlargeable;
1253 else
1254 this.setStartBefore( enlargeable );
1255 }
1256
1257 sibling = enlargeable.getPrevious();
1258 }
1259
1260 // Check all sibling nodes preceeding the enlargeable
1261 // node. The node wil lbe enlarged only if none of them
1262 // blocks it.
1263 while ( sibling ) {
1264 // This flag indicates that this node has
1265 // whitespaces at the end.
1266 isWhiteSpace = false;
1267
1268 if ( sibling.type == CKEDITOR.NODE_COMMENT ) {
1269 sibling = sibling.getPrevious();
1270 continue;
1271 } else if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1272 siblingText = sibling.getText();
1273
1274 if ( leadingWhitespaceRegex.test( siblingText ) )
1275 sibling = null;
1276
1277 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
1278 } else {
1279 // #12221 (Chrome) plus #11111 (Safari).
1280 var offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
1281
1282 // If this is a visible element.
1283 // We need to check for the bookmark attribute because IE insists on
1284 // rendering the display:none nodes we use for bookmarks. (#3363)
1285 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1286 if ( ( sibling.$.offsetWidth > offsetWidth0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1287 // We'll accept it only if we need
1288 // whitespace, and this is an inline
1289 // element with whitespace only.
1290 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1291 // It must contains spaces and inline elements only.
1292
1293 siblingText = sibling.getText();
1294
1295 if ( leadingWhitespaceRegex.test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
1296 sibling = null;
1297 else {
1298 var allChildren = sibling.$.getElementsByTagName( '*' );
1299 for ( var i = 0, child; child = allChildren[ i++ ]; ) {
1300 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1301 sibling = null;
1302 break;
1303 }
1304 }
1305 }
1306
1307 if ( sibling )
1308 isWhiteSpace = !!siblingText.length;
1309 } else {
1310 sibling = null;
1311 }
1312 }
1313 }
1314
1315 // A node with whitespaces has been found.
1316 if ( isWhiteSpace ) {
1317 // Enlarge the last enlargeable node, if we
1318 // were waiting for spaces.
1319 if ( needsWhiteSpace ) {
1320 if ( commonReached )
1321 startTop = enlargeable;
1322 else if ( enlargeable )
1323 this.setStartBefore( enlargeable );
1324 } else {
1325 needsWhiteSpace = true;
1326 }
1327 }
1328
1329 if ( sibling ) {
1330 var next = sibling.getPrevious();
1331
1332 if ( !enlargeable && !next ) {
1333 // Set the sibling as enlargeable, so it's
1334 // parent will be get later outside this while.
1335 enlargeable = sibling;
1336 sibling = null;
1337 break;
1338 }
1339
1340 sibling = next;
1341 } else {
1342 // If sibling has been set to null, then we
1343 // need to stop enlarging.
1344 enlargeable = null;
1345 }
1346 }
1347
1348 if ( enlargeable )
1349 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1350 }
1351
1352 // Process the end boundary. This is basically the same
1353 // code used for the start boundary, with small changes to
1354 // make it work in the oposite side (to the right). This
1355 // makes it difficult to reuse the code here. So, fixes to
1356 // the above code are likely to be replicated here.
1357
1358 container = this.endContainer;
1359 offset = this.endOffset;
1360
1361 // Reset the common variables.
1362 enlargeable = sibling = null;
1363 commonReached = needsWhiteSpace = false;
1364
1365 // Function check if there are only whitespaces from the given starting point
1366 // (startContainer and startOffset) till the end on block.
1367 // Examples ("[" is the start point):
1368 // - <p>foo[ </p> - will return true,
1369 // - <p><b>foo[ </b> </p> - will return true,
1370 // - <p>foo[ bar</p> - will return false,
1371 // - <p><b>foo[ </b>bar</p> - will return false,
1372 // - <p>foo[ <b></b></p> - will return false.
1373 function onlyWhiteSpaces( startContainer, startOffset ) {
1374 // We need to enlarge range if there is white space at the end of the block,
1375 // because it is not displayed in WYSIWYG mode and user can not select it. So
1376 // "<p>foo[bar] </p>" should be changed to "<p>foo[bar ]</p>". On the other hand
1377 // we should do nothing if we are not at the end of the block, so this should not
1378 // be changed: "<p><i>[foo] </i>bar</p>".
1379 var walkerRange = new CKEDITOR.dom.range( boundary );
1380 walkerRange.setStart( startContainer, startOffset );
1381 // The guard will find the end of range so I put boundary here.
1382 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1383
1384 var walker = new CKEDITOR.dom.walker( walkerRange ),
1385 node;
1386
1387 walker.guard = function( node ) {
1388 // Stop if you exit block.
1389 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() );
1390 };
1391
1392 while ( ( node = walker.next() ) ) {
1393 if ( node.type != CKEDITOR.NODE_TEXT ) {
1394 // Stop if you enter to any node (walker.next() will return node only
1395 // it goes out, not if it is go into node).
1396 return false;
1397 } else {
1398 // Trim the first node to startOffset.
1399 if ( node != startContainer )
1400 siblingText = node.getText();
1401 else
1402 siblingText = node.substring( startOffset );
1403
1404 // Check if it is white space.
1405 if ( leadingWhitespaceRegex.test( siblingText ) )
1406 return false;
1407 }
1408 }
1409
1410 return true;
1411 }
1412
1413 if ( container.type == CKEDITOR.NODE_TEXT ) {
1414 // Check if there is only white space after the offset.
1415 if ( CKEDITOR.tools.trim( container.substring( offset ) ).length ) {
1416 // If we found only whitespace in the node, it
1417 // means that we'll need more whitespace to be able
1418 // to expand. For example, <i> can be expanded in
1419 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1420 needsWhiteSpace = true;
1421 } else {
1422 needsWhiteSpace = !container.getLength();
1423
1424 if ( offset == container.getLength() ) {
1425 // If we are at the end of container and this is the last text node,
1426 // we should enlarge end to the parent.
1427 if ( !( sibling = container.getNext() ) )
1428 enlargeable = container.getParent();
1429 } else {
1430 // If we are in the middle on text node and there are only whitespaces
1431 // till the end of block, we should enlarge element.
1432 if ( onlyWhiteSpaces( container, offset ) )
1433 enlargeable = container.getParent();
1434 }
1435 }
1436 } else {
1437 // Get the node right after the boudary to be checked
1438 // first.
1439 sibling = container.getChild( offset );
1440
1441 if ( !sibling )
1442 enlargeable = container;
1443 }
1444
1445 while ( enlargeable || sibling ) {
1446 if ( enlargeable && !sibling ) {
1447 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1448 commonReached = true;
1449
1450 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1451 break;
1452
1453 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1454 needsWhiteSpace = false;
1455
1456 if ( commonReached )
1457 endTop = enlargeable;
1458 else if ( enlargeable )
1459 this.setEndAfter( enlargeable );
1460 }
1461
1462 sibling = enlargeable.getNext();
1463 }
1464
1465 while ( sibling ) {
1466 isWhiteSpace = false;
1467
1468 if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1469 siblingText = sibling.getText();
1470
1471 // Check if there are not whitespace characters till the end of editable.
1472 // If so stop expanding.
1473 if ( !onlyWhiteSpaces( sibling, 0 ) )
1474 sibling = null;
1475
1476 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
1477 } else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) {
1478 // If this is a visible element.
1479 // We need to check for the bookmark attribute because IE insists on
1480 // rendering the display:none nodes we use for bookmarks. (#3363)
1481 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1482 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1483 // We'll accept it only if we need
1484 // whitespace, and this is an inline
1485 // element with whitespace only.
1486 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1487 // It must contains spaces and inline elements only.
1488
1489 siblingText = sibling.getText();
1490
1491 if ( leadingWhitespaceRegex.test( siblingText ) )
1492 sibling = null;
1493 else {
1494 allChildren = sibling.$.getElementsByTagName( '*' );
1495 for ( i = 0; child = allChildren[ i++ ]; ) {
1496 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1497 sibling = null;
1498 break;
1499 }
1500 }
1501 }
1502
1503 if ( sibling )
1504 isWhiteSpace = !!siblingText.length;
1505 } else {
1506 sibling = null;
1507 }
1508 }
1509 } else {
1510 isWhiteSpace = 1;
1511 }
1512
1513 if ( isWhiteSpace ) {
1514 if ( needsWhiteSpace ) {
1515 if ( commonReached )
1516 endTop = enlargeable;
1517 else
1518 this.setEndAfter( enlargeable );
1519 }
1520 }
1521
1522 if ( sibling ) {
1523 next = sibling.getNext();
1524
1525 if ( !enlargeable && !next ) {
1526 enlargeable = sibling;
1527 sibling = null;
1528 break;
1529 }
1530
1531 sibling = next;
1532 } else {
1533 // If sibling has been set to null, then we
1534 // need to stop enlarging.
1535 enlargeable = null;
1536 }
1537 }
1538
1539 if ( enlargeable )
1540 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1541 }
1542
1543 // If the common ancestor can be enlarged by both boundaries, then include it also.
1544 if ( startTop && endTop ) {
1545 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
1546
1547 this.setStartBefore( commonAncestor );
1548 this.setEndAfter( commonAncestor );
1549 }
1550 break;
1551
1552 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
1553 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
1554
1555 // Enlarging the start boundary.
1556 var walkerRange = new CKEDITOR.dom.range( this.root );
1557
1558 boundary = this.root;
1559
1560 walkerRange.setStartAt( boundary, CKEDITOR.POSITION_AFTER_START );
1561 walkerRange.setEnd( this.startContainer, this.startOffset );
1562
1563 var walker = new CKEDITOR.dom.walker( walkerRange ),
1564 blockBoundary, // The node on which the enlarging should stop.
1565 tailBr, // In case BR as block boundary.
1566 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary( ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br: 1 } : null ),
1567 inNonEditable = null,
1568 // Record the encountered 'blockBoundary' for later use.
1569 boundaryGuard = function( node ) {
1570 // We should not check contents of non-editable elements. It may happen
1571 // that inline widget has display:table child which should not block range#enlarge.
1572 // When encoutered non-editable element...
1573 if ( node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'false' ) {
1574 if ( inNonEditable ) {
1575 // ... in which we already were, reset it (because we're leaving it) and return.
1576 if ( inNonEditable.equals( node ) ) {
1577 inNonEditable = null;
1578 return;
1579 }
1580 // ... which we're entering, remember it but check it (no return).
1581 } else {
1582 inNonEditable = node;
1583 }
1584 // When we are in non-editable element, do not check if current node is a block boundary.
1585 } else if ( inNonEditable ) {
1586 return;
1587 }
1588
1589 var retval = notBlockBoundary( node );
1590 if ( !retval )
1591 blockBoundary = node;
1592 return retval;
1593 },
1594 // Record the encounted 'tailBr' for later use.
1595 tailBrGuard = function( node ) {
1596 var retval = boundaryGuard( node );
1597 if ( !retval && node.is && node.is( 'br' ) )
1598 tailBr = node;
1599 return retval;
1600 };
1601
1602 walker.guard = boundaryGuard;
1603
1604 enlargeable = walker.lastBackward();
1605
1606 // It's the body which stop the enlarging if no block boundary found.
1607 blockBoundary = blockBoundary || boundary;
1608
1609 // Start the range either after the end of found block (<p>...</p>[text)
1610 // or at the start of block (<p>[text...), by comparing the document position
1611 // with 'enlargeable' node.
1612 this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() ||
1613 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END );
1614
1615 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
1616 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) {
1617 var theRange = this.clone();
1618 walker = new CKEDITOR.dom.walker( theRange );
1619
1620 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
1621 bookmark = CKEDITOR.dom.walker.bookmark();
1622
1623 walker.evaluator = function( node ) {
1624 return !whitespaces( node ) && !bookmark( node );
1625 };
1626 var previous = walker.previous();
1627 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
1628 return;
1629 }
1630
1631 // Enlarging the end boundary.
1632 // Set up new range and reset all flags (blockBoundary, inNonEditable, tailBr).
1633
1634 walkerRange = this.clone();
1635 walkerRange.collapse();
1636 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1637 walker = new CKEDITOR.dom.walker( walkerRange );
1638
1639 // tailBrGuard only used for on range end.
1640 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? tailBrGuard : boundaryGuard;
1641 blockBoundary = inNonEditable = tailBr = null;
1642
1643 // End the range right before the block boundary node.
1644 enlargeable = walker.lastForward();
1645
1646 // It's the body which stop the enlarging if no block boundary found.
1647 blockBoundary = blockBoundary || boundary;
1648
1649 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
1650 // by comparing the document position with 'enlargeable' node.
1651 this.setEndAt( blockBoundary, ( !enlargeable && this.checkEndOfBlock() ||
1652 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_BEFORE_START );
1653 // We must include the <br> at the end of range if there's
1654 // one and we're expanding list item contents
1655 if ( tailBr ) {
1656 this.setEndAfter( tailBr );
1657 }
1658 }
1659
1660 // Ensures that returned element can be enlarged by selection, null otherwise.
1661 // @param {CKEDITOR.dom.element} enlargeable
1662 // @returns {CKEDITOR.dom.element/null}
1663 function getValidEnlargeable( enlargeable ) {
1664 return enlargeable && enlargeable.type == CKEDITOR.NODE_ELEMENT && enlargeable.hasAttribute( 'contenteditable' ) ?
1665 null : enlargeable;
1666 }
1667 },
1668
1669 /**
1670 * Descrease the range to make sure that boundaries
1671 * always anchor beside text nodes or innermost element.
1672 *
1673 * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}).
1674 *
1675 * * {@link CKEDITOR#SHRINK_ELEMENT} - Shrink the range boundaries to the edge of the innermost element.
1676 * * {@link CKEDITOR#SHRINK_TEXT} - Shrink the range boudaries to anchor by the side of enclosed text
1677 * node, range remains if there's no text nodes on boundaries at all.
1678 *
1679 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
1680 */
1681 shrink: function( mode, selectContents, shrinkOnBlockBoundary ) {
1682 // Unable to shrink a collapsed range.
1683 if ( !this.collapsed ) {
1684 mode = mode || CKEDITOR.SHRINK_TEXT;
1685
1686 var walkerRange = this.clone();
1687
1688 var startContainer = this.startContainer,
1689 endContainer = this.endContainer,
1690 startOffset = this.startOffset,
1691 endOffset = this.endOffset;
1692
1693 // Whether the start/end boundary is moveable.
1694 var moveStart = 1,
1695 moveEnd = 1;
1696
1697 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1698 if ( !startOffset )
1699 walkerRange.setStartBefore( startContainer );
1700 else if ( startOffset >= startContainer.getLength() )
1701 walkerRange.setStartAfter( startContainer );
1702 else {
1703 // Enlarge the range properly to avoid walker making
1704 // DOM changes caused by triming the text nodes later.
1705 walkerRange.setStartBefore( startContainer );
1706 moveStart = 0;
1707 }
1708 }
1709
1710 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1711 if ( !endOffset )
1712 walkerRange.setEndBefore( endContainer );
1713 else if ( endOffset >= endContainer.getLength() )
1714 walkerRange.setEndAfter( endContainer );
1715 else {
1716 walkerRange.setEndAfter( endContainer );
1717 moveEnd = 0;
1718 }
1719 }
1720
1721 var walker = new CKEDITOR.dom.walker( walkerRange ),
1722 isBookmark = CKEDITOR.dom.walker.bookmark();
1723
1724 walker.evaluator = function( node ) {
1725 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
1726 };
1727
1728 var currentElement;
1729 walker.guard = function( node, movingOut ) {
1730 if ( isBookmark( node ) )
1731 return true;
1732
1733 // Stop when we're shrink in element mode while encountering a text node.
1734 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
1735 return false;
1736
1737 // Stop when we've already walked "through" an element.
1738 if ( movingOut && node.equals( currentElement ) )
1739 return false;
1740
1741 if ( shrinkOnBlockBoundary === false && node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() )
1742 return false;
1743
1744 // Stop shrinking when encountering an editable border.
1745 if ( node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'contenteditable' ) )
1746 return false;
1747
1748 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
1749 currentElement = node;
1750
1751 return true;
1752 };
1753
1754 if ( moveStart ) {
1755 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next' ]();
1756 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
1757 }
1758
1759 if ( moveEnd ) {
1760 walker.reset();
1761 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous' ]();
1762 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
1763 }
1764
1765 return !!( moveStart || moveEnd );
1766 }
1767 },
1768
1769 /**
1770 * Inserts a node at the start of the range. The range will be expanded
1771 * the contain the node.
1772 *
1773 * @param {CKEDITOR.dom.node} node
1774 */
1775 insertNode: function( node ) {
1776 this.optimizeBookmark();
1777 this.trim( false, true );
1778
1779 var startContainer = this.startContainer;
1780 var startOffset = this.startOffset;
1781
1782 var nextNode = startContainer.getChild( startOffset );
1783
1784 if ( nextNode )
1785 node.insertBefore( nextNode );
1786 else
1787 startContainer.append( node );
1788
1789 // Check if we need to update the end boundary.
1790 if ( node.getParent() && node.getParent().equals( this.endContainer ) )
1791 this.endOffset++;
1792
1793 // Expand the range to embrace the new node.
1794 this.setStartBefore( node );
1795 },
1796
1797 /**
1798 * Moves the range to given position according to specified node.
1799 *
1800 * // HTML: <p>Foo <b>bar</b></p>
1801 * range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START );
1802 * // Range will be moved to: <p>Foo ^<b>bar</b></p>
1803 *
1804 * See also {@link #setStartAt} and {@link #setEndAt}.
1805 *
1806 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
1807 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1808 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1809 * {@link CKEDITOR#POSITION_AFTER_END}.
1810 */
1811 moveToPosition: function( node, position ) {
1812 this.setStartAt( node, position );
1813 this.collapse( true );
1814 },
1815
1816 /**
1817 * Moves the range to the exact position of the specified range.
1818 *
1819 * @param {CKEDITOR.dom.range} range
1820 */
1821 moveToRange: function( range ) {
1822 this.setStart( range.startContainer, range.startOffset );
1823 this.setEnd( range.endContainer, range.endOffset );
1824 },
1825
1826 /**
1827 * Select nodes content. Range will start and end inside this node.
1828 *
1829 * @param {CKEDITOR.dom.node} node
1830 */
1831 selectNodeContents: function( node ) {
1832 this.setStart( node, 0 );
1833 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
1834 },
1835
1836 /**
1837 * Sets the start position of a range.
1838 *
1839 * @param {CKEDITOR.dom.node} startNode The node to start the range.
1840 * @param {Number} startOffset An integer greater than or equal to zero
1841 * representing the offset for the start of the range from the start
1842 * of `startNode`.
1843 */
1844 setStart: function( startNode, startOffset ) {
1845 // W3C requires a check for the new position. If it is after the end
1846 // boundary, the range should be collapsed to the new start. It seams
1847 // we will not need this check for our use of this class so we can
1848 // ignore it for now.
1849
1850 // Fixing invalid range start inside dtd empty elements.
1851 if ( startNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ startNode.getName() ] )
1852 startOffset = startNode.getIndex(), startNode = startNode.getParent();
1853
1854 this._setStartContainer( startNode );
1855 this.startOffset = startOffset;
1856
1857 if ( !this.endContainer ) {
1858 this._setEndContainer( startNode );
1859 this.endOffset = startOffset;
1860 }
1861
1862 updateCollapsed( this );
1863 },
1864
1865 /**
1866 * Sets the end position of a Range.
1867 *
1868 * @param {CKEDITOR.dom.node} endNode The node to end the range.
1869 * @param {Number} endOffset An integer greater than or equal to zero
1870 * representing the offset for the end of the range from the start
1871 * of `endNode`.
1872 */
1873 setEnd: function( endNode, endOffset ) {
1874 // W3C requires a check for the new position. If it is before the start
1875 // boundary, the range should be collapsed to the new end. It seams we
1876 // will not need this check for our use of this class so we can ignore
1877 // it for now.
1878
1879 // Fixing invalid range end inside dtd empty elements.
1880 if ( endNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ endNode.getName() ] )
1881 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
1882
1883 this._setEndContainer( endNode );
1884 this.endOffset = endOffset;
1885
1886 if ( !this.startContainer ) {
1887 this._setStartContainer( endNode );
1888 this.startOffset = endOffset;
1889 }
1890
1891 updateCollapsed( this );
1892 },
1893
1894 /**
1895 * Sets start of this range after the specified node.
1896 *
1897 * // Range: <p>foo<b>bar</b>^</p>
1898 * range.setStartAfter( textFoo );
1899 * // The range will be changed to:
1900 * // <p>foo[<b>bar</b>]</p>
1901 *
1902 * @param {CKEDITOR.dom.node} node
1903 */
1904 setStartAfter: function( node ) {
1905 this.setStart( node.getParent(), node.getIndex() + 1 );
1906 },
1907
1908 /**
1909 * Sets start of this range after the specified node.
1910 *
1911 * // Range: <p>foo<b>bar</b>^</p>
1912 * range.setStartBefore( elB );
1913 * // The range will be changed to:
1914 * // <p>foo[<b>bar</b>]</p>
1915 *
1916 * @param {CKEDITOR.dom.node} node
1917 */
1918 setStartBefore: function( node ) {
1919 this.setStart( node.getParent(), node.getIndex() );
1920 },
1921
1922 /**
1923 * Sets end of this range after the specified node.
1924 *
1925 * // Range: <p>foo^<b>bar</b></p>
1926 * range.setEndAfter( elB );
1927 * // The range will be changed to:
1928 * // <p>foo[<b>bar</b>]</p>
1929 *
1930 * @param {CKEDITOR.dom.node} node
1931 */
1932 setEndAfter: function( node ) {
1933 this.setEnd( node.getParent(), node.getIndex() + 1 );
1934 },
1935
1936 /**
1937 * Sets end of this range before the specified node.
1938 *
1939 * // Range: <p>^foo<b>bar</b></p>
1940 * range.setStartAfter( textBar );
1941 * // The range will be changed to:
1942 * // <p>[foo<b>]bar</b></p>
1943 *
1944 * @param {CKEDITOR.dom.node} node
1945 */
1946 setEndBefore: function( node ) {
1947 this.setEnd( node.getParent(), node.getIndex() );
1948 },
1949
1950 /**
1951 * Moves the start of this range to given position according to specified node.
1952 *
1953 * // HTML: <p>Foo <b>bar</b>^</p>
1954 * range.setStartAt( elB, CKEDITOR.POSITION_AFTER_START );
1955 * // The range will be changed to:
1956 * // <p>Foo <b>[bar</b>]</p>
1957 *
1958 * See also {@link #setEndAt} and {@link #moveToPosition}.
1959 *
1960 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
1961 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1962 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1963 * {@link CKEDITOR#POSITION_AFTER_END}.
1964 */
1965 setStartAt: function( node, position ) {
1966 switch ( position ) {
1967 case CKEDITOR.POSITION_AFTER_START:
1968 this.setStart( node, 0 );
1969 break;
1970
1971 case CKEDITOR.POSITION_BEFORE_END:
1972 if ( node.type == CKEDITOR.NODE_TEXT )
1973 this.setStart( node, node.getLength() );
1974 else
1975 this.setStart( node, node.getChildCount() );
1976 break;
1977
1978 case CKEDITOR.POSITION_BEFORE_START:
1979 this.setStartBefore( node );
1980 break;
1981
1982 case CKEDITOR.POSITION_AFTER_END:
1983 this.setStartAfter( node );
1984 }
1985
1986 updateCollapsed( this );
1987 },
1988
1989 /**
1990 * Moves the end of this range to given position according to specified node.
1991 *
1992 * // HTML: <p>^Foo <b>bar</b></p>
1993 * range.setEndAt( textBar, CKEDITOR.POSITION_BEFORE_START );
1994 * // The range will be changed to:
1995 * // <p>[Foo <b>]bar</b></p>
1996 *
1997 * See also {@link #setStartAt} and {@link #moveToPosition}.
1998 *
1999 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2000 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2001 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2002 * {@link CKEDITOR#POSITION_AFTER_END}.
2003 */
2004 setEndAt: function( node, position ) {
2005 switch ( position ) {
2006 case CKEDITOR.POSITION_AFTER_START:
2007 this.setEnd( node, 0 );
2008 break;
2009
2010 case CKEDITOR.POSITION_BEFORE_END:
2011 if ( node.type == CKEDITOR.NODE_TEXT )
2012 this.setEnd( node, node.getLength() );
2013 else
2014 this.setEnd( node, node.getChildCount() );
2015 break;
2016
2017 case CKEDITOR.POSITION_BEFORE_START:
2018 this.setEndBefore( node );
2019 break;
2020
2021 case CKEDITOR.POSITION_AFTER_END:
2022 this.setEndAfter( node );
2023 }
2024
2025 updateCollapsed( this );
2026 },
2027
2028 /**
2029 * Wraps inline content found around the range's start or end boundary
2030 * with a block element.
2031 *
2032 * // Assuming the following range:
2033 * // <h1>foo</h1>ba^r<br />bom<p>foo</p>
2034 * // The result of executing:
2035 * range.fixBlock( true, 'p' );
2036 * // will be:
2037 * // <h1>foo</h1><p>ba^r<br />bom</p><p>foo</p>
2038 *
2039 * Non-collapsed range:
2040 *
2041 * // Assuming the following range:
2042 * // ba[r<p>foo</p>bo]m
2043 * // The result of executing:
2044 * range.fixBlock( false, 'p' );
2045 * // will be:
2046 * // ba[r<p>foo</p><p>bo]m</p>
2047 *
2048 * @param {Boolean} isStart Whether the start or end boundary of a range should be checked.
2049 * @param {String} blockTag The name of a block element in which content will be wrapped.
2050 * For example: `'p'`.
2051 * @returns {CKEDITOR.dom.element} Created block wrapper.
2052 */
2053 fixBlock: function( isStart, blockTag ) {
2054 var bookmark = this.createBookmark(),
2055 fixedBlock = this.document.createElement( blockTag );
2056
2057 this.collapse( isStart );
2058
2059 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
2060
2061 this.extractContents().appendTo( fixedBlock );
2062 fixedBlock.trim();
2063
2064 this.insertNode( fixedBlock );
2065
2066 // Bogus <br> could already exist in the range's container before fixBlock() was called. In such case it was
2067 // extracted and appended to the fixBlock. However, we are not sure that it's at the end of
2068 // the fixedBlock, because of FF's terrible bug. When creating a bookmark in an empty editable
2069 // FF moves the bogus <br> before that bookmark (<editable><br /><bm />[]</editable>).
2070 // So even if the initial range was placed before the bogus <br>, after creating the bookmark it
2071 // is placed before the bookmark.
2072 // Fortunately, getBogus() is able to skip the bookmark so it finds the bogus <br> in this case.
2073 // We remove incorrectly placed one and add a brand new one. (#13001)
2074 var bogus = fixedBlock.getBogus();
2075 if ( bogus ) {
2076 bogus.remove();
2077 }
2078 fixedBlock.appendBogus();
2079
2080 this.moveToBookmark( bookmark );
2081
2082 return fixedBlock;
2083 },
2084
2085 /**
2086 * @todo
2087 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result blocks.
2088 */
2089 splitBlock: function( blockTag, cloneId ) {
2090 var startPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ),
2091 endPath = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2092
2093 var startBlockLimit = startPath.blockLimit,
2094 endBlockLimit = endPath.blockLimit;
2095
2096 var startBlock = startPath.block,
2097 endBlock = endPath.block;
2098
2099 var elementPath = null;
2100 // Do nothing if the boundaries are in different block limits.
2101 if ( !startBlockLimit.equals( endBlockLimit ) )
2102 return null;
2103
2104 // Get or fix current blocks.
2105 if ( blockTag != 'br' ) {
2106 if ( !startBlock ) {
2107 startBlock = this.fixBlock( true, blockTag );
2108 endBlock = new CKEDITOR.dom.elementPath( this.endContainer, this.root ).block;
2109 }
2110
2111 if ( !endBlock )
2112 endBlock = this.fixBlock( false, blockTag );
2113 }
2114
2115 // Get the range position.
2116 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
2117 isEndOfBlock = endBlock && this.checkEndOfBlock();
2118
2119 // Delete the current contents.
2120 // TODO: Why is 2.x doing CheckIsEmpty()?
2121 this.deleteContents();
2122
2123 if ( startBlock && startBlock.equals( endBlock ) ) {
2124 if ( isEndOfBlock ) {
2125 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2126 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
2127 endBlock = null;
2128 } else if ( isStartOfBlock ) {
2129 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2130 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
2131 startBlock = null;
2132 } else {
2133 endBlock = this.splitElement( startBlock, cloneId || false );
2134
2135 // In Gecko, the last child node must be a bogus <br>.
2136 // Note: bogus <br> added under <ul> or <ol> would cause
2137 // lists to be incorrectly rendered.
2138 if ( !startBlock.is( 'ul', 'ol' ) )
2139 startBlock.appendBogus();
2140 }
2141 }
2142
2143 return {
2144 previousBlock: startBlock,
2145 nextBlock: endBlock,
2146 wasStartOfBlock: isStartOfBlock,
2147 wasEndOfBlock: isEndOfBlock,
2148 elementPath: elementPath
2149 };
2150 },
2151
2152 /**
2153 * Branch the specified element from the collapsed range position and
2154 * place the caret between the two result branches.
2155 *
2156 * **Note:** The range must be collapsed and been enclosed by this element.
2157 *
2158 * @param {CKEDITOR.dom.element} element
2159 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result elements.
2160 * @returns {CKEDITOR.dom.element} Root element of the new branch after the split.
2161 */
2162 splitElement: function( toSplit, cloneId ) {
2163 if ( !this.collapsed )
2164 return null;
2165
2166 // Extract the contents of the block from the selection point to the end
2167 // of its contents.
2168 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
2169 var documentFragment = this.extractContents( false, cloneId || false );
2170
2171 // Duplicate the element after it.
2172 var clone = toSplit.clone( false, cloneId || false );
2173
2174 // Place the extracted contents into the duplicated element.
2175 documentFragment.appendTo( clone );
2176 clone.insertAfter( toSplit );
2177 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
2178 return clone;
2179 },
2180
2181 /**
2182 * Recursively remove any empty path blocks at the range boundary.
2183 *
2184 * @method
2185 * @param {Boolean} atEnd Removal to perform at the end boundary,
2186 * otherwise to perform at the start.
2187 */
2188 removeEmptyBlocksAtEnd: ( function() {
2189
2190 var whitespace = CKEDITOR.dom.walker.whitespaces(),
2191 bookmark = CKEDITOR.dom.walker.bookmark( false );
2192
2193 function childEval( parent ) {
2194 return function( node ) {
2195 // Whitespace, bookmarks, empty inlines.
2196 if ( whitespace( node ) || bookmark( node ) ||
2197 node.type == CKEDITOR.NODE_ELEMENT &&
2198 node.isEmptyInlineRemoveable() ) {
2199 return false;
2200 } else if ( parent.is( 'table' ) && node.is( 'caption' ) ) {
2201 return false;
2202 }
2203
2204 return true;
2205 };
2206 }
2207
2208 return function( atEnd ) {
2209
2210 var bm = this.createBookmark();
2211 var path = this[ atEnd ? 'endPath' : 'startPath' ]();
2212 var block = path.block || path.blockLimit, parent;
2213
2214 // Remove any childless block, including list and table.
2215 while ( block && !block.equals( path.root ) &&
2216 !block.getFirst( childEval( block ) ) ) {
2217 parent = block.getParent();
2218 this[ atEnd ? 'setEndAt' : 'setStartAt' ]( block, CKEDITOR.POSITION_AFTER_END );
2219 block.remove( 1 );
2220 block = parent;
2221 }
2222
2223 this.moveToBookmark( bm );
2224 };
2225
2226 } )(),
2227
2228 /**
2229 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #startContainer}.
2230 *
2231 * @returns {CKEDITOR.dom.elementPath}
2232 */
2233 startPath: function() {
2234 return new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2235 },
2236
2237 /**
2238 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #endContainer}.
2239 *
2240 * @returns {CKEDITOR.dom.elementPath}
2241 */
2242 endPath: function() {
2243 return new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2244 },
2245
2246 /**
2247 * Check whether a range boundary is at the inner boundary of a given
2248 * element.
2249 *
2250 * @param {CKEDITOR.dom.element} element The target element to check.
2251 * @param {Number} checkType The boundary to check for both the range
2252 * and the element. It can be {@link CKEDITOR#START} or {@link CKEDITOR#END}.
2253 * @returns {Boolean} `true` if the range boundary is at the inner
2254 * boundary of the element.
2255 */
2256 checkBoundaryOfElement: function( element, checkType ) {
2257 var checkStart = ( checkType == CKEDITOR.START );
2258
2259 // Create a copy of this range, so we can manipulate it for our checks.
2260 var walkerRange = this.clone();
2261
2262 // Collapse the range at the proper size.
2263 walkerRange.collapse( checkStart );
2264
2265 // Expand the range to element boundary.
2266 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
2267
2268 // Create the walker, which will check if we have anything useful
2269 // in the range.
2270 var walker = new CKEDITOR.dom.walker( walkerRange );
2271 walker.evaluator = elementBoundaryEval( checkStart );
2272
2273 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
2274 },
2275
2276 /**
2277 * **Note:** Calls to this function may produce changes to the DOM. The range may
2278 * be updated to reflect such changes.
2279 *
2280 * @returns {Boolean}
2281 * @todo
2282 */
2283 checkStartOfBlock: function() {
2284 var startContainer = this.startContainer,
2285 startOffset = this.startOffset;
2286
2287 // [IE] Special handling for range start in text with a leading NBSP,
2288 // we it to be isolated, for bogus check.
2289 if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) {
2290 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
2291 if ( nbspRegExp.test( textBefore ) )
2292 this.trim( 0, 1 );
2293 }
2294
2295 // Antecipate the trim() call here, so the walker will not make
2296 // changes to the DOM, which would not get reflected into this
2297 // range otherwise.
2298 this.trim();
2299
2300 // We need to grab the block element holding the start boundary, so
2301 // let's use an element path for it.
2302 var path = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2303
2304 // Creates a range starting at the block start until the range start.
2305 var walkerRange = this.clone();
2306 walkerRange.collapse( true );
2307 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
2308
2309 var walker = new CKEDITOR.dom.walker( walkerRange );
2310 walker.evaluator = getCheckStartEndBlockEvalFunction();
2311
2312 return walker.checkBackward();
2313 },
2314
2315 /**
2316 * **Note:** Calls to this function may produce changes to the DOM. The range may
2317 * be updated to reflect such changes.
2318 *
2319 * @returns {Boolean}
2320 * @todo
2321 */
2322 checkEndOfBlock: function() {
2323 var endContainer = this.endContainer,
2324 endOffset = this.endOffset;
2325
2326 // [IE] Special handling for range end in text with a following NBSP,
2327 // we it to be isolated, for bogus check.
2328 if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT ) {
2329 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
2330 if ( nbspRegExp.test( textAfter ) )
2331 this.trim( 1, 0 );
2332 }
2333
2334 // Antecipate the trim() call here, so the walker will not make
2335 // changes to the DOM, which would not get reflected into this
2336 // range otherwise.
2337 this.trim();
2338
2339 // We need to grab the block element holding the start boundary, so
2340 // let's use an element path for it.
2341 var path = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2342
2343 // Creates a range starting at the block start until the range start.
2344 var walkerRange = this.clone();
2345 walkerRange.collapse( false );
2346 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
2347
2348 var walker = new CKEDITOR.dom.walker( walkerRange );
2349 walker.evaluator = getCheckStartEndBlockEvalFunction();
2350
2351 return walker.checkForward();
2352 },
2353
2354 /**
2355 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.
2356 *
2357 * @param {Function} evaluator Function used as the walker's evaluator.
2358 * @param {Function} [guard] Function used as the walker's guard.
2359 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2360 * default to the root editable if not defined.
2361 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2362 */
2363 getPreviousNode: function( evaluator, guard, boundary ) {
2364 var walkerRange = this.clone();
2365 walkerRange.collapse( 1 );
2366 walkerRange.setStartAt( boundary || this.root, CKEDITOR.POSITION_AFTER_START );
2367
2368 var walker = new CKEDITOR.dom.walker( walkerRange );
2369 walker.evaluator = evaluator;
2370 walker.guard = guard;
2371 return walker.previous();
2372 },
2373
2374 /**
2375 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.
2376 *
2377 * @param {Function} evaluator Function used as the walker's evaluator.
2378 * @param {Function} [guard] Function used as the walker's guard.
2379 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2380 * default to the root editable if not defined.
2381 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2382 */
2383 getNextNode: function( evaluator, guard, boundary ) {
2384 var walkerRange = this.clone();
2385 walkerRange.collapse();
2386 walkerRange.setEndAt( boundary || this.root, CKEDITOR.POSITION_BEFORE_END );
2387
2388 var walker = new CKEDITOR.dom.walker( walkerRange );
2389 walker.evaluator = evaluator;
2390 walker.guard = guard;
2391 return walker.next();
2392 },
2393
2394 /**
2395 * Check if elements at which the range boundaries anchor are read-only,
2396 * with respect to `contenteditable` attribute.
2397 *
2398 * @returns {Boolean}
2399 */
2400 checkReadOnly: ( function() {
2401 function checkNodesEditable( node, anotherEnd ) {
2402 while ( node ) {
2403 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
2404 if ( node.getAttribute( 'contentEditable' ) == 'false' && !node.data( 'cke-editable' ) )
2405 return 0;
2406
2407 // Range enclosed entirely in an editable element.
2408 else if ( node.is( 'html' ) || node.getAttribute( 'contentEditable' ) == 'true' && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
2409 break;
2410
2411 }
2412 node = node.getParent();
2413 }
2414
2415 return 1;
2416 }
2417
2418 return function() {
2419 var startNode = this.startContainer,
2420 endNode = this.endContainer;
2421
2422 // Check if elements path at both boundaries are editable.
2423 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
2424 };
2425 } )(),
2426
2427 /**
2428 * Moves the range boundaries to the first/end editing point inside an
2429 * element.
2430 *
2431 * For example, in an element tree like
2432 * `<p><b><i></i></b> Text</p>`, the start editing point is
2433 * `<p><b><i>^</i></b> Text</p>` (inside `<i>`).
2434 *
2435 * @param {CKEDITOR.dom.element} el The element into which look for the
2436 * editing spot.
2437 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
2438 * @returns {Boolean} Whether range was moved.
2439 */
2440 moveToElementEditablePosition: function( el, isMoveToEnd ) {
2441
2442 function nextDFS( node, childOnly ) {
2443 var next;
2444
2445 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )
2446 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( notIgnoredEval );
2447
2448 if ( !childOnly && !next )
2449 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( notIgnoredEval );
2450
2451 return next;
2452 }
2453
2454 // Handle non-editable element e.g. HR.
2455 if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) {
2456 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2457 return true;
2458 }
2459
2460 var found = 0;
2461
2462 while ( el ) {
2463 // Stop immediately if we've found a text node.
2464 if ( el.type == CKEDITOR.NODE_TEXT ) {
2465 // Put cursor before block filler.
2466 if ( isMoveToEnd && this.endContainer && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )
2467 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2468 else
2469 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2470 found = 1;
2471 break;
2472 }
2473
2474 // If an editable element is found, move inside it, but not stop the searching.
2475 if ( el.type == CKEDITOR.NODE_ELEMENT ) {
2476 if ( el.isEditable() ) {
2477 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_START );
2478 found = 1;
2479 }
2480 // Put cursor before padding block br.
2481 else if ( isMoveToEnd && el.is( 'br' ) && this.endContainer && this.checkEndOfBlock() )
2482 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2483 // Special case - non-editable block. Select entire element, because it does not make sense
2484 // to place collapsed selection next to it, because browsers can't handle that.
2485 else if ( el.getAttribute( 'contenteditable' ) == 'false' && el.is( CKEDITOR.dtd.$block ) ) {
2486 this.setStartBefore( el );
2487 this.setEndAfter( el );
2488 return true;
2489 }
2490 }
2491
2492 el = nextDFS( el, found );
2493 }
2494
2495 return !!found;
2496 },
2497
2498 /**
2499 * Moves the range boundaries to the closest editing point after/before an
2500 * element or the current range position (depends on whether the element was specified).
2501 *
2502 * For example, if the start element has `id="start"`,
2503 * `<p><b>foo</b><span id="start">start</start></p>`, the closest previous editing point is
2504 * `<p><b>foo</b>^<span id="start">start</start></p>` (between `<b>` and `<span>`).
2505 *
2506 * See also: {@link #moveToElementEditablePosition}.
2507 *
2508 * @since 4.3
2509 * @param {CKEDITOR.dom.element} [element] The starting element. If not specified, the current range
2510 * position will be used.
2511 * @param {Boolean} [isMoveForward] Whether move to the end of editable. Otherwise, look back.
2512 * @returns {Boolean} Whether the range was moved.
2513 */
2514 moveToClosestEditablePosition: function( element, isMoveForward ) {
2515 // We don't want to modify original range if there's no editable position.
2516 var range,
2517 found = 0,
2518 sibling,
2519 isElement,
2520 positions = [ CKEDITOR.POSITION_AFTER_END, CKEDITOR.POSITION_BEFORE_START ];
2521
2522 if ( element ) {
2523 // Set collapsed range at one of ends of element.
2524 // Can't clone this range, because this range might not be yet positioned (no containers => errors).
2525 range = new CKEDITOR.dom.range( this.root );
2526 range.moveToPosition( element, positions[ isMoveForward ? 0 : 1 ] );
2527 } else {
2528 range = this.clone();
2529 }
2530
2531 // Start element isn't a block, so we can automatically place range
2532 // next to it.
2533 if ( element && !element.is( CKEDITOR.dtd.$block ) )
2534 found = 1;
2535 else {
2536 // Look for first node that fulfills eval function and place range next to it.
2537 sibling = range[ isMoveForward ? 'getNextEditableNode' : 'getPreviousEditableNode' ]();
2538 if ( sibling ) {
2539 found = 1;
2540 isElement = sibling.type == CKEDITOR.NODE_ELEMENT;
2541
2542 // Special case - eval accepts block element only if it's a non-editable block,
2543 // which we want to select, not place collapsed selection next to it (which browsers
2544 // can't handle).
2545 if ( isElement && sibling.is( CKEDITOR.dtd.$block ) && sibling.getAttribute( 'contenteditable' ) == 'false' ) {
2546 range.setStartAt( sibling, CKEDITOR.POSITION_BEFORE_START );
2547 range.setEndAt( sibling, CKEDITOR.POSITION_AFTER_END );
2548 }
2549 // Handle empty blocks which can be selection containers on old IEs.
2550 else if ( !CKEDITOR.env.needsBrFiller && isElement && sibling.is( CKEDITOR.dom.walker.validEmptyBlockContainers ) ) {
2551 range.setEnd( sibling, 0 );
2552 range.collapse();
2553 } else {
2554 range.moveToPosition( sibling, positions[ isMoveForward ? 1 : 0 ] );
2555 }
2556 }
2557 }
2558
2559 if ( found )
2560 this.moveToRange( range );
2561
2562 return !!found;
2563 },
2564
2565 /**
2566 * See {@link #moveToElementEditablePosition}.
2567 *
2568 * @returns {Boolean} Whether range was moved.
2569 */
2570 moveToElementEditStart: function( target ) {
2571 return this.moveToElementEditablePosition( target );
2572 },
2573
2574 /**
2575 * See {@link #moveToElementEditablePosition}.
2576 *
2577 * @returns {Boolean} Whether range was moved.
2578 */
2579 moveToElementEditEnd: function( target ) {
2580 return this.moveToElementEditablePosition( target, true );
2581 },
2582
2583 /**
2584 * Get the single node enclosed within the range if there's one.
2585 *
2586 * @returns {CKEDITOR.dom.node}
2587 */
2588 getEnclosedNode: function() {
2589 var walkerRange = this.clone();
2590
2591 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
2592 walkerRange.optimize();
2593 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
2594 return null;
2595
2596 var walker = new CKEDITOR.dom.walker( walkerRange ),
2597 isNotBookmarks = CKEDITOR.dom.walker.bookmark( false, true ),
2598 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true );
2599
2600 walker.evaluator = function( node ) {
2601 return isNotWhitespaces( node ) && isNotBookmarks( node );
2602 };
2603 var node = walker.next();
2604 walker.reset();
2605 return node && node.equals( walker.previous() ) ? node : null;
2606 },
2607
2608 /**
2609 * Get the node adjacent to the range start or {@link #startContainer}.
2610 *
2611 * @returns {CKEDITOR.dom.node}
2612 */
2613 getTouchedStartNode: function() {
2614 var container = this.startContainer;
2615
2616 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2617 return container;
2618
2619 return container.getChild( this.startOffset ) || container;
2620 },
2621
2622 /**
2623 * Get the node adjacent to the range end or {@link #endContainer}.
2624 *
2625 * @returns {CKEDITOR.dom.node}
2626 */
2627 getTouchedEndNode: function() {
2628 var container = this.endContainer;
2629
2630 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2631 return container;
2632
2633 return container.getChild( this.endOffset - 1 ) || container;
2634 },
2635
2636 /**
2637 * Gets next node which can be a container of a selection.
2638 * This methods mimics a behavior of right/left arrow keys in case of
2639 * collapsed selection. It does not return an exact position (with offset) though,
2640 * but just a selection's container.
2641 *
2642 * Note: use this method on a collapsed range.
2643 *
2644 * @since 4.3
2645 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2646 */
2647 getNextEditableNode: getNextEditableNode(),
2648
2649 /**
2650 * See {@link #getNextEditableNode}.
2651 *
2652 * @since 4.3
2653 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2654 */
2655 getPreviousEditableNode: getNextEditableNode( 1 ),
2656
2657 /**
2658 * Scrolls the start of current range into view.
2659 */
2660 scrollIntoView: function() {
2661
2662 // The reference element contains a zero-width space to avoid
2663 // a premature removal. The view is to be scrolled with respect
2664 // to this element.
2665 var reference = new CKEDITOR.dom.element.createFromHtml( '<span>&nbsp;</span>', this.document ),
2666 afterCaretNode, startContainerText, isStartText;
2667
2668 var range = this.clone();
2669
2670 // Work with the range to obtain a proper caret position.
2671 range.optimize();
2672
2673 // Currently in a text node, so we need to split it into two
2674 // halves and put the reference between.
2675 if ( isStartText = range.startContainer.type == CKEDITOR.NODE_TEXT ) {
2676 // Keep the original content. It will be restored.
2677 startContainerText = range.startContainer.getText();
2678
2679 // Split the startContainer at the this position.
2680 afterCaretNode = range.startContainer.split( range.startOffset );
2681
2682 // Insert the reference between two text nodes.
2683 reference.insertAfter( range.startContainer );
2684 }
2685
2686 // If not in a text node, simply insert the reference into the range.
2687 else {
2688 range.insertNode( reference );
2689 }
2690
2691 // Scroll with respect to the reference element.
2692 reference.scrollIntoView();
2693
2694 // Get rid of split parts if "in a text node" case.
2695 // Revert the original text of the startContainer.
2696 if ( isStartText ) {
2697 range.startContainer.setText( startContainerText );
2698 afterCaretNode.remove();
2699 }
2700
2701 // Get rid of the reference node. It is no longer necessary.
2702 reference.remove();
2703 },
2704
2705 /**
2706 * Setter for the {@link #startContainer}.
2707 *
2708 * @since 4.4.6
2709 * @private
2710 * @param {CKEDITOR.dom.element} startContainer
2711 */
2712 _setStartContainer: function( startContainer ) {
2713 // %REMOVE_START%
2714 var isRootAscendantOrSelf = this.root.equals( startContainer ) || this.root.contains( startContainer );
2715
2716 if ( !isRootAscendantOrSelf ) {
2717 CKEDITOR.warn( 'range-startcontainer', { startContainer: startContainer, root: this.root } );
2718 }
2719 // %REMOVE_END%
2720 this.startContainer = startContainer;
2721 },
2722
2723 /**
2724 * Setter for the {@link #endContainer}.
2725 *
2726 * @since 4.4.6
2727 * @private
2728 * @param {CKEDITOR.dom.element} endContainer
2729 */
2730 _setEndContainer: function( endContainer ) {
2731 // %REMOVE_START%
2732 var isRootAscendantOrSelf = this.root.equals( endContainer ) || this.root.contains( endContainer );
2733
2734 if ( !isRootAscendantOrSelf ) {
2735 CKEDITOR.warn( 'range-endcontainer', { endContainer: endContainer, root: this.root } );
2736 }
2737 // %REMOVE_END%
2738 this.endContainer = endContainer;
2739 }
2740 };
2741
2742
2743} )();
2744
2745/**
2746 * Indicates a position after start of a node.
2747 *
2748 * // When used according to an element:
2749 * // <element>^contents</element>
2750 *
2751 * // When used according to a text node:
2752 * // "^text" (range is anchored in the text node)
2753 *
2754 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2755 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2756 *
2757 * @readonly
2758 * @member CKEDITOR
2759 * @property {Number} [=1]
2760 */
2761CKEDITOR.POSITION_AFTER_START = 1;
2762
2763/**
2764 * Indicates a position before end of a node.
2765 *
2766 * // When used according to an element:
2767 * // <element>contents^</element>
2768 *
2769 * // When used according to a text node:
2770 * // "text^" (range is anchored in the text node)
2771 *
2772 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2773 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2774 *
2775 * @readonly
2776 * @member CKEDITOR
2777 * @property {Number} [=2]
2778 */
2779CKEDITOR.POSITION_BEFORE_END = 2;
2780
2781/**
2782 * Indicates a position before start of a node.
2783 *
2784 * // When used according to an element:
2785 * // ^<element>contents</element> (range is anchored in element's parent)
2786 *
2787 * // When used according to a text node:
2788 * // ^"text" (range is anchored in text node's parent)
2789 *
2790 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2791 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2792 *
2793 * @readonly
2794 * @member CKEDITOR
2795 * @property {Number} [=3]
2796 */
2797CKEDITOR.POSITION_BEFORE_START = 3;
2798
2799/**
2800 * Indicates a position after end of a node.
2801 *
2802 * // When used according to an element:
2803 * // <element>contents</element>^ (range is anchored in element's parent)
2804 *
2805 * // When used according to a text node:
2806 * // "text"^ (range is anchored in text node's parent)
2807 *
2808 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2809 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2810 *
2811 * @readonly
2812 * @member CKEDITOR
2813 * @property {Number} [=4]
2814 */
2815CKEDITOR.POSITION_AFTER_END = 4;
2816
2817CKEDITOR.ENLARGE_ELEMENT = 1;
2818CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
2819CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
2820CKEDITOR.ENLARGE_INLINE = 4;
2821
2822// Check boundary types.
2823
2824/**
2825 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2826 *
2827 * @readonly
2828 * @member CKEDITOR
2829 * @property {Number} [=1]
2830 */
2831CKEDITOR.START = 1;
2832
2833/**
2834 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2835 *
2836 * @readonly
2837 * @member CKEDITOR
2838 * @property {Number} [=2]
2839 */
2840CKEDITOR.END = 2;
2841
2842// Shrink range types.
2843
2844/**
2845 * See {@link CKEDITOR.dom.range#shrink}.
2846 *
2847 * @readonly
2848 * @member CKEDITOR
2849 * @property {Number} [=1]
2850 */
2851CKEDITOR.SHRINK_ELEMENT = 1;
2852
2853/**
2854 * See {@link CKEDITOR.dom.range#shrink}.
2855 *
2856 * @readonly
2857 * @member CKEDITOR
2858 * @property {Number} [=2]
2859 */
2860CKEDITOR.SHRINK_TEXT = 2;
diff --git a/sources/core/dom/rangelist.js b/sources/core/dom/rangelist.js
new file mode 100644
index 00000000..9c558b36
--- /dev/null
+++ b/sources/core/dom/rangelist.js
@@ -0,0 +1,199 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /**
8 * Represents a list os CKEDITOR.dom.range objects, which can be easily
9 * iterated sequentially.
10 *
11 * @class
12 * @extends Array
13 * @constructor Creates a rangeList class instance.
14 * @param {CKEDITOR.dom.range/CKEDITOR.dom.range[]} [ranges] The ranges contained on this list.
15 * Note that, if an array of ranges is specified, the range sequence
16 * should match its DOM order. This class will not help to sort them.
17 */
18 CKEDITOR.dom.rangeList = function( ranges ) {
19 if ( ranges instanceof CKEDITOR.dom.rangeList )
20 return ranges;
21
22 if ( !ranges )
23 ranges = [];
24 else if ( ranges instanceof CKEDITOR.dom.range )
25 ranges = [ ranges ];
26
27 return CKEDITOR.tools.extend( ranges, mixins );
28 };
29
30 var mixins = {
31 /**
32 * Creates an instance of the rangeList iterator, it should be used
33 * only when the ranges processing could be DOM intrusive, which
34 * means it may pollute and break other ranges in this list.
35 * Otherwise, it's enough to just iterate over this array in a for loop.
36 *
37 * @returns {CKEDITOR.dom.rangeListIterator}
38 */
39 createIterator: function() {
40 var rangeList = this,
41 bookmark = CKEDITOR.dom.walker.bookmark(),
42 bookmarks = [],
43 current;
44
45 return {
46 /**
47 * Retrieves the next range in the list.
48 *
49 * @member CKEDITOR.dom.rangeListIterator
50 * @param {Boolean} [mergeConsequent=false] Whether join two adjacent
51 * ranges into single, e.g. consequent table cells.
52 */
53 getNextRange: function( mergeConsequent ) {
54 current = current === undefined ? 0 : current + 1;
55
56 var range = rangeList[ current ];
57
58 // Multiple ranges might be mangled by each other.
59 if ( range && rangeList.length > 1 ) {
60 // Bookmarking all other ranges on the first iteration,
61 // the range correctness after it doesn't matter since we'll
62 // restore them before the next iteration.
63 if ( !current ) {
64 // Make sure bookmark correctness by reverse processing.
65 for ( var i = rangeList.length - 1; i >= 0; i-- )
66 bookmarks.unshift( rangeList[ i ].createBookmark( true ) );
67 }
68
69 if ( mergeConsequent ) {
70 // Figure out how many ranges should be merged.
71 var mergeCount = 0;
72 while ( rangeList[ current + mergeCount + 1 ] ) {
73 var doc = range.document,
74 found = 0,
75 left = doc.getById( bookmarks[ mergeCount ].endNode ),
76 right = doc.getById( bookmarks[ mergeCount + 1 ].startNode ),
77 next;
78
79 // Check subsequent range.
80 while ( 1 ) {
81 next = left.getNextSourceNode( false );
82 if ( !right.equals( next ) ) {
83 // This could be yet another bookmark or
84 // walking across block boundaries.
85 if ( bookmark( next ) || ( next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() ) ) {
86 left = next;
87 continue;
88 }
89 } else {
90 found = 1;
91 }
92
93 break;
94 }
95
96 if ( !found )
97 break;
98
99 mergeCount++;
100 }
101 }
102
103 range.moveToBookmark( bookmarks.shift() );
104
105 // Merge ranges finally after moving to bookmarks.
106 while ( mergeCount-- ) {
107 next = rangeList[ ++current ];
108 next.moveToBookmark( bookmarks.shift() );
109 range.setEnd( next.endContainer, next.endOffset );
110 }
111 }
112
113 return range;
114 }
115 };
116 },
117
118 /**
119 * Create bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark}.
120 *
121 * @param {Boolean} [serializable=false] See {@link CKEDITOR.dom.range#createBookmark}.
122 * @returns {Array} Array of bookmarks.
123 */
124 createBookmarks: function( serializable ) {
125 var retval = [],
126 bookmark;
127 for ( var i = 0; i < this.length; i++ ) {
128 retval.push( bookmark = this[ i ].createBookmark( serializable, true ) );
129
130 // Updating the container & offset values for ranges
131 // that have been touched.
132 for ( var j = i + 1; j < this.length; j++ ) {
133 this[ j ] = updateDirtyRange( bookmark, this[ j ] );
134 this[ j ] = updateDirtyRange( bookmark, this[ j ], true );
135 }
136 }
137 return retval;
138 },
139
140 /**
141 * Create "unobtrusive" bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark2}.
142 *
143 * @param {Boolean} [normalized=false] See {@link CKEDITOR.dom.range#createBookmark2}.
144 * @returns {Array} Array of bookmarks.
145 */
146 createBookmarks2: function( normalized ) {
147 var bookmarks = [];
148
149 for ( var i = 0; i < this.length; i++ )
150 bookmarks.push( this[ i ].createBookmark2( normalized ) );
151
152 return bookmarks;
153 },
154
155 /**
156 * Move each range in the list to the position specified by a list of bookmarks.
157 *
158 * @param {Array} bookmarks The list of bookmarks, each one matching a range in the list.
159 */
160 moveToBookmarks: function( bookmarks ) {
161 for ( var i = 0; i < this.length; i++ )
162 this[ i ].moveToBookmark( bookmarks[ i ] );
163 }
164 };
165
166 // Update the specified range which has been mangled by previous insertion of
167 // range bookmark nodes.(#3256)
168 function updateDirtyRange( bookmark, dirtyRange, checkEnd ) {
169 var serializable = bookmark.serializable,
170 container = dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ],
171 offset = checkEnd ? 'endOffset' : 'startOffset';
172
173 var bookmarkStart = serializable ? dirtyRange.document.getById( bookmark.startNode ) : bookmark.startNode;
174
175 var bookmarkEnd = serializable ? dirtyRange.document.getById( bookmark.endNode ) : bookmark.endNode;
176
177 if ( container.equals( bookmarkStart.getPrevious() ) ) {
178 dirtyRange.startOffset = dirtyRange.startOffset - container.getLength() - bookmarkEnd.getPrevious().getLength();
179 container = bookmarkEnd.getNext();
180 } else if ( container.equals( bookmarkEnd.getPrevious() ) ) {
181 dirtyRange.startOffset = dirtyRange.startOffset - container.getLength();
182 container = bookmarkEnd.getNext();
183 }
184
185 container.equals( bookmarkStart.getParent() ) && dirtyRange[ offset ]++;
186 container.equals( bookmarkEnd.getParent() ) && dirtyRange[ offset ]++;
187
188 // Update and return this range.
189 dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ] = container;
190 return dirtyRange;
191 }
192} )();
193
194/**
195 * (Virtual Class) Do not call this constructor. This class is not really part
196 * of the API. It just describes the return type of {@link CKEDITOR.dom.rangeList#createIterator}.
197 *
198 * @class CKEDITOR.dom.rangeListIterator
199 */
diff --git a/sources/core/dom/text.js b/sources/core/dom/text.js
new file mode 100644
index 00000000..7c403f90
--- /dev/null
+++ b/sources/core/dom/text.js
@@ -0,0 +1,135 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.text} class, which represents
8 * a DOM text node.
9 */
10
11/**
12 * Represents a DOM text node.
13 *
14 * var nativeNode = document.createTextNode( 'Example' );
15 * var text = new CKEDITOR.dom.text( nativeNode );
16 *
17 * var text = new CKEDITOR.dom.text( 'Example' );
18 *
19 * @class
20 * @extends CKEDITOR.dom.node
21 * @constructor Creates a text class instance.
22 * @param {Object/String} text A native DOM text node or a string containing
23 * the text to use to create a new text node.
24 * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
25 * the node in case of new node creation. Defaults to the current document.
26 */
27CKEDITOR.dom.text = function( text, ownerDocument ) {
28 if ( typeof text == 'string' )
29 text = ( ownerDocument ? ownerDocument.$ : document ).createTextNode( text );
30
31 // Theoretically, we should call the base constructor here
32 // (not CKEDITOR.dom.node though). But, IE doesn't support expando
33 // properties on text node, so the features provided by domObject will not
34 // work for text nodes (which is not a big issue for us).
35 //
36 // CKEDITOR.dom.domObject.call( this, element );
37
38 this.$ = text;
39};
40
41CKEDITOR.dom.text.prototype = new CKEDITOR.dom.node();
42
43CKEDITOR.tools.extend( CKEDITOR.dom.text.prototype, {
44 /**
45 * The node type. This is a constant value set to {@link CKEDITOR#NODE_TEXT}.
46 *
47 * @readonly
48 * @property {Number} [=CKEDITOR.NODE_TEXT]
49 */
50 type: CKEDITOR.NODE_TEXT,
51
52 /**
53 * Gets length of node's value.
54 *
55 * @returns {Number}
56 */
57 getLength: function() {
58 return this.$.nodeValue.length;
59 },
60
61 /**
62 * Gets node's value.
63 *
64 * @returns {String}
65 */
66 getText: function() {
67 return this.$.nodeValue;
68 },
69
70 /**
71 * Sets node's value.
72 *
73 * @param {String} text
74 */
75 setText: function( text ) {
76 this.$.nodeValue = text;
77 },
78
79 /**
80 * Breaks this text node into two nodes at the specified offset,
81 * keeping both in the tree as siblings. This node then only contains
82 * all the content up to the offset point. A new text node, which is
83 * inserted as the next sibling of this node, contains all the content
84 * at and after the offset point. When the offset is equal to the
85 * length of this node, the new node has no data.
86 *
87 * @param {Number} The position at which to split, starting from zero.
88 * @returns {CKEDITOR.dom.text} The new text node.
89 */
90 split: function( offset ) {
91
92 // Saved the children count and text length beforehand.
93 var parent = this.$.parentNode,
94 count = parent.childNodes.length,
95 length = this.getLength();
96
97 var doc = this.getDocument();
98 var retval = new CKEDITOR.dom.text( this.$.splitText( offset ), doc );
99
100 if ( parent.childNodes.length == count ) {
101 // If the offset is after the last char, IE creates the text node
102 // on split, but don't include it into the DOM. So, we have to do
103 // that manually here.
104 if ( offset >= length ) {
105 retval = doc.createText( '' );
106 retval.insertAfter( this );
107 } else {
108 // IE BUG: IE8+ does not update the childNodes array in DOM after splitText(),
109 // we need to make some DOM changes to make it update. (#3436)
110 var workaround = doc.createText( '' );
111 workaround.insertAfter( retval );
112 workaround.remove();
113 }
114 }
115
116 return retval;
117 },
118
119 /**
120 * Extracts characters from indexA up to but not including `indexB`.
121 *
122 * @param {Number} indexA An integer between `0` and one less than the
123 * length of the text.
124 * @param {Number} [indexB] An integer between `0` and the length of the
125 * string. If omitted, extracts characters to the end of the text.
126 */
127 substring: function( indexA, indexB ) {
128 // We need the following check due to a Firefox bug
129 // https://bugzilla.mozilla.org/show_bug.cgi?id=458886
130 if ( typeof indexB != 'number' )
131 return this.$.nodeValue.substr( indexA );
132 else
133 return this.$.nodeValue.substring( indexA, indexB );
134 }
135} );
diff --git a/sources/core/dom/walker.js b/sources/core/dom/walker.js
new file mode 100644
index 00000000..5f2c8f40
--- /dev/null
+++ b/sources/core/dom/walker.js
@@ -0,0 +1,652 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 // This function is to be called under a "walker" instance scope.
8 function iterate( rtl, breakOnFalse ) {
9 var range = this.range;
10
11 // Return null if we have reached the end.
12 if ( this._.end )
13 return null;
14
15 // This is the first call. Initialize it.
16 if ( !this._.start ) {
17 this._.start = 1;
18
19 // A collapsed range must return null at first call.
20 if ( range.collapsed ) {
21 this.end();
22 return null;
23 }
24
25 // Move outside of text node edges.
26 range.optimize();
27 }
28
29 var node,
30 startCt = range.startContainer,
31 endCt = range.endContainer,
32 startOffset = range.startOffset,
33 endOffset = range.endOffset,
34 guard,
35 userGuard = this.guard,
36 type = this.type,
37 getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' );
38
39 // Create the LTR guard function, if necessary.
40 if ( !rtl && !this._.guardLTR ) {
41 // The node that stops walker from moving up.
42 var limitLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt : endCt.getParent();
43
44 // The node that stops the walker from going to next.
45 var blockerLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt.getChild( endOffset ) : endCt.getNext();
46
47 this._.guardLTR = function( node, movingOut ) {
48 return ( ( !movingOut || !limitLTR.equals( node ) ) && ( !blockerLTR || !node.equals( blockerLTR ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
49 };
50 }
51
52 // Create the RTL guard function, if necessary.
53 if ( rtl && !this._.guardRTL ) {
54 // The node that stops walker from moving up.
55 var limitRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startCt : startCt.getParent();
56
57 // The node that stops the walker from going to next.
58 var blockerRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startOffset ? startCt.getChild( startOffset - 1 ) : null : startCt.getPrevious();
59
60 this._.guardRTL = function( node, movingOut ) {
61 return ( ( !movingOut || !limitRTL.equals( node ) ) && ( !blockerRTL || !node.equals( blockerRTL ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
62 };
63 }
64
65 // Define which guard function to use.
66 var stopGuard = rtl ? this._.guardRTL : this._.guardLTR;
67
68 // Make the user defined guard function participate in the process,
69 // otherwise simply use the boundary guard.
70 if ( userGuard ) {
71 guard = function( node, movingOut ) {
72 if ( stopGuard( node, movingOut ) === false )
73 return false;
74
75 return userGuard( node, movingOut );
76 };
77 } else {
78 guard = stopGuard;
79 }
80
81 if ( this.current )
82 node = this.current[ getSourceNodeFn ]( false, type, guard );
83 else {
84 // Get the first node to be returned.
85 if ( rtl ) {
86 node = endCt;
87
88 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
89 if ( endOffset > 0 )
90 node = node.getChild( endOffset - 1 );
91 else
92 node = ( guard( node, true ) === false ) ? null : node.getPreviousSourceNode( true, type, guard );
93 }
94 } else {
95 node = startCt;
96
97 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
98 if ( !( node = node.getChild( startOffset ) ) )
99 node = ( guard( startCt, true ) === false ) ? null : startCt.getNextSourceNode( true, type, guard );
100 }
101 }
102
103 if ( node && guard( node ) === false )
104 node = null;
105 }
106
107 while ( node && !this._.end ) {
108 this.current = node;
109
110 if ( !this.evaluator || this.evaluator( node ) !== false ) {
111 if ( !breakOnFalse )
112 return node;
113 } else if ( breakOnFalse && this.evaluator ) {
114 return false;
115 }
116
117 node = node[ getSourceNodeFn ]( false, type, guard );
118 }
119
120 this.end();
121 return this.current = null;
122 }
123
124 function iterateToLast( rtl ) {
125 var node,
126 last = null;
127
128 while ( ( node = iterate.call( this, rtl ) ) )
129 last = node;
130
131 return last;
132 }
133
134 /**
135 * Utility class to "walk" the DOM inside range boundaries. If the
136 * range starts or ends in the middle of the text node, this node will
137 * be included as a whole. Outside changes to the range may break the walker.
138 *
139 * The walker may return nodes that are not totally included in the
140 * range boundaries. Let us take the following range representation,
141 * where the square brackets indicate the boundaries:
142 *
143 * [<p>Some <b>sample] text</b>
144 *
145 * While walking forward into the above range, the following nodes are
146 * returned: `<p>`, `"Some "`, `<b>` and `"sample"`. Going
147 * backwards instead we have: `"sample"` and `"Some "`. So note that the
148 * walker always returns nodes when "entering" them, but not when
149 * "leaving" them. The {@link #guard} function is instead called both when
150 * entering and when leaving nodes.
151 *
152 * @class
153 */
154 CKEDITOR.dom.walker = CKEDITOR.tools.createClass( {
155 /**
156 * Creates a walker class instance.
157 *
158 * @constructor
159 * @param {CKEDITOR.dom.range} range The range within which to walk.
160 */
161 $: function( range ) {
162 this.range = range;
163
164 /**
165 * A function executed for every matched node to check whether
166 * it is to be considered in the walk or not. If not provided, all
167 * matched nodes are considered good.
168 *
169 * If the function returns `false`, the node is ignored.
170 *
171 * @property {Function} evaluator
172 */
173 // this.evaluator = null;
174
175 /**
176 * A function executed for every node the walk passes by to check
177 * whether the walk is to be finished. It is called both when
178 * entering and when exiting nodes, as well as for the matched nodes.
179 *
180 * If this function returns `false`, the walking ends and no more
181 * nodes are evaluated.
182
183 * @property {Function} guard
184 */
185 // this.guard = null;
186
187 /** @private */
188 this._ = {};
189 },
190
191 // statics :
192 // {
193 // /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes.
194 // * @param {CKEDITOR.dom.node} startNode The node from which the walk
195 // * will start.
196 // * @param {CKEDITOR.dom.node} [endNode] The last node to be considered
197 // * in the walk. No more nodes are retrieved after touching or
198 // * passing it. If not provided, the walker stops at the
199 // * &lt;body&gt; closing boundary.
200 // * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the
201 // * provided nodes.
202 // */
203 // createOnNodes : function( startNode, endNode, startInclusive, endInclusive )
204 // {
205 // var range = new CKEDITOR.dom.range();
206 // if ( startNode )
207 // range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ;
208 // else
209 // range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ;
210 //
211 // if ( endNode )
212 // range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ;
213 // else
214 // range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ;
215 //
216 // return new CKEDITOR.dom.walker( range );
217 // }
218 // },
219 //
220 proto: {
221 /**
222 * Stops walking. No more nodes are retrieved if this function is called.
223 */
224 end: function() {
225 this._.end = 1;
226 },
227
228 /**
229 * Retrieves the next node (on the right).
230 *
231 * @returns {CKEDITOR.dom.node} The next node or `null` if no more
232 * nodes are available.
233 */
234 next: function() {
235 return iterate.call( this );
236 },
237
238 /**
239 * Retrieves the previous node (on the left).
240 *
241 * @returns {CKEDITOR.dom.node} The previous node or `null` if no more
242 * nodes are available.
243 */
244 previous: function() {
245 return iterate.call( this, 1 );
246 },
247
248 /**
249 * Checks all nodes on the right, executing the evaluation function.
250 *
251 * @returns {Boolean} `false` if the evaluator function returned
252 * `false` for any of the matched nodes. Otherwise `true`.
253 */
254 checkForward: function() {
255 return iterate.call( this, 0, 1 ) !== false;
256 },
257
258 /**
259 * Check all nodes on the left, executing the evaluation function.
260 *
261 * @returns {Boolean} `false` if the evaluator function returned
262 * `false` for any of the matched nodes. Otherwise `true`.
263 */
264 checkBackward: function() {
265 return iterate.call( this, 1, 1 ) !== false;
266 },
267
268 /**
269 * Executes a full walk forward (to the right), until no more nodes
270 * are available, returning the last valid node.
271 *
272 * @returns {CKEDITOR.dom.node} The last node on the right or `null`
273 * if no valid nodes are available.
274 */
275 lastForward: function() {
276 return iterateToLast.call( this );
277 },
278
279 /**
280 * Executes a full walk backwards (to the left), until no more nodes
281 * are available, returning the last valid node.
282 *
283 * @returns {CKEDITOR.dom.node} The last node on the left or `null`
284 * if no valid nodes are available.
285 */
286 lastBackward: function() {
287 return iterateToLast.call( this, 1 );
288 },
289
290 /**
291 * Resets the walker.
292 */
293 reset: function() {
294 delete this.current;
295 this._ = {};
296 }
297
298 }
299 } );
300
301 // Anything whose display computed style is block, list-item, table,
302 // table-row-group, table-header-group, table-footer-group, table-row,
303 // table-column-group, table-column, table-cell, table-caption, or whose node
304 // name is hr, br (when enterMode is br only) is a block boundary.
305 var blockBoundaryDisplayMatch = {
306 block: 1, 'list-item': 1, table: 1, 'table-row-group': 1,
307 'table-header-group': 1, 'table-footer-group': 1, 'table-row': 1, 'table-column-group': 1,
308 'table-column': 1, 'table-cell': 1, 'table-caption': 1
309 },
310 outOfFlowPositions = { absolute: 1, fixed: 1 };
311
312 /**
313 * Checks whether an element is displayed as a block.
314 *
315 * @member CKEDITOR.dom.element
316 * @param [customNodeNames] Custom list of nodes which will extend
317 * the default {@link CKEDITOR.dtd#$block} list.
318 * @returns {Boolean}
319 */
320 CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) {
321 // Whether element is in normal page flow. Floated or positioned elements are out of page flow.
322 // Don't consider floated or positioned formatting as block boundary, fall back to dtd check in that case. (#6297)
323 var inPageFlow = this.getComputedStyle( 'float' ) == 'none' && !( this.getComputedStyle( 'position' ) in outOfFlowPositions );
324
325 if ( inPageFlow && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] )
326 return true;
327
328 // Either in $block or in customNodeNames if defined.
329 return !!( this.is( CKEDITOR.dtd.$block ) || customNodeNames && this.is( customNodeNames ) );
330 };
331
332 /**
333 * Returns a function which checks whether the node is a block boundary.
334 * See {@link CKEDITOR.dom.element#isBlockBoundary}.
335 *
336 * @static
337 * @param customNodeNames
338 * @returns {Function}
339 */
340 CKEDITOR.dom.walker.blockBoundary = function( customNodeNames ) {
341 return function( node ) {
342 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary( customNodeNames ) );
343 };
344 };
345
346 /**
347 * @static
348 * @todo
349 */
350 CKEDITOR.dom.walker.listItemBoundary = function() {
351 return this.blockBoundary( { br: 1 } );
352 };
353
354 /**
355 * Returns a function which checks whether the node is a bookmark node or the bookmark node
356 * inner content.
357 *
358 * @static
359 * @param {Boolean} [contentOnly=false] Whether only test against the text content of
360 * a bookmark node instead of the element itself (default).
361 * @param {Boolean} [isReject=false] Whether to return `false` for the bookmark
362 * node instead of `true` (default).
363 * @returns {Function}
364 */
365 CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject ) {
366 function isBookmarkNode( node ) {
367 return ( node && node.getName && node.getName() == 'span' && node.data( 'cke-bookmark' ) );
368 }
369
370 return function( node ) {
371 var isBookmark, parent;
372 // Is bookmark inner text node?
373 isBookmark = ( node && node.type != CKEDITOR.NODE_ELEMENT && ( parent = node.getParent() ) && isBookmarkNode( parent ) );
374 // Is bookmark node?
375 isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node );
376 return !!( isReject ^ isBookmark );
377 };
378 };
379
380 /**
381 * Returns a function which checks whether the node is a text node containing only whitespace characters.
382 *
383 * @static
384 * @param {Boolean} [isReject=false]
385 * @returns {Function}
386 */
387 CKEDITOR.dom.walker.whitespaces = function( isReject ) {
388 return function( node ) {
389 var isWhitespace;
390 if ( node && node.type == CKEDITOR.NODE_TEXT ) {
391 // whitespace, as well as the text cursor filler node we used in Webkit. (#9384)
392 isWhitespace = !CKEDITOR.tools.trim( node.getText() ) ||
393 CKEDITOR.env.webkit && node.getText() == '\u200b';
394 }
395
396 return !!( isReject ^ isWhitespace );
397 };
398 };
399
400 /**
401 * Returns a function which checks whether the node is invisible in the WYSIWYG mode.
402 *
403 * @static
404 * @param {Boolean} [isReject=false]
405 * @returns {Function}
406 */
407 CKEDITOR.dom.walker.invisible = function( isReject ) {
408 var whitespace = CKEDITOR.dom.walker.whitespaces(),
409 // #12221 (Chrome) plus #11111 (Safari).
410 offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
411
412 return function( node ) {
413 var invisible;
414
415 if ( whitespace( node ) )
416 invisible = 1;
417 else {
418 // Visibility should be checked on element.
419 if ( node.type == CKEDITOR.NODE_TEXT )
420 node = node.getParent();
421
422 // Nodes that take no spaces in wysiwyg:
423 // 1. White-spaces but not including NBSP.
424 // 2. Empty inline elements, e.g. <b></b>.
425 // 3. <br> elements (bogus, surrounded by text) (#12423).
426 invisible = node.$.offsetWidth <= offsetWidth0;
427 }
428
429 return !!( isReject ^ invisible );
430 };
431 };
432
433 /**
434 * Returns a function which checks whether the node type is equal to the passed one.
435 *
436 * @static
437 * @param {Number} type
438 * @param {Boolean} [isReject=false]
439 * @returns {Function}
440 */
441 CKEDITOR.dom.walker.nodeType = function( type, isReject ) {
442 return function( node ) {
443 return !!( isReject ^ ( node.type == type ) );
444 };
445 };
446
447 /**
448 * Returns a function which checks whether the node is a bogus (filler) node from
449 * `contenteditable` element's point of view.
450 *
451 * @static
452 * @param {Boolean} [isReject=false]
453 * @returns {Function}
454 */
455 CKEDITOR.dom.walker.bogus = function( isReject ) {
456 function nonEmpty( node ) {
457 return !isWhitespaces( node ) && !isBookmark( node );
458 }
459
460 return function( node ) {
461 var isBogus = CKEDITOR.env.needsBrFiller ? node.is && node.is( 'br' ) : node.getText && tailNbspRegex.test( node.getText() );
462
463 if ( isBogus ) {
464 var parent = node.getParent(),
465 next = node.getNext( nonEmpty );
466
467 isBogus = parent.isBlockBoundary() && ( !next || next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() );
468 }
469
470 return !!( isReject ^ isBogus );
471 };
472 };
473
474 /**
475 * Returns a function which checks whether the node is a temporary element
476 * (element with the `data-cke-temp` attribute) or its child.
477 *
478 * @since 4.3
479 * @static
480 * @param {Boolean} [isReject=false] Whether to return `false` for the
481 * temporary element instead of `true` (default).
482 * @returns {Function}
483 */
484 CKEDITOR.dom.walker.temp = function( isReject ) {
485 return function( node ) {
486 if ( node.type != CKEDITOR.NODE_ELEMENT )
487 node = node.getParent();
488
489 var isTemp = node && node.hasAttribute( 'data-cke-temp' );
490
491 return !!( isReject ^ isTemp );
492 };
493 };
494
495 var tailNbspRegex = /^[\t\r\n ]*(?:&nbsp;|\xa0)$/,
496 isWhitespaces = CKEDITOR.dom.walker.whitespaces(),
497 isBookmark = CKEDITOR.dom.walker.bookmark(),
498 isTemp = CKEDITOR.dom.walker.temp(),
499 toSkip = function( node ) {
500 return isBookmark( node ) ||
501 isWhitespaces( node ) ||
502 node.type == CKEDITOR.NODE_ELEMENT && node.is( CKEDITOR.dtd.$inline ) && !node.is( CKEDITOR.dtd.$empty );
503 };
504
505 /**
506 * Returns a function which checks whether the node should be ignored in terms of "editability".
507 *
508 * This includes:
509 *
510 * * whitespaces (see {@link CKEDITOR.dom.walker#whitespaces}),
511 * * bookmarks (see {@link CKEDITOR.dom.walker#bookmark}),
512 * * temporary elements (see {@link CKEDITOR.dom.walker#temp}).
513 *
514 * @since 4.3
515 * @static
516 * @param {Boolean} [isReject=false] Whether to return `false` for the
517 * ignored element instead of `true` (default).
518 * @returns {Function}
519 */
520 CKEDITOR.dom.walker.ignored = function( isReject ) {
521 return function( node ) {
522 var isIgnored = isWhitespaces( node ) || isBookmark( node ) || isTemp( node );
523
524 return !!( isReject ^ isIgnored );
525 };
526 };
527
528 var isIgnored = CKEDITOR.dom.walker.ignored();
529
530 /**
531 * Returns a function which checks whether the node is empty.
532 *
533 * @since 4.5
534 * @static
535 * @param {Boolean} [isReject=false] Whether to return `false` for the
536 * ignored element instead of `true` (default).
537 * @returns {Function}
538 */
539 CKEDITOR.dom.walker.empty = function( isReject ) {
540 return function( node ) {
541 var i = 0,
542 l = node.getChildCount();
543
544 for ( ; i < l; ++i ) {
545 if ( !isIgnored( node.getChild( i ) ) ) {
546 return !!isReject;
547 }
548 }
549
550 return !isReject;
551 };
552 };
553
554 var isEmpty = CKEDITOR.dom.walker.empty();
555
556 function filterTextContainers( dtd ) {
557 var hash = {},
558 name;
559
560 for ( name in dtd ) {
561 if ( CKEDITOR.dtd[ name ][ '#' ] )
562 hash[ name ] = 1;
563 }
564 return hash;
565 }
566
567 /**
568 * A hash of element names which in browsers that {@link CKEDITOR.env#needsBrFiller do not need `<br>` fillers}
569 * can be selection containers despite being empty.
570 *
571 * @since 4.5
572 * @static
573 * @property {Object} validEmptyBlockContainers
574 */
575 var validEmptyBlocks = CKEDITOR.dom.walker.validEmptyBlockContainers = CKEDITOR.tools.extend(
576 filterTextContainers( CKEDITOR.dtd.$block ),
577 { caption: 1, td: 1, th: 1 }
578 );
579
580 function isEditable( node ) {
581 // Skip temporary elements, bookmarks and whitespaces.
582 if ( isIgnored( node ) )
583 return false;
584
585 if ( node.type == CKEDITOR.NODE_TEXT )
586 return true;
587
588 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
589 // All inline and non-editable elements are valid editable places.
590 // Note: the <hr> is currently the only element in CKEDITOR.dtd.$empty and CKEDITOR.dtd.$block,
591 // but generally speaking we need an intersection of these two sets.
592 // Note: non-editable block has to be treated differently (should be selected entirely).
593 if ( node.is( CKEDITOR.dtd.$inline ) || node.is( 'hr' ) || node.getAttribute( 'contenteditable' ) == 'false' )
594 return true;
595
596 // Empty blocks are editable on IE.
597 if ( !CKEDITOR.env.needsBrFiller && node.is( validEmptyBlocks ) && isEmpty( node ) )
598 return true;
599 }
600
601 // Skip all other nodes.
602 return false;
603 }
604
605 /**
606 * Returns a function which checks whether the node can be a container or a sibling
607 * of the selection end.
608 *
609 * This includes:
610 *
611 * * text nodes (but not whitespaces),
612 * * inline elements,
613 * * intersection of {@link CKEDITOR.dtd#$empty} and {@link CKEDITOR.dtd#$block} (currently
614 * it is only `<hr>`),
615 * * non-editable blocks (special case &mdash; such blocks cannot be containers nor
616 * siblings, they need to be selected entirely),
617 * * empty {@link #validEmptyBlockContainers blocks} which can contain text
618 * ({@link CKEDITOR.env#needsBrFiller old IEs only}).
619 *
620 * @since 4.3
621 * @static
622 * @param {Boolean} [isReject=false] Whether to return `false` for the
623 * ignored element instead of `true` (default).
624 * @returns {Function}
625 */
626 CKEDITOR.dom.walker.editable = function( isReject ) {
627 return function( node ) {
628 return !!( isReject ^ isEditable( node ) );
629 };
630 };
631
632 /**
633 * Checks if there is a filler node at the end of an element, and returns it.
634 *
635 * @member CKEDITOR.dom.element
636 * @returns {CKEDITOR.dom.node/Boolean} Bogus node or `false`.
637 */
638 CKEDITOR.dom.element.prototype.getBogus = function() {
639 // Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (#7070).
640 var tail = this;
641 do {
642 tail = tail.getPreviousSourceNode();
643 }
644 while ( toSkip( tail ) );
645
646 if ( tail && ( CKEDITOR.env.needsBrFiller ? tail.is && tail.is( 'br' ) : tail.getText && tailNbspRegex.test( tail.getText() ) ) )
647 return tail;
648
649 return false;
650 };
651
652} )();
diff --git a/sources/core/dom/window.js b/sources/core/dom/window.js
new file mode 100644
index 00000000..123c981a
--- /dev/null
+++ b/sources/core/dom/window.js
@@ -0,0 +1,95 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.document} class, which
8 * represents a DOM document.
9 */
10
11/**
12 * Represents a DOM window.
13 *
14 * var document = new CKEDITOR.dom.window( window );
15 *
16 * @class
17 * @extends CKEDITOR.dom.domObject
18 * @constructor Creates a window class instance.
19 * @param {Object} domWindow A native DOM window.
20 */
21CKEDITOR.dom.window = function( domWindow ) {
22 CKEDITOR.dom.domObject.call( this, domWindow );
23};
24
25CKEDITOR.dom.window.prototype = new CKEDITOR.dom.domObject();
26
27CKEDITOR.tools.extend( CKEDITOR.dom.window.prototype, {
28 /**
29 * Moves the selection focus to this window.
30 *
31 * var win = new CKEDITOR.dom.window( window );
32 * win.focus();
33 */
34 focus: function() {
35 this.$.focus();
36 },
37
38 /**
39 * Gets the width and height of this window's viewable area.
40 *
41 * var win = new CKEDITOR.dom.window( window );
42 * var size = win.getViewPaneSize();
43 * alert( size.width );
44 * alert( size.height );
45 *
46 * @returns {Object} An object with the `width` and `height`
47 * properties containing the size.
48 */
49 getViewPaneSize: function() {
50 var doc = this.$.document,
51 stdMode = doc.compatMode == 'CSS1Compat';
52 return {
53 width: ( stdMode ? doc.documentElement.clientWidth : doc.body.clientWidth ) || 0,
54 height: ( stdMode ? doc.documentElement.clientHeight : doc.body.clientHeight ) || 0
55 };
56 },
57
58 /**
59 * Gets the current position of the window's scroll.
60 *
61 * var win = new CKEDITOR.dom.window( window );
62 * var pos = win.getScrollPosition();
63 * alert( pos.x );
64 * alert( pos.y );
65 *
66 * @returns {Object} An object with the `x` and `y` properties
67 * containing the scroll position.
68 */
69 getScrollPosition: function() {
70 var $ = this.$;
71
72 if ( 'pageXOffset' in $ ) {
73 return {
74 x: $.pageXOffset || 0,
75 y: $.pageYOffset || 0
76 };
77 } else {
78 var doc = $.document;
79 return {
80 x: doc.documentElement.scrollLeft || doc.body.scrollLeft || 0,
81 y: doc.documentElement.scrollTop || doc.body.scrollTop || 0
82 };
83 }
84 },
85
86 /**
87 * Gets the frame element containing this window context.
88 *
89 * @returns {CKEDITOR.dom.element} The frame element or `null` if not in a frame context.
90 */
91 getFrame: function() {
92 var iframe = this.$.frameElement;
93 return iframe ? new CKEDITOR.dom.element.get( iframe ) : null;
94 }
95} );
diff --git a/sources/core/dtd.js b/sources/core/dtd.js
new file mode 100644
index 00000000..9ef4fa7c
--- /dev/null
+++ b/sources/core/dtd.js
@@ -0,0 +1,349 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 */
35CKEDITOR.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 00000000..4c941cd3
--- /dev/null
+++ b/sources/core/editable.js
@@ -0,0 +1,3158 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /**
8 * 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** &ndash; the initial state. The editable's instance was created but
36 * is not fully loaded (in particular it has no data).
37 * * **ready** &ndash; the editable is fully initialized. The `ready` status is set after
38 * the first {@link CKEDITOR.editor#method-setData} is called.
39 * * **detached** &ndash; 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[^>]*>|&nbsp;|\u00A0|&#160;)?\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( /^(?:&nbsp;|\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>&nbsp;</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">&nbsp;</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|&nbsp;)$/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 00000000..f29be63f
--- /dev/null
+++ b/sources/core/editor.js
@@ -0,0 +1,1963 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.editor} class 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 &mdash; the editor instance was initialized,
110 * but its components (configuration, plugins, language files) are not loaded yet.
111 * * **loaded**: The editor components were loaded &mdash; see the {@link CKEDITOR.editor#loaded} event.
112 * * **ready**: The editor is fully initialized and ready &mdash; see the {@link CKEDITOR.editor#instanceReady} event.
113 * * **destroyed**: The editor was destroyed &mdash; 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 CKEDITOR.tools.extend( CKEDITOR.editor.prototype, {
658 /**
659 * Adds a command definition to the editor instance. Commands added with
660 * this function can be executed later with the <code>{@link #execCommand}</code> method.
661 *
662 * editorInstance.addCommand( 'sample', {
663 * exec: function( editor ) {
664 * alert( 'Executing a command for the editor name "' + editor.name + '"!' );
665 * }
666 * } );
667 *
668 * @param {String} commandName The indentifier name of the command.
669 * @param {CKEDITOR.commandDefinition} commandDefinition The command definition.
670 */
671 addCommand: function( commandName, commandDefinition ) {
672 commandDefinition.name = commandName.toLowerCase();
673 var cmd = new CKEDITOR.command( this, commandDefinition );
674
675 // Update command when mode is set.
676 // This guarantees that commands added before first editor#mode
677 // aren't immediately updated, but waits for editor#mode and that
678 // commands added later are immediately refreshed, even when added
679 // before instanceReady. #10103, #10249
680 if ( this.mode )
681 updateCommand( this, cmd );
682
683 return this.commands[ commandName ] = cmd;
684 },
685
686 /**
687 * Attaches the editor to a form to call {@link #updateElement} before form submission.
688 * This method is called by both creators ({@link CKEDITOR#replace replace} and
689 * {@link CKEDITOR#inline inline}), so there is no reason to call it manually.
690 *
691 * @private
692 */
693 _attachToForm: function() {
694 var editor = this,
695 element = editor.element,
696 form = new CKEDITOR.dom.element( element.$.form );
697
698 // If are replacing a textarea, we must
699 if ( element.is( 'textarea' ) ) {
700 if ( form ) {
701 form.on( 'submit', onSubmit );
702
703 // Check if there is no element/elements input with name == "submit".
704 // If they exists they will overwrite form submit function (form.$.submit).
705 // If form.$.submit is overwritten we can not do anything with it.
706 if ( isFunction( form.$.submit ) ) {
707 // Setup the submit function because it doesn't fire the
708 // "submit" event.
709 form.$.submit = CKEDITOR.tools.override( form.$.submit, function( originalSubmit ) {
710 return function() {
711 onSubmit();
712
713 // For IE, the DOM submit function is not a
714 // function, so we need third check.
715 if ( originalSubmit.apply )
716 originalSubmit.apply( this );
717 else
718 originalSubmit();
719 };
720 } );
721 }
722
723 // Remove 'submit' events registered on form element before destroying.(#3988)
724 editor.on( 'destroy', function() {
725 form.removeListener( 'submit', onSubmit );
726 } );
727 }
728 }
729
730 function onSubmit( evt ) {
731 editor.updateElement();
732
733 // #8031 If textarea had required attribute and editor is empty fire 'required' event and if
734 // it was cancelled, prevent submitting the form.
735 if ( editor._.required && !element.getValue() && editor.fire( 'required' ) === false ) {
736 // When user press save button event (evt) is undefined (see save plugin).
737 // This method works because it throws error so originalSubmit won't be called.
738 // Also this error won't be shown because it will be caught in save plugin.
739 evt.data.preventDefault();
740 }
741 }
742
743 function isFunction( f ) {
744 // For IE8 typeof fun == object so we cannot use it.
745 return !!( f && f.call && f.apply );
746 }
747 },
748
749 /**
750 * Destroys the editor instance, releasing all resources used by it.
751 * If the editor replaced an element, the element will be recovered.
752 *
753 * alert( CKEDITOR.instances.editor1 ); // e.g. object
754 * CKEDITOR.instances.editor1.destroy();
755 * alert( CKEDITOR.instances.editor1 ); // undefined
756 *
757 * @param {Boolean} [noUpdate] If the instance is replacing a DOM
758 * element, this parameter indicates whether or not to update the
759 * element with the instance content.
760 */
761 destroy: function( noUpdate ) {
762 this.fire( 'beforeDestroy' );
763
764 !noUpdate && updateEditorElement.call( this );
765
766 this.editable( null );
767
768 if ( this.filter ) {
769 this.filter.destroy();
770 delete this.filter;
771 }
772
773 delete this.activeFilter;
774
775 this.status = 'destroyed';
776
777 this.fire( 'destroy' );
778
779 // Plug off all listeners to prevent any further events firing.
780 this.removeAllListeners();
781
782 CKEDITOR.remove( this );
783 CKEDITOR.fire( 'instanceDestroyed', null, this );
784 },
785
786 /**
787 * Returns an {@link CKEDITOR.dom.elementPath element path} for the selection in the editor.
788 *
789 * @param {CKEDITOR.dom.node} [startNode] From which the path should start,
790 * if not specified defaults to editor selection's
791 * start element yielded by {@link CKEDITOR.dom.selection#getStartElement}.
792 * @returns {CKEDITOR.dom.elementPath}
793 */
794 elementPath: function( startNode ) {
795 if ( !startNode ) {
796 var sel = this.getSelection();
797 if ( !sel )
798 return null;
799
800 startNode = sel.getStartElement();
801 }
802
803 return startNode ? new CKEDITOR.dom.elementPath( startNode, this.editable() ) : null;
804 },
805
806 /**
807 * Shortcut to create a {@link CKEDITOR.dom.range} instance from the editable element.
808 *
809 * @returns {CKEDITOR.dom.range} The DOM range created if the editable has presented.
810 * @see CKEDITOR.dom.range
811 */
812 createRange: function() {
813 var editable = this.editable();
814 return editable ? new CKEDITOR.dom.range( editable ) : null;
815 },
816
817 /**
818 * Executes a command associated with the editor.
819 *
820 * editorInstance.execCommand( 'bold' );
821 *
822 * @param {String} commandName The indentifier name of the command.
823 * @param {Object} [data] The data to be passed to the command.
824 * @returns {Boolean} `true` if the command was executed
825 * successfully, otherwise `false`.
826 * @see CKEDITOR.editor#addCommand
827 */
828 execCommand: function( commandName, data ) {
829 var command = this.getCommand( commandName );
830
831 var eventData = {
832 name: commandName,
833 commandData: data,
834 command: command
835 };
836
837 if ( command && command.state != CKEDITOR.TRISTATE_DISABLED ) {
838 if ( this.fire( 'beforeCommandExec', eventData ) !== false ) {
839 eventData.returnValue = command.exec( eventData.commandData );
840
841 // Fire the 'afterCommandExec' immediately if command is synchronous.
842 if ( !command.async && this.fire( 'afterCommandExec', eventData ) !== false )
843 return eventData.returnValue;
844 }
845 }
846
847 // throw 'Unknown command name "' + commandName + '"';
848 return false;
849 },
850
851 /**
852 * Gets one of the registered commands. Note that after registering a
853 * command definition with {@link #addCommand}, it is
854 * transformed internally into an instance of
855 * {@link CKEDITOR.command}, which will then be returned by this function.
856 *
857 * @param {String} commandName The name of the command to be returned.
858 * This is the same name that is used to register the command with `addCommand`.
859 * @returns {CKEDITOR.command} The command object identified by the provided name.
860 */
861 getCommand: function( commandName ) {
862 return this.commands[ commandName ];
863 },
864
865 /**
866 * Gets the editor data. The data will be in "raw" format. It is the same
867 * data that is posted by the editor.
868 *
869 * if ( CKEDITOR.instances.editor1.getData() == '' )
870 * alert( 'There is no data available.' );
871 *
872 * @param {Boolean} internal If set to `true`, it will prevent firing the
873 * {@link CKEDITOR.editor#beforeGetData} and {@link CKEDITOR.editor#event-getData} events, so
874 * the real content of the editor will not be read and cached data will be returned. The method will work
875 * much faster, but this may result in the editor returning the data that is not up to date. This parameter
876 * should thus only be set to `true` when you are certain that the cached data is up to date.
877 *
878 * @returns {String} The editor data.
879 */
880 getData: function( internal ) {
881 !internal && this.fire( 'beforeGetData' );
882
883 var eventData = this._.data;
884
885 if ( typeof eventData != 'string' ) {
886 var element = this.element;
887 if ( element && this.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE )
888 eventData = element.is( 'textarea' ) ? element.getValue() : element.getHtml();
889 else
890 eventData = '';
891 }
892
893 eventData = { dataValue: eventData };
894
895 // Fire "getData" so data manipulation may happen.
896 !internal && this.fire( 'getData', eventData );
897
898 return eventData.dataValue;
899 },
900
901 /**
902 * Gets the "raw data" currently available in the editor. This is a
903 * fast method which returns the data as is, without processing, so it is
904 * not recommended to use it on resulting pages. Instead it can be used
905 * combined with the {@link #method-loadSnapshot} method in order
906 * to automatically save the editor data from time to time
907 * while the user is using the editor, to avoid data loss, without risking
908 * performance issues.
909 *
910 * alert( editor.getSnapshot() );
911 *
912 * See also:
913 *
914 * * {@link CKEDITOR.editor#method-getData}.
915 *
916 * @returns {String} Editor "raw data".
917 */
918 getSnapshot: function() {
919 var data = this.fire( 'getSnapshot' );
920
921 if ( typeof data != 'string' ) {
922 var element = this.element;
923
924 if ( element && this.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) {
925 data = element.is( 'textarea' ) ? element.getValue() : element.getHtml();
926 }
927 else {
928 // If we don't have a proper element, set data to an empty string,
929 // as this method is expected to return a string. (#13385)
930 data = '';
931 }
932 }
933
934 return data;
935 },
936
937 /**
938 * Loads "raw data" into the editor. The data is loaded with processing
939 * straight to the editing area. It should not be used as a way to load
940 * any kind of data, but instead in combination with
941 * {@link #method-getSnapshot}-produced data.
942 *
943 * var data = editor.getSnapshot();
944 * editor.loadSnapshot( data );
945 *
946 * @see CKEDITOR.editor#setData
947 */
948 loadSnapshot: function( snapshot ) {
949 this.fire( 'loadSnapshot', snapshot );
950 },
951
952 /**
953 * Sets the editor data. The data must be provided in the "raw" format (HTML).
954 *
955 * Note that this method is asynchronous. The `callback` parameter must
956 * be used if interaction with the editor is needed after setting the data.
957 *
958 * CKEDITOR.instances.editor1.setData( '<p>This is the editor data.</p>' );
959 *
960 * CKEDITOR.instances.editor1.setData( '<p>Some other editor data.</p>', {
961 * callback: function() {
962 * this.checkDirty(); // true
963 * }
964 * } );
965 *
966 * Note: In **CKEditor 4.4.2** the signature of this method has changed. All arguments
967 * except `data` were wrapped into the `options` object. However, backward compatibility
968 * was preserved and it is still possible to use the `data, callback, internal` arguments.
969 *
970 *
971 * @param {String} data The HTML code to replace current editor content.
972 * @param {Object} [options]
973 * @param {Boolean} [options.internal=false] Whether to suppress any event firing when copying data internally inside the editor.
974 * @param {Function} [options.callback] Function to be called after `setData` is completed (on {@link #dataReady}).
975 * @param {Boolean} [options.noSnapshot=false] If set to `true`, it will prevent recording an undo snapshot.
976 * Introduced in CKEditor 4.4.2.
977 */
978 setData: function( data, options, internal ) {
979 var fireSnapshot = true,
980 // Backward compatibility.
981 callback = options,
982 eventData;
983
984 if ( options && typeof options == 'object' ) {
985 internal = options.internal;
986 callback = options.callback;
987 fireSnapshot = !options.noSnapshot;
988 }
989
990 if ( !internal && fireSnapshot )
991 this.fire( 'saveSnapshot' );
992
993 if ( callback || !internal ) {
994 this.once( 'dataReady', function( evt ) {
995 if ( !internal && fireSnapshot )
996 this.fire( 'saveSnapshot' );
997
998 if ( callback )
999 callback.call( evt.editor );
1000 } );
1001 }
1002
1003 // Fire "setData" so data manipulation may happen.
1004 eventData = { dataValue: data };
1005 !internal && this.fire( 'setData', eventData );
1006
1007 this._.data = eventData.dataValue;
1008
1009 !internal && this.fire( 'afterSetData', eventData );
1010 },
1011
1012 /**
1013 * Puts or restores the editor into the read-only state. When in read-only,
1014 * the user is not able to change the editor content, but can still use
1015 * some editor features. This function sets the {@link #property-readOnly}
1016 * property of the editor, firing the {@link #event-readOnly} event.
1017 *
1018 * **Note:** The current editing area will be reloaded.
1019 *
1020 * @since 3.6
1021 * @param {Boolean} [isReadOnly] Indicates that the editor must go
1022 * read-only (`true`, default) or be restored and made editable (`false`).
1023 */
1024 setReadOnly: function( isReadOnly ) {
1025 isReadOnly = ( isReadOnly == null ) || isReadOnly;
1026
1027 if ( this.readOnly != isReadOnly ) {
1028 this.readOnly = isReadOnly;
1029
1030 // Block or release BACKSPACE key according to current read-only
1031 // state to prevent browser's history navigation (#9761).
1032 this.keystrokeHandler.blockedKeystrokes[ 8 ] = +isReadOnly;
1033
1034 this.editable().setReadOnly( isReadOnly );
1035
1036 // Fire the readOnly event so the editor features can update
1037 // their state accordingly.
1038 this.fire( 'readOnly' );
1039 }
1040 },
1041
1042 /**
1043 * Inserts HTML code into the currently selected position in the editor in WYSIWYG mode.
1044 *
1045 * Example:
1046 *
1047 * CKEDITOR.instances.editor1.insertHtml( '<p>This is a new paragraph.</p>' );
1048 *
1049 * Fires the {@link #event-insertHtml} and {@link #event-afterInsertHtml} events. The HTML is inserted
1050 * in the {@link #event-insertHtml} event's listener with a default priority (10) so you can add listeners with
1051 * lower or higher priorities in order to execute some code before or after the HTML is inserted.
1052 *
1053 * @param {String} html HTML code to be inserted into the editor.
1054 * @param {String} [mode='html'] The mode in which the HTML code will be inserted. One of the following:
1055 *
1056 * * `'html'` &ndash; The inserted content will completely override the styles at the selected position.
1057 * * `'unfiltered_html'` &ndash; Like `'html'` but the content is not filtered with {@link CKEDITOR.filter}.
1058 * * `'text'` &ndash; The inserted content will inherit the styles applied in
1059 * the selected position. This mode should be used when inserting "htmlified" plain text
1060 * (HTML without inline styles and styling elements like `<b>`, `<strong>`, `<span style="...">`).
1061 *
1062 * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range
1063 * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case).
1064 * Introduced in CKEditor 4.5.
1065 */
1066 insertHtml: function( html, mode, range ) {
1067 this.fire( 'insertHtml', { dataValue: html, mode: mode, range: range } );
1068 },
1069
1070 /**
1071 * Inserts text content into the currently selected position in the
1072 * editor in WYSIWYG mode. The styles of the selected element will be applied to the inserted text.
1073 * Spaces around the text will be left untouched.
1074 *
1075 * CKEDITOR.instances.editor1.insertText( ' line1 \n\n line2' );
1076 *
1077 * Fires the {@link #event-insertText} and {@link #event-afterInsertHtml} events. The text is inserted
1078 * in the {@link #event-insertText} event's listener with a default priority (10) so you can add listeners with
1079 * lower or higher priorities in order to execute some code before or after the text is inserted.
1080 *
1081 * @since 3.5
1082 * @param {String} text Text to be inserted into the editor.
1083 */
1084 insertText: function( text ) {
1085 this.fire( 'insertText', text );
1086 },
1087
1088 /**
1089 * Inserts an element into the currently selected position in the editor in WYSIWYG mode.
1090 *
1091 * var element = CKEDITOR.dom.element.createFromHtml( '<img src="hello.png" border="0" title="Hello" />' );
1092 * CKEDITOR.instances.editor1.insertElement( element );
1093 *
1094 * Fires the {@link #event-insertElement} event. The element is inserted in the listener with a default priority (10),
1095 * so you can add listeners with lower or higher priorities in order to execute some code before or after
1096 * the element is inserted.
1097 *
1098 * @param {CKEDITOR.dom.element} element The element to be inserted into the editor.
1099 */
1100 insertElement: function( element ) {
1101 this.fire( 'insertElement', element );
1102 },
1103
1104 /**
1105 * Gets the selected HTML (it is returned as a {@link CKEDITOR.dom.documentFragment document fragment}
1106 * or a string). This method is designed to work as the user would expect the copy functionality to work.
1107 * For instance, if the following selection was made:
1108 *
1109 * <p>a<b>b{c}d</b>e</p>
1110 *
1111 * The following HTML will be returned:
1112 *
1113 * <b>c</b>
1114 *
1115 * As you can see, the information about the bold formatting was preserved, even though the selection was
1116 * anchored inside the `<b>` element.
1117 *
1118 * See also:
1119 *
1120 * * the {@link #extractSelectedHtml} method,
1121 * * the {@link CKEDITOR.editable#getHtmlFromRange} method.
1122 *
1123 * @since 4.5
1124 * @param {Boolean} [toString] If `true`, then stringified HTML will be returned.
1125 * @returns {CKEDITOR.dom.documentFragment/String}
1126 */
1127 getSelectedHtml: function( toString ) {
1128 var editable = this.editable(),
1129 selection = this.getSelection(),
1130 ranges = selection && selection.getRanges();
1131
1132 if ( !editable || !ranges || ranges.length === 0 ) {
1133 return null;
1134 }
1135
1136 var docFragment = editable.getHtmlFromRange( ranges[ 0 ] );
1137
1138 return toString ? docFragment.getHtml() : docFragment;
1139 },
1140
1141 /**
1142 * Gets the selected HTML (it is returned as a {@link CKEDITOR.dom.documentFragment document fragment}
1143 * or a string) and removes the selected part of the DOM. This method is designed to work as the user would
1144 * expect the cut and delete functionalities to work.
1145 *
1146 * See also:
1147 *
1148 * * the {@link #getSelectedHtml} method,
1149 * * the {@link CKEDITOR.editable#extractHtmlFromRange} method.
1150 *
1151 * @since 4.5
1152 * @param {Boolean} [toString] If `true`, then stringified HTML will be returned.
1153 * @param {Boolean} [removeEmptyBlock=false] Default `false` means that the function will keep an empty block (if the
1154 * entire content was removed) or it will create it (if a block element was removed) and set the selection in that block.
1155 * If `true`, the empty block will be removed or not created. In this case the function will not handle the selection.
1156 * @returns {CKEDITOR.dom.documentFragment/String/null}
1157 */
1158 extractSelectedHtml: function( toString, removeEmptyBlock ) {
1159 var editable = this.editable(),
1160 ranges = this.getSelection().getRanges();
1161
1162 if ( !editable || ranges.length === 0 ) {
1163 return null;
1164 }
1165
1166 var range = ranges[ 0 ],
1167 docFragment = editable.extractHtmlFromRange( range, removeEmptyBlock );
1168
1169 if ( !removeEmptyBlock ) {
1170 this.getSelection().selectRanges( [ range ] );
1171 }
1172
1173 return toString ? docFragment.getHtml() : docFragment;
1174 },
1175
1176 /**
1177 * Moves the selection focus to the editing area space in the editor.
1178 */
1179 focus: function() {
1180 this.fire( 'beforeFocus' );
1181 },
1182
1183 /**
1184 * Checks whether the current editor content contains changes when
1185 * compared to the content loaded into the editor at startup, or to
1186 * the content available in the editor when {@link #resetDirty}
1187 * was called.
1188 *
1189 * function beforeUnload( evt ) {
1190 * if ( CKEDITOR.instances.editor1.checkDirty() )
1191 * return evt.returnValue = "You will lose the changes made in the editor.";
1192 * }
1193 *
1194 * if ( window.addEventListener )
1195 * window.addEventListener( 'beforeunload', beforeUnload, false );
1196 * else
1197 * window.attachEvent( 'onbeforeunload', beforeUnload );
1198 *
1199 * @returns {Boolean} `true` if the content contains changes.
1200 */
1201 checkDirty: function() {
1202 return this.status == 'ready' && this._.previousValue !== this.getSnapshot();
1203 },
1204
1205 /**
1206 * Resets the "dirty state" of the editor so subsequent calls to
1207 * {@link #checkDirty} will return `false` if the user will not
1208 * have made further changes to the content.
1209 *
1210 * alert( editor.checkDirty() ); // e.g. true
1211 * editor.resetDirty();
1212 * alert( editor.checkDirty() ); // false
1213 */
1214 resetDirty: function() {
1215 this._.previousValue = this.getSnapshot();
1216 },
1217
1218 /**
1219 * Updates the `<textarea>` element that was replaced by the editor with
1220 * the current data available in the editor.
1221 *
1222 * **Note:** This method will only affect those editor instances created
1223 * with the {@link CKEDITOR#ELEMENT_MODE_REPLACE} element mode or inline instances
1224 * bound to `<textarea>` elements.
1225 *
1226 * CKEDITOR.instances.editor1.updateElement();
1227 * alert( document.getElementById( 'editor1' ).value ); // The current editor data.
1228 *
1229 * @see CKEDITOR.editor#element
1230 */
1231 updateElement: function() {
1232 return updateEditorElement.call( this );
1233 },
1234
1235 /**
1236 * Assigns keystrokes associated with editor commands.
1237 *
1238 * editor.setKeystroke( CKEDITOR.CTRL + 115, 'save' ); // Assigned Ctrl+S to the "save" command.
1239 * editor.setKeystroke( CKEDITOR.CTRL + 115, false ); // Disabled Ctrl+S keystroke assignment.
1240 * editor.setKeystroke( [
1241 * [ CKEDITOR.ALT + 122, false ],
1242 * [ CKEDITOR.CTRL + 121, 'link' ],
1243 * [ CKEDITOR.SHIFT + 120, 'bold' ]
1244 * ] );
1245 *
1246 * This method may be used in the following cases:
1247 *
1248 * * By plugins (like `link` or `basicstyles`) to set their keystrokes when plugins are being loaded.
1249 * * During the runtime to modify existing keystrokes.
1250 *
1251 * The editor handles keystroke configuration in the following order:
1252 *
1253 * 1. Plugins use this method to define default keystrokes.
1254 * 2. Editor extends default keystrokes with {@link CKEDITOR.config#keystrokes}.
1255 * 3. Editor blocks keystrokes defined in {@link CKEDITOR.config#blockedKeystrokes}.
1256 *
1257 * You can then set new keystrokes using this method during the runtime.
1258 *
1259 * @since 4.0
1260 * @param {Number/Array} keystroke A keystroke or an array of keystroke definitions.
1261 * @param {String/Boolean} [behavior] A command to be executed on the keystroke.
1262 */
1263 setKeystroke: function() {
1264 var keystrokes = this.keystrokeHandler.keystrokes,
1265 newKeystrokes = CKEDITOR.tools.isArray( arguments[ 0 ] ) ? arguments[ 0 ] : [ [].slice.call( arguments, 0 ) ],
1266 keystroke, behavior;
1267
1268 for ( var i = newKeystrokes.length; i--; ) {
1269 keystroke = newKeystrokes[ i ];
1270 behavior = 0;
1271
1272 // It may be a pair of: [ key, command ]
1273 if ( CKEDITOR.tools.isArray( keystroke ) ) {
1274 behavior = keystroke[ 1 ];
1275 keystroke = keystroke[ 0 ];
1276 }
1277
1278 if ( behavior )
1279 keystrokes[ keystroke ] = behavior;
1280 else
1281 delete keystrokes[ keystroke ];
1282 }
1283 },
1284
1285 /**
1286 * Shorthand for {@link CKEDITOR.filter#addFeature}.
1287 *
1288 * @since 4.1
1289 * @param {CKEDITOR.feature} feature See {@link CKEDITOR.filter#addFeature}.
1290 * @returns {Boolean} See {@link CKEDITOR.filter#addFeature}.
1291 */
1292 addFeature: function( feature ) {
1293 return this.filter.addFeature( feature );
1294 },
1295
1296 /**
1297 * Sets the active filter ({@link #activeFilter}). Fires the {@link #activeFilterChange} event.
1298 *
1299 * // Set active filter which allows only 4 elements.
1300 * // Buttons like Bold, Italic will be disabled.
1301 * var filter = new CKEDITOR.filter( 'p strong em br' );
1302 * editor.setActiveFilter( filter );
1303 *
1304 * Setting a new filter will also change the {@link #setActiveEnterMode active Enter modes} to the first values
1305 * allowed by the new filter (see {@link CKEDITOR.filter#getAllowedEnterMode}).
1306 *
1307 * @since 4.3
1308 * @param {CKEDITOR.filter} filter Filter instance or a falsy value (e.g. `null`) to reset to the default one.
1309 */
1310 setActiveFilter: function( filter ) {
1311 if ( !filter )
1312 filter = this.filter;
1313
1314 if ( this.activeFilter !== filter ) {
1315 this.activeFilter = filter;
1316 this.fire( 'activeFilterChange' );
1317
1318 // Reset active filter to the main one - it resets enter modes, too.
1319 if ( filter === this.filter )
1320 this.setActiveEnterMode( null, null );
1321 else
1322 this.setActiveEnterMode(
1323 filter.getAllowedEnterMode( this.enterMode ),
1324 filter.getAllowedEnterMode( this.shiftEnterMode, true )
1325 );
1326 }
1327 },
1328
1329 /**
1330 * Sets the active Enter modes: ({@link #enterMode} and {@link #shiftEnterMode}).
1331 * Fires the {@link #activeEnterModeChange} event.
1332 *
1333 * Prior to CKEditor 4.3 Enter modes were static and it was enough to check {@link CKEDITOR.config#enterMode}
1334 * and {@link CKEDITOR.config#shiftEnterMode} when implementing a feature which should depend on the Enter modes.
1335 * Since CKEditor 4.3 these options are source of initial:
1336 *
1337 * * static {@link #enterMode} and {@link #shiftEnterMode} values,
1338 * * dynamic {@link #activeEnterMode} and {@link #activeShiftEnterMode} values.
1339 *
1340 * However, the dynamic Enter modes can be changed during runtime by using this method, to reflect the selection context.
1341 * For example, if selection is moved to the {@link CKEDITOR.plugins.widget widget}'s nested editable which
1342 * is a {@link #blockless blockless one}, then the active Enter modes should be changed to {@link CKEDITOR#ENTER_BR}
1343 * (in this case [Widget System](#!/guide/dev_widgets) takes care of that).
1344 *
1345 * **Note:** This method should not be used to configure the editor &ndash; use {@link CKEDITOR.config#enterMode} and
1346 * {@link CKEDITOR.config#shiftEnterMode} instead. This method should only be used to dynamically change
1347 * Enter modes during runtime based on selection changes.
1348 * Keep in mind that changed Enter mode may be overwritten by another plugin/feature when it decided that
1349 * the changed context requires this.
1350 *
1351 * **Note:** In case of blockless editor (inline editor based on an element which cannot contain block elements
1352 * &mdash; see {@link CKEDITOR.editor#blockless}) only {@link CKEDITOR#ENTER_BR} is a valid Enter mode. Therefore
1353 * this method will not allow to set other values.
1354 *
1355 * **Note:** Changing the {@link #activeFilter active filter} may cause the Enter mode to change if default Enter modes
1356 * are not allowed by the new filter.
1357 *
1358 * @since 4.3
1359 * @param {Number} enterMode One of {@link CKEDITOR#ENTER_P}, {@link CKEDITOR#ENTER_DIV}, {@link CKEDITOR#ENTER_BR}.
1360 * Pass falsy value (e.g. `null`) to reset the Enter mode to the default value ({@link #enterMode} and/or {@link #shiftEnterMode}).
1361 * @param {Number} shiftEnterMode See the `enterMode` argument.
1362 */
1363 setActiveEnterMode: function( enterMode, shiftEnterMode ) {
1364 // Validate passed modes or use default ones (validated on init).
1365 enterMode = enterMode ? validateEnterMode( this, enterMode ) : this.enterMode;
1366 shiftEnterMode = shiftEnterMode ? validateEnterMode( this, shiftEnterMode ) : this.shiftEnterMode;
1367
1368 if ( this.activeEnterMode != enterMode || this.activeShiftEnterMode != shiftEnterMode ) {
1369 this.activeEnterMode = enterMode;
1370 this.activeShiftEnterMode = shiftEnterMode;
1371 this.fire( 'activeEnterModeChange' );
1372 }
1373 },
1374
1375 /**
1376 * Shows a notification to the user.
1377 *
1378 * If the [Notification](http://ckeditor.com/addons/notification) plugin is not enabled, this function shows
1379 * a normal alert with the given `message`. The `type` and `progressOrDuration` parameters are supported
1380 * only by the Notification plugin.
1381 *
1382 * If the Notification plugin is enabled, this method creates and shows a new notification.
1383 * By default the notification is shown over the editor content, in the viewport if it is possible.
1384 *
1385 * See {@link CKEDITOR.plugins.notification}.
1386 *
1387 * @since 4.5
1388 * @member CKEDITOR.editor
1389 * @param {String} message The message displayed in the notification.
1390 * @param {String} [type='info'] The type of the notification. Can be `'info'`, `'warning'`, `'success'` or `'progress'`.
1391 * @param {Number} [progressOrDuration] If the type is `progress`, the third parameter may be a progress from `0` to `1`
1392 * (defaults to `0`). Otherwise the third parameter may be a notification duration denoting after how many milliseconds
1393 * the notification should be closed automatically. `0` means that the notification will not close automatically and the user
1394 * needs to close it manually. See {@link CKEDITOR.plugins.notification#duration}.
1395 * Note that `warning` notifications will not be closed automatically.
1396 * @returns {CKEDITOR.plugins.notification} Created and shown notification.
1397 */
1398 showNotification: function( message ) {
1399 alert( message ); // jshint ignore:line
1400 }
1401 } );
1402} )();
1403
1404/**
1405 * The editor has no associated element.
1406 *
1407 * @readonly
1408 * @property {Number} [=0]
1409 * @member CKEDITOR
1410 */
1411CKEDITOR.ELEMENT_MODE_NONE = 0;
1412
1413/**
1414 * The element is to be replaced by the editor instance.
1415 *
1416 * @readonly
1417 * @property {Number} [=1]
1418 * @member CKEDITOR
1419 */
1420CKEDITOR.ELEMENT_MODE_REPLACE = 1;
1421
1422/**
1423 * The editor is to be created inside the element.
1424 *
1425 * @readonly
1426 * @property {Number} [=2]
1427 * @member CKEDITOR
1428 */
1429CKEDITOR.ELEMENT_MODE_APPENDTO = 2;
1430
1431/**
1432 * The editor is to be attached to the element, using it as the editing block.
1433 *
1434 * @readonly
1435 * @property {Number} [=3]
1436 * @member CKEDITOR
1437 */
1438CKEDITOR.ELEMENT_MODE_INLINE = 3;
1439
1440/**
1441 * Whether to escape HTML when the editor updates the original input element.
1442 *
1443 * config.htmlEncodeOutput = true;
1444 *
1445 * @since 3.1
1446 * @cfg {Boolean} [htmlEncodeOutput=false]
1447 * @member CKEDITOR.config
1448 */
1449
1450/**
1451 * If `true`, makes the editor start in read-only state. Otherwise, it will check
1452 * if the linked `<textarea>` element has the `disabled` attribute.
1453 *
1454 * Read more in the [documentation](#!/guide/dev_readonly)
1455 * and see the [SDK sample](http://sdk.ckeditor.com/samples/readonly.html).
1456 *
1457 * config.readOnly = true;
1458 *
1459 * @since 3.6
1460 * @cfg {Boolean} [readOnly=false]
1461 * @member CKEDITOR.config
1462 * @see CKEDITOR.editor#setReadOnly
1463 */
1464
1465/**
1466 * Whether an editable element should have focus when the editor is loading for the first time.
1467 *
1468 * config.startupFocus = true;
1469 *
1470 * @cfg {Boolean} [startupFocus=false]
1471 * @member CKEDITOR.config
1472 */
1473
1474 /**
1475 * Customizes the {@link CKEDITOR.editor#title human-readable title} of this editor. This title is displayed in
1476 * tooltips and impacts various [accessibility aspects](#!/guide/dev_a11y-section-announcing-the-editor-on-the-page),
1477 * e.g. it is commonly used by screen readers for distinguishing editor instances and for navigation.
1478 * Accepted values are a string or `false`.
1479 *
1480 * **Note:** When `config.title` is set globally, the same value will be applied to all editor instances
1481 * loaded with this config. This may adversely affect accessibility as screen reader users will be unable
1482 * to distinguish particular editor instances and navigate between them.
1483 *
1484 * **Note:** Setting `config.title = false` may also impair accessibility in a similar way.
1485 *
1486 * **Note:** Please do not confuse this property with {@link CKEDITOR.editor#name}
1487 * which identifies the instance in the {@link CKEDITOR#instances} literal.
1488 *
1489 * // Sets the title to 'My WYSIWYG editor.'. The original title of the element (if it exists)
1490 * // will be restored once the editor instance is destroyed.
1491 * config.title = 'My WYSIWYG editor.';
1492 *
1493 * // Do not touch the title. If the element already has a title, it remains unchanged.
1494 * // Also if no `title` attribute exists, nothing new will be added.
1495 * config.title = false;
1496 *
1497 * See also:
1498 *
1499 * * CKEDITOR.editor#name
1500 * * CKEDITOR.editor#title
1501 *
1502 * @since 4.2
1503 * @cfg {String/Boolean} [title=based on editor.name]
1504 * @member CKEDITOR.config
1505 */
1506
1507/**
1508 * Sets listeners on editor events.
1509 *
1510 * **Note:** This property can only be set in the `config` object passed directly
1511 * to {@link CKEDITOR#replace}, {@link CKEDITOR#inline}, and other creators.
1512 *
1513 * CKEDITOR.replace( 'editor1', {
1514 * on: {
1515 * instanceReady: function() {
1516 * alert( this.name ); // 'editor1'
1517 * },
1518 *
1519 * key: function() {
1520 * // ...
1521 * }
1522 * }
1523 * } );
1524 *
1525 * @cfg {Object} on
1526 * @member CKEDITOR.config
1527 */
1528
1529/**
1530 * The outermost element in the DOM tree in which the editable element resides. It is provided
1531 * by a specific editor creator after the editor UI is created and is not intended to
1532 * be modified.
1533 *
1534 * var editor = CKEDITOR.instances.editor1;
1535 * alert( editor.container.getName() ); // 'span'
1536 *
1537 * @readonly
1538 * @property {CKEDITOR.dom.element} container
1539 */
1540
1541/**
1542 * The document that stores the editor content.
1543 *
1544 * * For the classic (`iframe`-based) editor it is equal to the document inside the
1545 * `iframe` containing the editable element.
1546 * * For the inline editor it is equal to {@link CKEDITOR#document}.
1547 *
1548 * The document object is available after the {@link #contentDom} event is fired
1549 * and may be invalidated when the {@link #contentDomUnload} event is fired
1550 * (classic editor only).
1551 *
1552 * editor.on( 'contentDom', function() {
1553 * console.log( editor.document );
1554 * } );
1555 *
1556 * @readonly
1557 * @property {CKEDITOR.dom.document} document
1558 */
1559
1560/**
1561 * The window instance related to the {@link #document} property.
1562 *
1563 * It is always equal to the `editor.document.getWindow()`.
1564 *
1565 * See the {@link #document} property documentation.
1566 *
1567 * @readonly
1568 * @property {CKEDITOR.dom.window} window
1569 */
1570
1571/**
1572 * The main filter instance used for input data filtering, data
1573 * transformations, and activation of features.
1574 *
1575 * It points to a {@link CKEDITOR.filter} instance set up based on
1576 * editor configuration.
1577 *
1578 * @since 4.1
1579 * @readonly
1580 * @property {CKEDITOR.filter} filter
1581 */
1582
1583/**
1584 * The active filter instance which should be used in the current context (location selection).
1585 * This instance will be used to make a decision which commands, buttons and other
1586 * {@link CKEDITOR.feature features} can be enabled.
1587 *
1588 * By default it equals the {@link #filter} and it can be changed by the {@link #setActiveFilter} method.
1589 *
1590 * editor.on( 'activeFilterChange', function() {
1591 * if ( editor.activeFilter.check( 'cite' ) )
1592 * // Do something when <cite> was enabled - e.g. enable a button.
1593 * else
1594 * // Otherwise do something else.
1595 * } );
1596 *
1597 * See also the {@link #setActiveEnterMode} method for an explanation of dynamic settings.
1598 *
1599 * @since 4.3
1600 * @readonly
1601 * @property {CKEDITOR.filter} activeFilter
1602 */
1603
1604/**
1605 * The main (static) Enter mode which is a validated version of the {@link CKEDITOR.config#enterMode} setting.
1606 * Currently only one rule exists &mdash; {@link #blockless blockless editors} may have
1607 * Enter modes set only to {@link CKEDITOR#ENTER_BR}.
1608 *
1609 * @since 4.3
1610 * @readonly
1611 * @property {Number} enterMode
1612 */
1613
1614/**
1615 * See the {@link #enterMode} property.
1616 *
1617 * @since 4.3
1618 * @readonly
1619 * @property {Number} shiftEnterMode
1620 */
1621
1622/**
1623 * The dynamic Enter mode which should be used in the current context (selection location).
1624 * By default it equals the {@link #enterMode} and it can be changed by the {@link #setActiveEnterMode} method.
1625 *
1626 * See also the {@link #setActiveEnterMode} method for an explanation of dynamic settings.
1627 *
1628 * @since 4.3
1629 * @readonly
1630 * @property {Number} activeEnterMode
1631 */
1632
1633/**
1634 * See the {@link #activeEnterMode} property.
1635 *
1636 * @since 4.3
1637 * @readonly
1638 * @property {Number} activeShiftEnterMode
1639 */
1640
1641/**
1642 * Event fired by the {@link #setActiveFilter} method when the {@link #activeFilter} is changed.
1643 *
1644 * @since 4.3
1645 * @event activeFilterChange
1646 */
1647
1648/**
1649 * Event fired by the {@link #setActiveEnterMode} method when any of the active Enter modes is changed.
1650 * See also the {@link #activeEnterMode} and {@link #activeShiftEnterMode} properties.
1651 *
1652 * @since 4.3
1653 * @event activeEnterModeChange
1654 */
1655
1656/**
1657 * Event fired when a CKEDITOR instance is created, but still before initializing it.
1658 * To interact with a fully initialized instance, use the
1659 * {@link CKEDITOR#instanceReady} event instead.
1660 *
1661 * @event instanceCreated
1662 * @member CKEDITOR
1663 * @param {CKEDITOR.editor} editor The editor instance that has been created.
1664 */
1665
1666/**
1667 * Event fired when CKEDITOR instance's components (configuration, languages and plugins) are fully
1668 * loaded and initialized. However, the editor will be fully ready for interaction
1669 * on {@link CKEDITOR#instanceReady}.
1670 *
1671 * @event instanceLoaded
1672 * @member CKEDITOR
1673 * @param {CKEDITOR.editor} editor This editor instance that has been loaded.
1674 */
1675
1676/**
1677 * Event fired when a CKEDITOR instance is destroyed.
1678 *
1679 * @event instanceDestroyed
1680 * @member CKEDITOR
1681 * @param {CKEDITOR.editor} editor The editor instance that has been destroyed.
1682 */
1683
1684/**
1685 * Event fired when a CKEDITOR instance is created, fully initialized and ready for interaction.
1686 *
1687 * @event instanceReady
1688 * @member CKEDITOR
1689 * @param {CKEDITOR.editor} editor The editor instance that has been created.
1690 */
1691
1692/**
1693 * Event fired when the language is loaded into the editor instance.
1694 *
1695 * @since 3.6.1
1696 * @event langLoaded
1697 * @param {CKEDITOR.editor} editor This editor instance.
1698 */
1699
1700/**
1701 * Event fired when all plugins are loaded and initialized into the editor instance.
1702 *
1703 * @event pluginsLoaded
1704 * @param {CKEDITOR.editor} editor This editor instance.
1705 */
1706
1707/**
1708 * Event fired when the styles set is loaded. During the editor initialization
1709 * phase the {@link #getStylesSet} method returns only styles that
1710 * are already loaded, which may not include e.g. styles parsed
1711 * by the `stylesheetparser` plugin. Thus, to be notified when all
1712 * styles are ready, you can listen on this event.
1713 *
1714 * @since 4.1
1715 * @event stylesSet
1716 * @param {CKEDITOR.editor} editor This editor instance.
1717 * @param {Array} styles An array of styles definitions.
1718 */
1719
1720/**
1721 * Event fired before the command execution when {@link #execCommand} is called.
1722 *
1723 * @event beforeCommandExec
1724 * @param {CKEDITOR.editor} editor This editor instance.
1725 * @param data
1726 * @param {String} data.name The command name.
1727 * @param {Object} data.commandData The data to be sent to the command. This
1728 * can be manipulated by the event listener.
1729 * @param {CKEDITOR.command} data.command The command itself.
1730 */
1731
1732/**
1733 * Event fired after the command execution when {@link #execCommand} is called.
1734 *
1735 * @event afterCommandExec
1736 * @param {CKEDITOR.editor} editor This editor instance.
1737 * @param data
1738 * @param {String} data.name The command name.
1739 * @param {Object} data.commandData The data sent to the command.
1740 * @param {CKEDITOR.command} data.command The command itself.
1741 * @param {Object} data.returnValue The value returned by the command execution.
1742 */
1743
1744/**
1745 * Event fired when a custom configuration file is loaded, before the final
1746 * configuration initialization.
1747 *
1748 * Custom configuration files can be loaded thorugh the
1749 * {@link CKEDITOR.config#customConfig} setting. Several files can be loaded
1750 * by changing this setting.
1751 *
1752 * @event customConfigLoaded
1753 * @param {CKEDITOR.editor} editor This editor instance.
1754 */
1755
1756/**
1757 * Event fired once the editor configuration is ready (loaded and processed).
1758 *
1759 * @event configLoaded
1760 * @param {CKEDITOR.editor} editor This editor instance.
1761 */
1762
1763/**
1764 * Event fired when this editor instance is destroyed. The editor at this
1765 * point is not usable and this event should be used to perform the clean-up
1766 * in any plugin.
1767 *
1768 * @event destroy
1769 * @param {CKEDITOR.editor} editor This editor instance.
1770 */
1771
1772/**
1773 * Internal event to get the current data.
1774 *
1775 * @event beforeGetData
1776 * @param {CKEDITOR.editor} editor This editor instance.
1777 */
1778
1779/**
1780 * Internal event to perform the {@link #method-getSnapshot} call.
1781 *
1782 * @event getSnapshot
1783 * @param {CKEDITOR.editor} editor This editor instance.
1784 */
1785
1786/**
1787 * Internal event to perform the {@link #method-loadSnapshot} call.
1788 *
1789 * @event loadSnapshot
1790 * @param {CKEDITOR.editor} editor This editor instance.
1791 * @param {String} data The data that will be used.
1792 */
1793
1794/**
1795 * Event fired before the {@link #method-getData} call returns, allowing for additional manipulation.
1796 *
1797 * @event getData
1798 * @param {CKEDITOR.editor} editor This editor instance.
1799 * @param data
1800 * @param {String} data.dataValue The data that will be returned.
1801 */
1802
1803/**
1804 * Event fired before the {@link #method-setData} call is executed, allowing for additional manipulation.
1805 *
1806 * @event setData
1807 * @param {CKEDITOR.editor} editor This editor instance.
1808 * @param data
1809 * @param {String} data.dataValue The data that will be used.
1810 */
1811
1812/**
1813 * Event fired at the end of the {@link #method-setData} call execution. Usually it is better to use the
1814 * {@link #dataReady} event.
1815 *
1816 * @event afterSetData
1817 * @param {CKEDITOR.editor} editor This editor instance.
1818 * @param data
1819 * @param {String} data.dataValue The data that has been set.
1820 */
1821
1822/**
1823 * Event fired as an indicator of the editor data loading. It may be the result of
1824 * calling {@link #method-setData} explicitly or an internal
1825 * editor function, like the editor editing mode switching (move to Source and back).
1826 *
1827 * @event dataReady
1828 * @param {CKEDITOR.editor} editor This editor instance.
1829 */
1830
1831/**
1832 * Event fired when the CKEDITOR instance is completely created, fully initialized
1833 * and ready for interaction.
1834 *
1835 * @event instanceReady
1836 * @param {CKEDITOR.editor} editor This editor instance.
1837 */
1838
1839/**
1840 * Event fired when editor components (configuration, languages and plugins) are fully
1841 * loaded and initialized. However, the editor will be fully ready to for interaction
1842 * on {@link #instanceReady}.
1843 *
1844 * @event loaded
1845 * @param {CKEDITOR.editor} editor This editor instance.
1846 */
1847
1848/**
1849 * Event fired by the {@link #method-insertHtml} method. See the method documentation for more information
1850 * about how this event can be used.
1851 *
1852 * @event insertHtml
1853 * @param {CKEDITOR.editor} editor This editor instance.
1854 * @param data
1855 * @param {String} data.mode The mode in which the data is inserted (see {@link #method-insertHtml}).
1856 * @param {String} data.dataValue The HTML code to insert.
1857 * @param {CKEDITOR.dom.range} [data.range] See {@link #method-insertHtml}'s `range` parameter.
1858 */
1859
1860/**
1861 * Event fired by the {@link #method-insertText} method. See the method documentation for more information
1862 * about how this event can be used.
1863 *
1864 * @event insertText
1865 * @param {CKEDITOR.editor} editor This editor instance.
1866 * @param {String} data The text to insert.
1867 */
1868
1869/**
1870 * Event fired by the {@link #method-insertElement} method. See the method documentation for more information
1871 * about how this event can be used.
1872 *
1873 * @event insertElement
1874 * @param {CKEDITOR.editor} editor This editor instance.
1875 * @param {CKEDITOR.dom.element} data The element to insert.
1876 */
1877
1878/**
1879 * Event fired after data insertion using the {@link #method-insertHtml}, {@link CKEDITOR.editable#insertHtml},
1880 * or {@link CKEDITOR.editable#insertHtmlIntoRange} methods.
1881 *
1882 * @since 4.5
1883 * @event afterInsertHtml
1884 * @param data
1885 * @param {CKEDITOR.dom.range} [data.intoRange] If set, the HTML was not inserted into the current selection, but into
1886 * the specified range. This property is set if the {@link CKEDITOR.editable#insertHtmlIntoRange} method was used,
1887 * but not if for the {@link CKEDITOR.editable#insertHtml} method.
1888 */
1889
1890/**
1891 * Event fired after the {@link #property-readOnly} property changes.
1892 *
1893 * @since 3.6
1894 * @event readOnly
1895 * @param {CKEDITOR.editor} editor This editor instance.
1896 */
1897
1898/**
1899 * Event fired when a UI template is added to the editor instance. It makes
1900 * it possible to bring customizations to the template source.
1901 *
1902 * @event template
1903 * @param {CKEDITOR.editor} editor This editor instance.
1904 * @param data
1905 * @param {String} data.name The template name.
1906 * @param {String} data.source The source data for this template.
1907 */
1908
1909/**
1910 * Event fired when the editor content (its DOM structure) is ready.
1911 * It is similar to the native `DOMContentLoaded` event, but it applies to
1912 * the editor content. It is also the first event fired after
1913 * the {@link CKEDITOR.editable} is initialized.
1914 *
1915 * This event is particularly important for classic (`iframe`-based)
1916 * editor, because on editor initialization and every time the data are set
1917 * (by {@link CKEDITOR.editor#method-setData}) content DOM structure
1918 * is rebuilt. Thus, e.g. you need to attach DOM event listeners
1919 * on editable one more time.
1920 *
1921 * For inline editor this event is fired only once &mdash; when the
1922 * editor is initialized for the first time. This is because setting
1923 * editor content does not cause editable destruction and creation.
1924 *
1925 * The {@link #contentDom} event goes along with {@link #contentDomUnload}
1926 * which is fired before the content DOM structure is destroyed. This is the
1927 * right moment to detach content DOM event listener. Otherwise
1928 * browsers like IE or Opera may throw exceptions when accessing
1929 * elements from the detached document.
1930 *
1931 * **Note:** {@link CKEDITOR.editable#attachListener} is a convenient
1932 * way to attach listeners that will be detached on {@link #contentDomUnload}.
1933 *
1934 * editor.on( 'contentDom', function() {
1935 * var editable = editor.editable();
1936 *
1937 * editable.attachListener( editable, 'click', function() {
1938 * console.log( 'The editable was clicked.' );
1939 * });
1940 * });
1941 *
1942 * @event contentDom
1943 * @param {CKEDITOR.editor} editor This editor instance.
1944 */
1945
1946/**
1947 * Event fired before the content DOM structure is destroyed.
1948 * See {@link #contentDom} documentation for more details.
1949 *
1950 * @event contentDomUnload
1951 * @param {CKEDITOR.editor} editor This editor instance.
1952 */
1953
1954/**
1955 * Event fired when the content DOM changes and some of the references as well as
1956 * the native DOM event listeners could be lost.
1957 * This event is useful when it is important to keep track of references
1958 * to elements in the editable content from code.
1959 *
1960 * @since 4.3
1961 * @event contentDomInvalidated
1962 * @param {CKEDITOR.editor} editor This editor instance.
1963 */
diff --git a/sources/core/editor_basic.js b/sources/core/editor_basic.js
new file mode 100644
index 00000000..f81efb35
--- /dev/null
+++ b/sources/core/editor_basic.js
@@ -0,0 +1,36 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6if ( !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 00000000..1659071f
--- /dev/null
+++ b/sources/core/env.js
@@ -0,0 +1,361 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.env} object which contains
8 * environment and browser information.
9 */
10
11if ( !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 00000000..a660f34b
--- /dev/null
+++ b/sources/core/event.js
@@ -0,0 +1,389 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.event} class, which serves as the
8 * base for classes and objects that require event handling features.
9 */
10
11if ( !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 00000000..db0ee390
--- /dev/null
+++ b/sources/core/eventInfo.js
@@ -0,0 +1,115 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the "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 00000000..c85b6beb
--- /dev/null
+++ b/sources/core/filter.js
@@ -0,0 +1,2440 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 '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'` &ndash; for {@link CKEDITOR#ENTER_P},
53 * * `'div'` &ndash; for {@link CKEDITOR#ENTER_DIV},
54 * * `'br'` &ndash; 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) &ndash; 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) &ndash; 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) &ndash; 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` &ndash; 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 &mdash; 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 &ndash; 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} &ndash; defined rules will be added
2192 * to the {@link CKEDITOR.editor#filter}.
2193 * * `true` &ndash; will disable the filter (data will not be filtered,
2194 * all features will be activated).
2195 * * default &ndash; 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 &ndash; 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 &ndash; it does not mean exactly
2347 * `<strong>` or `<b>` but just the ability to create bold text.
2348 * * "Format" drop-down list &ndash; 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 00000000..b9c779bf
--- /dev/null
+++ b/sources/core/focusmanager.js
@@ -0,0 +1,273 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 00000000..9bc9ef0b
--- /dev/null
+++ b/sources/core/htmldataprocessor.js
@@ -0,0 +1,1036 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /**
8 * Represents 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 &nbsp; at the end of blocks, which are actually placeholders.
487 // Safari transforms the &nbsp; to \xa0. (#4172)
488 var tailNbspRegex = /(?:&nbsp;|\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="&lt;a href=&quot;X&quot;" />
767 //
768 // then the "non-greedy match" returns:
769 //
770 // 'href' => '&quot;X&quot;' // It's wrong! Href is not an attribute of <img>.
771 //
772 // while greedy match returns:
773 //
774 // 'data-x' => '&lt;a href=&quot;X&quot;'
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, '&lt;' ).replace( />/g, '&gt;' ) + '</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 &mdash; `&nbsp;`) 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 00000000..078fc3a9
--- /dev/null
+++ b/sources/core/htmlparser.js
@@ -0,0 +1,205 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * 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 */
18CKEDITOR.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 00000000..705a4ac6
--- /dev/null
+++ b/sources/core/htmlparser/basicwriter.js
@@ -0,0 +1,152 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * TODO
8 *
9 * @class
10 * @todo
11 */
12CKEDITOR.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 00000000..2acba74f
--- /dev/null
+++ b/sources/core/htmlparser/cdata.js
@@ -0,0 +1,48 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 'use strict';
7
8( function() {
9
10 /**
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 00000000..1508d71a
--- /dev/null
+++ b/sources/core/htmlparser/comment.js
@@ -0,0 +1,80 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 'use strict';
7
8/**
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 */
16CKEDITOR.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
30CKEDITOR.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 00000000..c4497dab
--- /dev/null
+++ b/sources/core/htmlparser/element.js
@@ -0,0 +1,536 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8/**
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 */
18CKEDITOR.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 */
66CKEDITOR.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: &quot;Lucida, Console&quot;'
76 // TODO reuse CSS methods from tools.
77 ( styleText || '' ).replace( /&quot;/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 &mdash; `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 00000000..06550641
--- /dev/null
+++ b/sources/core/htmlparser/filter.js
@@ -0,0 +1,407 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8( function() {
9 /**
10 * 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 00000000..00e87909
--- /dev/null
+++ b/sources/core/htmlparser/fragment.js
@@ -0,0 +1,646 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8/**
9 * A lightweight representation of an HTML DOM structure.
10 *
11 * @class
12 * @constructor Creates a fragment class instance.
13 */
14CKEDITOR.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 00000000..a70dede1
--- /dev/null
+++ b/sources/core/htmlparser/node.js
@@ -0,0 +1,156 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8( function() {
9 /**
10 * 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 00000000..d7b0d321
--- /dev/null
+++ b/sources/core/htmlparser/text.js
@@ -0,0 +1,70 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 'use strict';
7
8( function() {
9 /**
10 * 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 00000000..8d8b443b
--- /dev/null
+++ b/sources/core/keystrokehandler.js
@@ -0,0 +1,169 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * 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 */
13CKEDITOR.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 00000000..f2417e8a
--- /dev/null
+++ b/sources/core/lang.js
@@ -0,0 +1,103 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /**
8 * 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, 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 00000000..298106a6
--- /dev/null
+++ b/sources/core/loader.js
@@ -0,0 +1,225 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.loader} objects, which is used to
8 * load core scripts and their dependencies from _source.
9 */
10
11if ( typeof CKEDITOR == 'undefined' )
12 CKEDITOR = {}; // jshint ignore:line
13
14if ( !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.
222if ( 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 00000000..8461021d
--- /dev/null
+++ b/sources/core/log.js
@@ -0,0 +1,127 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines {@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 */
26CKEDITOR.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 */
38CKEDITOR.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 */
55CKEDITOR.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 */
66CKEDITOR.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 */
82CKEDITOR.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 */
109CKEDITOR.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 00000000..4d8b1139
--- /dev/null
+++ b/sources/core/plugindefinition.js
@@ -0,0 +1,177 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the "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 00000000..66f500d1
--- /dev/null
+++ b/sources/core/plugins.js
@@ -0,0 +1,119 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 */
18CKEDITOR.plugins = new CKEDITOR.resourceManager( 'plugins/', 'plugin' );
19
20// PACKAGER_RENAME( CKEDITOR.plugins )
21
22CKEDITOR.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 */
107CKEDITOR.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 00000000..af7d12e0
--- /dev/null
+++ b/sources/core/resourcemanager.js
@@ -0,0 +1,228 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 */
20CKEDITOR.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
62CKEDITOR.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 00000000..a6bcbd2b
--- /dev/null
+++ b/sources/core/scriptloader.js
@@ -0,0 +1,203 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.scriptLoader} object, used to load scripts
8 * asynchronously.
9 */
10
11/**
12 * Load scripts asynchronously.
13 *
14 * @class
15 * @singleton
16 */
17CKEDITOR.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 00000000..fec3b7fc
--- /dev/null
+++ b/sources/core/selection.js
@@ -0,0 +1,2183 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 // #### 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 createFillingChar( element ) {
142 removeFillingChar( element, false );
143
144 var fillingChar = element.getDocument().createText( '\u200B' );
145 element.setCustomData( 'cke-fillingChar', fillingChar );
146
147 return fillingChar;
148 }
149
150 function getFillingChar( element ) {
151 return element.getCustomData( 'cke-fillingChar' );
152 }
153
154 // Checks if a filling char has been used, eventualy removing it (#1272).
155 function checkFillingChar( element ) {
156 var fillingChar = getFillingChar( element );
157 if ( fillingChar ) {
158 // Use this flag to avoid removing the filling char right after
159 // creating it.
160 if ( fillingChar.getCustomData( 'ready' ) )
161 removeFillingChar( element );
162 else
163 fillingChar.setCustomData( 'ready', 1 );
164 }
165 }
166
167 function removeFillingChar( element, keepSelection ) {
168 var fillingChar = element && element.removeCustomData( 'cke-fillingChar' );
169 if ( fillingChar ) {
170
171 // Text selection position might get mangled by
172 // subsequent dom modification, save it now for restoring. (#8617)
173 if ( keepSelection !== false ) {
174 var bm,
175 sel = element.getDocument().getSelection().getNative(),
176 // Be error proof.
177 range = sel && sel.type != 'None' && sel.getRangeAt( 0 );
178
179 if ( fillingChar.getLength() > 1 && range && range.intersectsNode( fillingChar.$ ) ) {
180 bm = createNativeSelectionBookmark( sel );
181
182 // Anticipate the offset change brought by the removed char.
183 var startAffected = sel.anchorNode == fillingChar.$ && sel.anchorOffset > 0,
184 endAffected = sel.focusNode == fillingChar.$ && sel.focusOffset > 0;
185 startAffected && bm[ 0 ].offset--;
186 endAffected && bm[ 1 ].offset--;
187 }
188 }
189
190 // We can't simply remove the filling node because the user
191 // will actually enlarge it when typing, so we just remove the
192 // invisible char from it.
193 fillingChar.setText( replaceFillingChar( fillingChar.getText() ) );
194
195 // Restore the bookmark preserving selection's direction.
196 if ( bm ) {
197 moveNativeSelectionToBookmark( element.getDocument().$, bm );
198 }
199 }
200 }
201
202 function replaceFillingChar( html ) {
203 return html.replace( /\u200B( )?/g, function( match ) {
204 // #10291 if filling char is followed by a space replace it with nbsp.
205 return match[ 1 ] ? '\xa0' : '';
206 } );
207 }
208
209 function createNativeSelectionBookmark( sel ) {
210 return [
211 { node: sel.anchorNode, offset: sel.anchorOffset },
212 { node: sel.focusNode, offset: sel.focusOffset }
213 ];
214 }
215
216 function moveNativeSelectionToBookmark( document, bm ) {
217 var sel = document.getSelection(),
218 range = document.createRange();
219
220 range.setStart( bm[ 0 ].node, bm[ 0 ].offset );
221 range.collapse( true );
222 sel.removeAllRanges();
223 sel.addRange( range );
224 sel.extend( bm[ 1 ].node, bm[ 1 ].offset );
225 }
226
227 // Creates cke_hidden_sel container and puts real selection there.
228 function hideSelection( editor ) {
229 var style = CKEDITOR.env.ie ? 'display:none' : 'position:fixed;top:0;left:-1000px',
230 hiddenEl = CKEDITOR.dom.element.createFromHtml(
231 '<div data-cke-hidden-sel="1" data-cke-temp="1" style="' + style + '">&nbsp;</div>',
232 editor.document );
233
234 editor.fire( 'lockSnapshot' );
235
236 editor.editable().append( hiddenEl );
237
238 // Always use real selection to avoid overriding locked one (http://dev.ckeditor.com/ticket/11104#comment:13).
239 var sel = editor.getSelection( 1 ),
240 range = editor.createRange(),
241 // Cancel selectionchange fired by selectRanges - prevent from firing selectionChange.
242 listener = sel.root.on( 'selectionchange', function( evt ) {
243 evt.cancel();
244 }, null, null, 0 );
245
246 range.setStartAt( hiddenEl, CKEDITOR.POSITION_AFTER_START );
247 range.setEndAt( hiddenEl, CKEDITOR.POSITION_BEFORE_END );
248 sel.selectRanges( [ range ] );
249
250 listener.removeListener();
251
252 editor.fire( 'unlockSnapshot' );
253
254 // Set this value at the end, so reset() executed by selectRanges()
255 // will clean up old hidden selection container.
256 editor._.hiddenSelectionContainer = hiddenEl;
257 }
258
259 function removeHiddenSelectionContainer( editor ) {
260 var hiddenEl = editor._.hiddenSelectionContainer;
261
262 if ( hiddenEl ) {
263 var isDirty = editor.checkDirty();
264
265 editor.fire( 'lockSnapshot' );
266 hiddenEl.remove();
267 editor.fire( 'unlockSnapshot' );
268
269 !isDirty && editor.resetDirty();
270 }
271
272 delete editor._.hiddenSelectionContainer;
273 }
274
275 // Object containing keystroke handlers for fake selection.
276 var fakeSelectionDefaultKeystrokeHandlers = ( function() {
277 function leave( right ) {
278 return function( evt ) {
279 var range = evt.editor.createRange();
280
281 // Move selection only if there's a editable place for it.
282 // It no, then do nothing (keystroke will be blocked, widget selection kept).
283 if ( range.moveToClosestEditablePosition( evt.selected, right ) )
284 evt.editor.getSelection().selectRanges( [ range ] );
285
286 // Prevent default.
287 return false;
288 };
289 }
290
291 function del( right ) {
292 return function( evt ) {
293 var editor = evt.editor,
294 range = editor.createRange(),
295 found;
296
297 // If haven't found place for caret on the default side,
298 // try to find it on the other side.
299 if ( !( found = range.moveToClosestEditablePosition( evt.selected, right ) ) )
300 found = range.moveToClosestEditablePosition( evt.selected, !right );
301
302 if ( found )
303 editor.getSelection().selectRanges( [ range ] );
304
305 // Save the state before removing selected element.
306 editor.fire( 'saveSnapshot' );
307
308 evt.selected.remove();
309
310 // Haven't found any editable space before removing element,
311 // try to place the caret anywhere (most likely, in empty editable).
312 if ( !found ) {
313 range.moveToElementEditablePosition( editor.editable() );
314 editor.getSelection().selectRanges( [ range ] );
315 }
316
317 editor.fire( 'saveSnapshot' );
318
319 // Prevent default.
320 return false;
321 };
322 }
323
324 var leaveLeft = leave(),
325 leaveRight = leave( 1 );
326
327 return {
328 37: leaveLeft, // LEFT
329 38: leaveLeft, // UP
330 39: leaveRight, // RIGHT
331 40: leaveRight, // DOWN
332 8: del(), // BACKSPACE
333 46: del( 1 ) // DELETE
334 };
335 } )();
336
337 // Handle left, right, delete and backspace keystrokes next to non-editable elements
338 // by faking selection on them.
339 function getOnKeyDownListener( editor ) {
340 var keystrokes = { 37: 1, 39: 1, 8: 1, 46: 1 };
341
342 return function( evt ) {
343 var keystroke = evt.data.getKeystroke();
344
345 // Handle only left/right/del/bspace keys.
346 if ( !keystrokes[ keystroke ] )
347 return;
348
349 var sel = editor.getSelection(),
350 ranges = sel.getRanges(),
351 range = ranges[ 0 ];
352
353 // Handle only single range and it has to be collapsed.
354 if ( ranges.length != 1 || !range.collapsed )
355 return;
356
357 var next = range[ keystroke < 38 ? 'getPreviousEditableNode' : 'getNextEditableNode' ]();
358
359 if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getAttribute( 'contenteditable' ) == 'false' ) {
360 editor.getSelection().fake( next );
361 evt.data.preventDefault();
362 evt.cancel();
363 }
364 };
365 }
366
367 // If fake selection should be applied this function will return instance of
368 // CKEDITOR.dom.element which should gain fake selection.
369 function getNonEditableFakeSelectionReceiver( ranges ) {
370 var enclosedNode, shrinkedNode, clone, range;
371
372 if ( ranges.length == 1 && !( range = ranges[ 0 ] ).collapsed &&
373 ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) {
374 // So far we can't say that enclosed element is non-editable. Before checking,
375 // we'll shrink range (clone). Shrinking will stop on non-editable range, or
376 // innermost element (#11114).
377 clone = range.clone();
378 clone.shrink( CKEDITOR.SHRINK_ELEMENT, true );
379
380 // If shrinked range still encloses an element, check this one (shrink stops only on non-editable elements).
381 if ( ( shrinkedNode = clone.getEnclosedNode() ) && shrinkedNode.type == CKEDITOR.NODE_ELEMENT )
382 enclosedNode = shrinkedNode;
383
384 if ( enclosedNode.getAttribute( 'contenteditable' ) == 'false' )
385 return enclosedNode;
386 }
387 }
388
389 // Fix ranges which may end after hidden selection container.
390 // Note: this function may only be used if hidden selection container
391 // is not in DOM any more.
392 function fixRangesAfterHiddenSelectionContainer( ranges, root ) {
393 var range;
394 for ( var i = 0; i < ranges.length; ++i ) {
395 range = ranges[ i ];
396 if ( range.endContainer.equals( root ) ) {
397 // We can use getChildCount() because hidden selection container is not in DOM.
398 range.endOffset = Math.min( range.endOffset, root.getChildCount() );
399 }
400 }
401 }
402
403 // Extract only editable part or ranges.
404 // Note: this function modifies ranges list!
405 // @param {CKEDITOR.dom.rangeList} ranges
406 function extractEditableRanges( ranges ) {
407 for ( var i = 0; i < ranges.length; i++ ) {
408 var range = ranges[ i ];
409
410 // Drop range spans inside one ready-only node.
411 var parent = range.getCommonAncestor();
412 if ( parent.isReadOnly() )
413 ranges.splice( i, 1 );
414
415 if ( range.collapsed )
416 continue;
417
418 // Range may start inside a non-editable element,
419 // replace the range start after it.
420 if ( range.startContainer.isReadOnly() ) {
421 var current = range.startContainer,
422 isElement;
423
424 while ( current ) {
425 isElement = current.type == CKEDITOR.NODE_ELEMENT;
426
427 if ( ( isElement && current.is( 'body' ) ) || !current.isReadOnly() )
428 break;
429
430 if ( isElement && current.getAttribute( 'contentEditable' ) == 'false' )
431 range.setStartAfter( current );
432
433 current = current.getParent();
434 }
435 }
436
437 var startContainer = range.startContainer,
438 endContainer = range.endContainer,
439 startOffset = range.startOffset,
440 endOffset = range.endOffset,
441 walkerRange = range.clone();
442
443 // Enlarge range start/end with text node to avoid walker
444 // being DOM destructive, it doesn't interfere our checking
445 // of elements below as well.
446 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
447 if ( startOffset >= startContainer.getLength() )
448 walkerRange.setStartAfter( startContainer );
449 else
450 walkerRange.setStartBefore( startContainer );
451 }
452
453 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
454 if ( !endOffset )
455 walkerRange.setEndBefore( endContainer );
456 else
457 walkerRange.setEndAfter( endContainer );
458 }
459
460 // Looking for non-editable element inside the range.
461 var walker = new CKEDITOR.dom.walker( walkerRange );
462 walker.evaluator = function( node ) {
463 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isReadOnly() ) {
464 var newRange = range.clone();
465 range.setEndBefore( node );
466
467 // Drop collapsed range around read-only elements,
468 // it make sure the range list empty when selecting
469 // only non-editable elements.
470 if ( range.collapsed )
471 ranges.splice( i--, 1 );
472
473 // Avoid creating invalid range.
474 if ( !( node.getPosition( walkerRange.endContainer ) & CKEDITOR.POSITION_CONTAINS ) ) {
475 newRange.setStartAfter( node );
476 if ( !newRange.collapsed )
477 ranges.splice( i + 1, 0, newRange );
478 }
479
480 return true;
481 }
482
483 return false;
484 };
485
486 walker.next();
487 }
488
489 return ranges;
490 }
491
492 // Setup all editor instances for the necessary selection hooks.
493 CKEDITOR.on( 'instanceCreated', function( ev ) {
494 var editor = ev.editor;
495
496 editor.on( 'contentDom', function() {
497 var doc = editor.document,
498 outerDoc = CKEDITOR.document,
499 editable = editor.editable(),
500 body = doc.getBody(),
501 html = doc.getDocumentElement();
502
503 var isInline = editable.isInline();
504
505 var restoreSel,
506 lastSel;
507
508 // Give the editable an initial selection on first focus,
509 // put selection at a consistent position at the start
510 // of the contents. (#9507)
511 if ( CKEDITOR.env.gecko ) {
512 editable.attachListener( editable, 'focus', function( evt ) {
513 evt.removeListener();
514
515 if ( restoreSel !== 0 ) {
516 var nativ = editor.getSelection().getNative();
517 // Do it only if the native selection is at an unwanted
518 // place (at the very start of the editable). #10119
519 if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) {
520 var rng = editor.createRange();
521 rng.moveToElementEditStart( editable );
522 rng.select();
523 }
524 }
525 }, null, null, -2 );
526 }
527
528 // Plays the magic here to restore/save dom selection on editable focus/blur.
529 editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusIn' : 'focus', function() {
530 // On Webkit we use DOMFocusIn which is fired more often than focus - e.g. when moving from main editable
531 // to nested editable (or the opposite). Unlock selection all, but restore only when it was locked
532 // for the same active element, what will e.g. mean restoring after displaying dialog.
533 if ( restoreSel && CKEDITOR.env.webkit )
534 restoreSel = editor._.previousActive && editor._.previousActive.equals( doc.getActive() );
535
536 editor.unlockSelection( restoreSel );
537 restoreSel = 0;
538 }, null, null, -1 );
539
540 // Disable selection restoring when clicking in.
541 editable.attachListener( editable, 'mousedown', function() {
542 restoreSel = 0;
543 } );
544
545 // Save a cloned version of current selection.
546 function saveSel() {
547 lastSel = new CKEDITOR.dom.selection( editor.getSelection() );
548 lastSel.lock();
549 }
550
551 // Browsers could loose the selection once the editable lost focus,
552 // in such case we need to reproduce it by saving a locked selection
553 // and restoring it upon focus gain.
554 if ( CKEDITOR.env.ie || isInline ) {
555 // For old IEs, we can retrieve the last correct DOM selection upon the "beforedeactivate" event.
556 // For the rest, a more frequent check is required for each selection change made.
557 if ( isMSSelection )
558 editable.attachListener( editable, 'beforedeactivate', saveSel, null, null, -1 );
559 else
560 editable.attachListener( editor, 'selectionCheck', saveSel, null, null, -1 );
561
562 // Lock the selection and mark it to be restored.
563 // On Webkit we use DOMFocusOut which is fired more often than blur. I.e. it will also be
564 // fired when nested editable is blurred.
565 editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusOut' : 'blur', function() {
566 editor.lockSelection( lastSel );
567 restoreSel = 1;
568 }, null, null, -1 );
569
570 // Disable selection restoring when clicking in.
571 editable.attachListener( editable, 'mousedown', function() {
572 restoreSel = 0;
573 } );
574 }
575
576 // The following selection-related fixes only apply to classic (`iframe`-based) editable.
577 if ( CKEDITOR.env.ie && !isInline ) {
578 var scroll;
579 editable.attachListener( editable, 'mousedown', function( evt ) {
580 // IE scrolls document to top on right mousedown
581 // when editor has no focus, remember this scroll
582 // position and revert it before context menu opens. (#5778)
583 if ( evt.data.$.button == 2 ) {
584 var sel = editor.document.getSelection();
585 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
586 scroll = editor.window.getScrollPosition();
587 }
588 } );
589
590 editable.attachListener( editable, 'mouseup', function( evt ) {
591 // Restore recorded scroll position when needed on right mouseup.
592 if ( evt.data.$.button == 2 && scroll ) {
593 editor.document.$.documentElement.scrollLeft = scroll.x;
594 editor.document.$.documentElement.scrollTop = scroll.y;
595 }
596 scroll = null;
597 } );
598
599 // When content doc is in standards mode, IE doesn't focus the editor when
600 // clicking at the region below body (on html element) content, we emulate
601 // the normal behavior on old IEs. (#1659, #7932)
602 if ( doc.$.compatMode != 'BackCompat' ) {
603 if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
604 html.on( 'mousedown', function( evt ) {
605 evt = evt.data;
606
607 // Expand the text range along with mouse move.
608 function onHover( evt ) {
609 evt = evt.data.$;
610 if ( textRng ) {
611 // Read the current cursor.
612 var rngEnd = body.$.createTextRange();
613
614 moveRangeToPoint( rngEnd, evt.clientX, evt.clientY );
615
616 // Handle drag directions.
617 textRng.setEndPoint(
618 startRng.compareEndPoints( 'StartToStart', rngEnd ) < 0 ?
619 'EndToEnd' : 'StartToStart', rngEnd );
620
621 // Update selection with new range.
622 textRng.select();
623 }
624 }
625
626 function removeListeners() {
627 outerDoc.removeListener( 'mouseup', onSelectEnd );
628 html.removeListener( 'mouseup', onSelectEnd );
629 }
630
631 function onSelectEnd() {
632 html.removeListener( 'mousemove', onHover );
633 removeListeners();
634
635 // Make it in effect on mouse up. (#9022)
636 textRng.select();
637 }
638
639
640 // We're sure that the click happens at the region
641 // below body, but not on scrollbar.
642 if ( evt.getTarget().is( 'html' ) &&
643 evt.$.y < html.$.clientHeight &&
644 evt.$.x < html.$.clientWidth ) {
645 // Start to build the text range.
646 var textRng = body.$.createTextRange();
647 moveRangeToPoint( textRng, evt.$.clientX, evt.$.clientY );
648
649 // Records the dragging start of the above text range.
650 var startRng = textRng.duplicate();
651
652 html.on( 'mousemove', onHover );
653 outerDoc.on( 'mouseup', onSelectEnd );
654 html.on( 'mouseup', onSelectEnd );
655 }
656 } );
657 }
658
659 // It's much simpler for IE8+, we just need to reselect the reported range.
660 // This hack does not work on IE>=11 because there's no old selection&range APIs.
661 if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) {
662 html.on( 'mousedown', function( evt ) {
663 if ( evt.data.getTarget().is( 'html' ) ) {
664 // Limit the text selection mouse move inside of editable. (#9715)
665 outerDoc.on( 'mouseup', onSelectEnd );
666 html.on( 'mouseup', onSelectEnd );
667 }
668 } );
669 }
670 }
671 }
672
673 // We check the selection change:
674 // 1. Upon "selectionchange" event from the editable element. (which might be faked event fired by our code)
675 // 2. After the accomplish of keyboard and mouse events.
676 editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor );
677 editable.attachListener( editable, 'keyup', checkSelectionChangeTimeout, editor );
678 // Always fire the selection change on focus gain.
679 // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and
680 // we need synchronization between those listeners to not lost cached editor._.previousActive property
681 // (which is updated on selectionCheck).
682 editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusIn' : 'focus', function() {
683 editor.forceNextSelectionCheck();
684 editor.selectionChange( 1 );
685 } );
686
687 // #9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
688 // by dragging and releasing mouse button outside editable. Dragging (mousedown)
689 // has to be initialized in editable, but for mouseup we listen on document element.
690 if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) {
691 var mouseDown;
692 editable.attachListener( editable, 'mousedown', function() {
693 mouseDown = 1;
694 } );
695 editable.attachListener( doc.getDocumentElement(), 'mouseup', function() {
696 if ( mouseDown )
697 checkSelectionChangeTimeout.call( editor );
698 mouseDown = 0;
699 } );
700 }
701 // In all other cases listen on simple mouseup over editable, as we did before #9699.
702 //
703 // Use document instead of editable in non-IEs for observing mouseup
704 // since editable won't fire the event if selection process started within iframe and ended out
705 // of the editor (#9851).
706 else {
707 editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor );
708 }
709
710 if ( CKEDITOR.env.webkit ) {
711 // Before keystroke is handled by editor, check to remove the filling char.
712 editable.attachListener( doc, 'keydown', function( evt ) {
713 var key = evt.data.getKey();
714 // Remove the filling char before some keys get
715 // executed, so they'll not get blocked by it.
716 switch ( key ) {
717 case 13: // ENTER
718 case 33: // PAGEUP
719 case 34: // PAGEDOWN
720 case 35: // HOME
721 case 36: // END
722 case 37: // LEFT-ARROW
723 case 39: // RIGHT-ARROW
724 case 8: // BACKSPACE
725 case 45: // INS
726 case 46: // DEl
727 removeFillingChar( editable );
728 }
729
730 }, null, null, -1 );
731 }
732
733 // Automatically select non-editable element when navigating into
734 // it by left/right or backspace/del keys.
735 editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 );
736
737 function moveRangeToPoint( range, x, y ) {
738 // Error prune in IE7. (#9034, #9110)
739 try {
740 range.moveToPoint( x, y );
741 } catch ( e ) {}
742 }
743
744 function removeListeners() {
745 outerDoc.removeListener( 'mouseup', onSelectEnd );
746 html.removeListener( 'mouseup', onSelectEnd );
747 }
748
749 function onSelectEnd() {
750 removeListeners();
751
752 // The event is not fired when clicking on the scrollbars,
753 // so we can safely check the following to understand
754 // whether the empty space following <body> has been clicked.
755 var sel = CKEDITOR.document.$.selection,
756 range = sel.createRange();
757
758 // The selection range is reported on host, but actually it should applies to the content doc.
759 if ( sel.type != 'None' && range.parentElement().ownerDocument == doc.$ )
760 range.select();
761 }
762 } );
763
764 editor.on( 'setData', function() {
765 // Invalidate locked selection when unloading DOM.
766 // (#9521, #5217#comment:32 and #11500#comment:11)
767 editor.unlockSelection();
768
769 // Webkit's selection will mess up after the data loading.
770 if ( CKEDITOR.env.webkit )
771 clearSelection();
772 } );
773
774 // Catch all the cases which above setData listener couldn't catch.
775 // For example: switching to source mode and destroying editor.
776 editor.on( 'contentDomUnload', function() {
777 editor.unlockSelection();
778 } );
779
780 // IE9 might cease to work if there's an object selection inside the iframe (#7639).
781 if ( CKEDITOR.env.ie9Compat )
782 editor.on( 'beforeDestroy', clearSelection, null, null, 9 );
783
784 // Check selection change on data reload.
785 editor.on( 'dataReady', function() {
786 // Clean up fake selection after setting data.
787 delete editor._.fakeSelection;
788 delete editor._.hiddenSelectionContainer;
789
790 editor.selectionChange( 1 );
791 } );
792
793 // When loaded data are ready check whether hidden selection container was not loaded.
794 editor.on( 'loadSnapshot', function() {
795 var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ),
796 // TODO replace with el.find() which will be introduced in #9764,
797 // because it may happen that hidden sel container won't be the last element.
798 last = editor.editable().getLast( isElement );
799
800 if ( last && last.hasAttribute( 'data-cke-hidden-sel' ) ) {
801 last.remove();
802
803 // Firefox does a very unfortunate thing. When a non-editable element is the only
804 // element in the editable, when we remove the hidden selection container, Firefox
805 // will insert a bogus <br> at the beginning of the editable...
806 // See: https://bugzilla.mozilla.org/show_bug.cgi?id=911201
807 //
808 // This behavior is never desired because this <br> pushes the content lower, but in
809 // this case it is especially dangerous, because it happens when a bookmark is being restored.
810 // Since this <br> is inserted at the beginning it changes indexes and thus breaks the bookmark2
811 // what results in errors.
812 //
813 // So... let's revert what Firefox broke.
814 if ( CKEDITOR.env.gecko ) {
815 var first = editor.editable().getFirst( isElement );
816 if ( first && first.is( 'br' ) && first.getAttribute( '_moz_editor_bogus_node' ) ) {
817 first.remove();
818 }
819 }
820 }
821 }, null, null, 100 );
822
823 editor.on( 'key', function( evt ) {
824 if ( editor.mode != 'wysiwyg' )
825 return;
826
827 var sel = editor.getSelection();
828 if ( !sel.isFake )
829 return;
830
831 var handler = fakeSelectionDefaultKeystrokeHandlers[ evt.data.keyCode ];
832 if ( handler )
833 return handler( { editor: editor, selected: sel.getSelectedElement(), selection: sel, keyEvent: evt } );
834 } );
835
836 function clearSelection() {
837 var sel = editor.getSelection();
838 sel && sel.removeAllRanges();
839 }
840 } );
841
842 CKEDITOR.on( 'instanceReady', function( evt ) {
843 var editor = evt.editor,
844 fillingCharBefore,
845 selectionBookmark;
846
847 // On WebKit only, we need a special "filling" char on some situations
848 // (#1272). Here we set the events that should invalidate that char.
849 if ( CKEDITOR.env.webkit ) {
850 editor.on( 'selectionChange', function() {
851 checkFillingChar( editor.editable() );
852 }, null, null, -1 );
853 editor.on( 'beforeSetMode', function() {
854 removeFillingChar( editor.editable() );
855 }, null, null, -1 );
856
857 editor.on( 'beforeUndoImage', beforeData );
858 editor.on( 'afterUndoImage', afterData );
859 editor.on( 'beforeGetData', beforeData, null, null, 0 );
860 editor.on( 'getData', afterData );
861 }
862
863 function beforeData() {
864 var editable = editor.editable();
865 if ( !editable )
866 return;
867
868 var fillingChar = getFillingChar( editable );
869
870 if ( fillingChar ) {
871 // If the selection's focus or anchor is located in the filling char's text node,
872 // we need to restore the selection in afterData, because it will be lost
873 // when setting text. Selection's direction must be preserved.
874 // (#7437, #12489, #12491 comment:3)
875 var sel = editor.document.$.getSelection();
876 if ( sel.type != 'None' && ( sel.anchorNode == fillingChar.$ || sel.focusNode == fillingChar.$ ) )
877 selectionBookmark = createNativeSelectionBookmark( sel );
878
879 fillingCharBefore = fillingChar.getText();
880 fillingChar.setText( replaceFillingChar( fillingCharBefore ) );
881 }
882 }
883
884 function afterData() {
885 var editable = editor.editable();
886 if ( !editable )
887 return;
888
889 var fillingChar = getFillingChar( editable );
890
891 if ( fillingChar ) {
892 fillingChar.setText( fillingCharBefore );
893
894 if ( selectionBookmark ) {
895 moveNativeSelectionToBookmark( editor.document.$, selectionBookmark );
896 selectionBookmark = null;
897 }
898 }
899 }
900 } );
901
902 /**
903 * Check the selection change in editor and potentially fires
904 * the {@link CKEDITOR.editor#event-selectionChange} event.
905 *
906 * @method
907 * @member CKEDITOR.editor
908 * @param {Boolean} [checkNow=false] Force the check to happen immediately
909 * instead of coming with a timeout delay (default).
910 */
911 CKEDITOR.editor.prototype.selectionChange = function( checkNow ) {
912 ( checkNow ? checkSelectionChange : checkSelectionChangeTimeout ).call( this );
913 };
914
915 /**
916 * Retrieve the editor selection in scope of editable element.
917 *
918 * **Note:** Since the native browser selection provides only one single
919 * selection at a time per document, so if editor's editable element has lost focus,
920 * this method will return a null value unless the {@link CKEDITOR.editor#lockSelection}
921 * has been called beforehand so the saved selection is retrieved.
922 *
923 * var selection = CKEDITOR.instances.editor1.getSelection();
924 * alert( selection.getType() );
925 *
926 * @method
927 * @member CKEDITOR.editor
928 * @param {Boolean} forceRealSelection Return real selection, instead of saved or fake one.
929 * @returns {CKEDITOR.dom.selection} A selection object or null if not available for the moment.
930 */
931 CKEDITOR.editor.prototype.getSelection = function( forceRealSelection ) {
932
933 // Check if there exists a locked or fake selection.
934 if ( ( this._.savedSelection || this._.fakeSelection ) && !forceRealSelection )
935 return this._.savedSelection || this._.fakeSelection;
936
937 // Editable element might be absent or editor might not be in a wysiwyg mode.
938 var editable = this.editable();
939 return editable && this.mode == 'wysiwyg' ? new CKEDITOR.dom.selection( editable ) : null;
940 };
941
942 /**
943 * Locks the selection made in the editor in order to make it possible to
944 * manipulate it without browser interference. A locked selection is
945 * cached and remains unchanged until it is released with the
946 * {@link CKEDITOR.editor#unlockSelection} method.
947 *
948 * @method
949 * @member CKEDITOR.editor
950 * @param {CKEDITOR.dom.selection} [sel] Specify the selection to be locked.
951 * @returns {Boolean} `true` if selection was locked.
952 */
953 CKEDITOR.editor.prototype.lockSelection = function( sel ) {
954 sel = sel || this.getSelection( 1 );
955 if ( sel.getType() != CKEDITOR.SELECTION_NONE ) {
956 !sel.isLocked && sel.lock();
957 this._.savedSelection = sel;
958 return true;
959 }
960 return false;
961 };
962
963 /**
964 * Unlocks the selection made in the editor and locked with the
965 * {@link CKEDITOR.editor#unlockSelection} method. An unlocked selection
966 * is no longer cached and can be changed.
967 *
968 * @method
969 * @member CKEDITOR.editor
970 * @param {Boolean} [restore] If set to `true`, the selection is
971 * restored back to the selection saved earlier by using the
972 * {@link CKEDITOR.dom.selection#lock} method.
973 */
974 CKEDITOR.editor.prototype.unlockSelection = function( restore ) {
975 var sel = this._.savedSelection;
976 if ( sel ) {
977 sel.unlock( restore );
978 delete this._.savedSelection;
979 return true;
980 }
981
982 return false;
983 };
984
985 /**
986 * @method
987 * @member CKEDITOR.editor
988 * @todo
989 */
990 CKEDITOR.editor.prototype.forceNextSelectionCheck = function() {
991 delete this._.selectionPreviousPath;
992 };
993
994 /**
995 * Gets the current selection in context of the document's body element.
996 *
997 * var selection = CKEDITOR.instances.editor1.document.getSelection();
998 * alert( selection.getType() );
999 *
1000 * @method
1001 * @member CKEDITOR.dom.document
1002 * @returns {CKEDITOR.dom.selection} A selection object.
1003 */
1004 CKEDITOR.dom.document.prototype.getSelection = function() {
1005 return new CKEDITOR.dom.selection( this );
1006 };
1007
1008 /**
1009 * Select this range as the only one with {@link CKEDITOR.dom.selection#selectRanges}.
1010 *
1011 * @method
1012 * @returns {CKEDITOR.dom.selection}
1013 * @member CKEDITOR.dom.range
1014 */
1015 CKEDITOR.dom.range.prototype.select = function() {
1016 var sel = this.root instanceof CKEDITOR.editable ? this.root.editor.getSelection() : new CKEDITOR.dom.selection( this.root );
1017
1018 sel.selectRanges( [ this ] );
1019
1020 return sel;
1021 };
1022
1023 /**
1024 * No selection.
1025 *
1026 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE )
1027 * alert( 'Nothing is selected' );
1028 *
1029 * @readonly
1030 * @property {Number} [=1]
1031 * @member CKEDITOR
1032 */
1033 CKEDITOR.SELECTION_NONE = 1;
1034
1035 /**
1036 * A text or a collapsed selection.
1037 *
1038 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
1039 * alert( 'A text is selected' );
1040 *
1041 * @readonly
1042 * @property {Number} [=2]
1043 * @member CKEDITOR
1044 */
1045 CKEDITOR.SELECTION_TEXT = 2;
1046
1047 /**
1048 * Element selection.
1049 *
1050 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT )
1051 * alert( 'An element is selected' );
1052 *
1053 * @readonly
1054 * @property {Number} [=3]
1055 * @member CKEDITOR
1056 */
1057 CKEDITOR.SELECTION_ELEMENT = 3;
1058
1059 var isMSSelection = typeof window.getSelection != 'function',
1060 nextRev = 1;
1061
1062 /**
1063 * Manipulates the selection within a DOM element. If the current browser selection
1064 * spans outside of the element, an empty selection object is returned.
1065 *
1066 * Despite the fact that selection's constructor allows to create selection instances,
1067 * usually it's better to get selection from the editor instance:
1068 *
1069 * var sel = editor.getSelection();
1070 *
1071 * See {@link CKEDITOR.editor#getSelection}.
1072 *
1073 * @class
1074 * @constructor Creates a selection class instance.
1075 *
1076 * // Selection scoped in document.
1077 * var sel = new CKEDITOR.dom.selection( CKEDITOR.document );
1078 *
1079 * // Selection scoped in element with 'editable' id.
1080 * var sel = new CKEDITOR.dom.selection( CKEDITOR.document.getById( 'editable' ) );
1081 *
1082 * // Cloning selection.
1083 * var clone = new CKEDITOR.dom.selection( sel );
1084 *
1085 * @param {CKEDITOR.dom.document/CKEDITOR.dom.element/CKEDITOR.dom.selection} target
1086 * The DOM document/element that the DOM selection is restrained to. Only selection which spans
1087 * within the target element is considered as valid.
1088 *
1089 * If {@link CKEDITOR.dom.selection} is passed, then its clone will be created.
1090 */
1091 CKEDITOR.dom.selection = function( target ) {
1092 // Target is a selection - clone it.
1093 if ( target instanceof CKEDITOR.dom.selection ) {
1094 var selection = target;
1095 target = target.root;
1096 }
1097
1098 var isElement = target instanceof CKEDITOR.dom.element,
1099 root;
1100
1101 this.rev = selection ? selection.rev : nextRev++;
1102 this.document = target instanceof CKEDITOR.dom.document ? target : target.getDocument();
1103 this.root = root = isElement ? target : this.document.getBody();
1104 this.isLocked = 0;
1105 this._ = {
1106 cache: {}
1107 };
1108
1109 // Clone selection.
1110 if ( selection ) {
1111 CKEDITOR.tools.extend( this._.cache, selection._.cache );
1112 this.isFake = selection.isFake;
1113 this.isLocked = selection.isLocked;
1114 return this;
1115 }
1116
1117 // Check whether browser focus is really inside of the editable element.
1118
1119 var nativeSel = this.getNative(),
1120 rangeParent,
1121 range;
1122
1123 if ( nativeSel ) {
1124 if ( nativeSel.getRangeAt ) {
1125 range = nativeSel.rangeCount && nativeSel.getRangeAt( 0 );
1126 rangeParent = range && new CKEDITOR.dom.node( range.commonAncestorContainer );
1127 }
1128 // For old IEs.
1129 else {
1130 // Sometimes, mostly when selection is close to the table or hr,
1131 // IE throws "Unspecified error".
1132 try {
1133 range = nativeSel.createRange();
1134 } catch ( err ) {}
1135 rangeParent = range && CKEDITOR.dom.element.get( range.item && range.item( 0 ) || range.parentElement() );
1136 }
1137 }
1138
1139 // Selection out of concerned range, empty the selection.
1140 // TODO check whether this condition cannot be reverted to its old
1141 // form (commented out) after we closed #10438.
1142 //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) {
1143 if ( !(
1144 rangeParent &&
1145 ( rangeParent.type == CKEDITOR.NODE_ELEMENT || rangeParent.type == CKEDITOR.NODE_TEXT ) &&
1146 ( this.root.equals( rangeParent ) || this.root.contains( rangeParent ) )
1147 ) ) {
1148
1149 this._.cache.type = CKEDITOR.SELECTION_NONE;
1150 this._.cache.startElement = null;
1151 this._.cache.selectedElement = null;
1152 this._.cache.selectedText = '';
1153 this._.cache.ranges = new CKEDITOR.dom.rangeList();
1154 }
1155
1156 return this;
1157 };
1158
1159 var styleObjectElements = { img: 1, hr: 1, li: 1, table: 1, tr: 1, td: 1, th: 1, embed: 1, object: 1, ol: 1, ul: 1,
1160 a: 1, input: 1, form: 1, select: 1, textarea: 1, button: 1, fieldset: 1, thead: 1, tfoot: 1 };
1161
1162 CKEDITOR.dom.selection.prototype = {
1163 /**
1164 * Gets the native selection object from the browser.
1165 *
1166 * var selection = editor.getSelection().getNative();
1167 *
1168 * @returns {Object} The native browser selection object.
1169 */
1170 getNative: function() {
1171 if ( this._.cache.nativeSel !== undefined )
1172 return this._.cache.nativeSel;
1173
1174 return ( this._.cache.nativeSel = isMSSelection ? this.document.$.selection : this.document.getWindow().$.getSelection() );
1175 },
1176
1177 /**
1178 * Gets the type of the current selection. The following values are
1179 * available:
1180 *
1181 * * {@link CKEDITOR#SELECTION_NONE} (1): No selection.
1182 * * {@link CKEDITOR#SELECTION_TEXT} (2): A text or a collapsed selection is selected.
1183 * * {@link CKEDITOR#SELECTION_ELEMENT} (3): An element is selected.
1184 *
1185 * Example:
1186 *
1187 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
1188 * alert( 'A text is selected' );
1189 *
1190 * @method
1191 * @returns {Number} One of the following constant values: {@link CKEDITOR#SELECTION_NONE},
1192 * {@link CKEDITOR#SELECTION_TEXT} or {@link CKEDITOR#SELECTION_ELEMENT}.
1193 */
1194 getType: isMSSelection ?
1195 function() {
1196 var cache = this._.cache;
1197 if ( cache.type )
1198 return cache.type;
1199
1200 var type = CKEDITOR.SELECTION_NONE;
1201
1202 try {
1203 var sel = this.getNative(),
1204 ieType = sel.type;
1205
1206 if ( ieType == 'Text' )
1207 type = CKEDITOR.SELECTION_TEXT;
1208
1209 if ( ieType == 'Control' )
1210 type = CKEDITOR.SELECTION_ELEMENT;
1211
1212 // It is possible that we can still get a text range
1213 // object even when type == 'None' is returned by IE.
1214 // So we'd better check the object returned by
1215 // createRange() rather than by looking at the type.
1216 if ( sel.createRange().parentElement() )
1217 type = CKEDITOR.SELECTION_TEXT;
1218 } catch ( e ) {}
1219
1220 return ( cache.type = type );
1221 } : function() {
1222 var cache = this._.cache;
1223 if ( cache.type )
1224 return cache.type;
1225
1226 var type = CKEDITOR.SELECTION_TEXT;
1227
1228 var sel = this.getNative();
1229
1230 if ( !( sel && sel.rangeCount ) )
1231 type = CKEDITOR.SELECTION_NONE;
1232 else if ( sel.rangeCount == 1 ) {
1233 // Check if the actual selection is a control (IMG,
1234 // TABLE, HR, etc...).
1235
1236 var range = sel.getRangeAt( 0 ),
1237 startContainer = range.startContainer;
1238
1239 if ( startContainer == range.endContainer && startContainer.nodeType == 1 &&
1240 ( range.endOffset - range.startOffset ) == 1 &&
1241 styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] ) {
1242 type = CKEDITOR.SELECTION_ELEMENT;
1243 }
1244
1245 }
1246
1247 return ( cache.type = type );
1248 },
1249
1250 /**
1251 * Retrieves the {@link CKEDITOR.dom.range} instances that represent the current selection.
1252 *
1253 * Note: Some browsers return multiple ranges even for a continuous selection. Firefox, for example, returns
1254 * one range for each table cell when one or more table rows are selected.
1255 *
1256 * var ranges = selection.getRanges();
1257 * alert( ranges.length );
1258 *
1259 * @method
1260 * @param {Boolean} [onlyEditables] If set to `true`, this function retrives editable ranges only.
1261 * @returns {Array} Range instances that represent the current selection.
1262 */
1263 getRanges: ( function() {
1264 var func = isMSSelection ? ( function() {
1265 function getNodeIndex( node ) {
1266 return new CKEDITOR.dom.node( node ).getIndex();
1267 }
1268
1269 // Finds the container and offset for a specific boundary
1270 // of an IE range.
1271 var getBoundaryInformation = function( range, start ) {
1272 // Creates a collapsed range at the requested boundary.
1273 range = range.duplicate();
1274 range.collapse( start );
1275
1276 // Gets the element that encloses the range entirely.
1277 var parent = range.parentElement();
1278
1279 // Empty parent element, e.g. <i>^</i>
1280 if ( !parent.hasChildNodes() )
1281 return { container: parent, offset: 0 };
1282
1283 var siblings = parent.children,
1284 child, sibling,
1285 testRange = range.duplicate(),
1286 startIndex = 0,
1287 endIndex = siblings.length - 1,
1288 index = -1,
1289 position, distance, container;
1290
1291 // Binary search over all element childs to test the range to see whether
1292 // range is right on the boundary of one element.
1293 while ( startIndex <= endIndex ) {
1294 index = Math.floor( ( startIndex + endIndex ) / 2 );
1295 child = siblings[ index ];
1296 testRange.moveToElementText( child );
1297 position = testRange.compareEndPoints( 'StartToStart', range );
1298
1299 if ( position > 0 )
1300 endIndex = index - 1;
1301 else if ( position < 0 )
1302 startIndex = index + 1;
1303 else
1304 return { container: parent, offset: getNodeIndex( child ) };
1305 }
1306
1307 // All childs are text nodes,
1308 // or to the right hand of test range are all text nodes. (#6992)
1309 if ( index == -1 || index == siblings.length - 1 && position < 0 ) {
1310 // Adapt test range to embrace the entire parent contents.
1311 testRange.moveToElementText( parent );
1312 testRange.setEndPoint( 'StartToStart', range );
1313
1314 // IE report line break as CRLF with range.text but
1315 // only LF with textnode.nodeValue, normalize them to avoid
1316 // breaking character counting logic below. (#3949)
1317 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1318
1319 siblings = parent.childNodes;
1320
1321 // Actual range anchor right beside test range at the boundary of text node.
1322 if ( !distance ) {
1323 child = siblings[ siblings.length - 1 ];
1324
1325 if ( child.nodeType != CKEDITOR.NODE_TEXT )
1326 return { container: parent, offset: siblings.length };
1327 else
1328 return { container: child, offset: child.nodeValue.length };
1329 }
1330
1331 // Start the measuring until distance overflows, meanwhile count the text nodes.
1332 var i = siblings.length;
1333 while ( distance > 0 && i > 0 ) {
1334 sibling = siblings[ --i ];
1335 if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) {
1336 container = sibling;
1337 distance -= sibling.nodeValue.length;
1338 }
1339 }
1340
1341 return { container: container, offset: -distance };
1342 }
1343 // Test range was one offset beyond OR behind the anchored text node.
1344 else {
1345 // Adapt one side of test range to the actual range
1346 // for measuring the offset between them.
1347 testRange.collapse( position > 0 ? true : false );
1348 testRange.setEndPoint( position > 0 ? 'StartToStart' : 'EndToStart', range );
1349
1350 // IE report line break as CRLF with range.text but
1351 // only LF with textnode.nodeValue, normalize them to avoid
1352 // breaking character counting logic below. (#3949)
1353 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1354
1355 // Actual range anchor right beside test range at the inner boundary of text node.
1356 if ( !distance )
1357 return { container: parent, offset: getNodeIndex( child ) + ( position > 0 ? 0 : 1 ) };
1358
1359 // Start the measuring until distance overflows, meanwhile count the text nodes.
1360 while ( distance > 0 ) {
1361 try {
1362 sibling = child[ position > 0 ? 'previousSibling' : 'nextSibling' ];
1363 if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) {
1364 distance -= sibling.nodeValue.length;
1365 container = sibling;
1366 }
1367 child = sibling;
1368 }
1369 // Measurement in IE could be somtimes wrong because of <select> element. (#4611)
1370 catch ( e ) {
1371 return { container: parent, offset: getNodeIndex( child ) };
1372 }
1373 }
1374
1375 return { container: container, offset: position > 0 ? -distance : container.nodeValue.length + distance };
1376 }
1377 };
1378
1379 return function() {
1380 // IE doesn't have range support (in the W3C way), so we
1381 // need to do some magic to transform selections into
1382 // CKEDITOR.dom.range instances.
1383
1384 var sel = this.getNative(),
1385 nativeRange = sel && sel.createRange(),
1386 type = this.getType(),
1387 range;
1388
1389 if ( !sel )
1390 return [];
1391
1392 if ( type == CKEDITOR.SELECTION_TEXT ) {
1393 range = new CKEDITOR.dom.range( this.root );
1394
1395 var boundaryInfo = getBoundaryInformation( nativeRange, true );
1396 range.setStart( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
1397
1398 boundaryInfo = getBoundaryInformation( nativeRange );
1399 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
1400
1401 // Correct an invalid IE range case on empty list item. (#5850)
1402 if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING && range.endOffset <= range.startContainer.getIndex() )
1403 range.collapse();
1404
1405 return [ range ];
1406 } else if ( type == CKEDITOR.SELECTION_ELEMENT ) {
1407 var retval = [];
1408
1409 for ( var i = 0; i < nativeRange.length; i++ ) {
1410 var element = nativeRange.item( i ),
1411 parentElement = element.parentNode,
1412 j = 0;
1413
1414 range = new CKEDITOR.dom.range( this.root );
1415
1416 for ( ; j < parentElement.childNodes.length && parentElement.childNodes[ j ] != element; j++ ) {
1417
1418 }
1419
1420 range.setStart( new CKEDITOR.dom.node( parentElement ), j );
1421 range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 );
1422 retval.push( range );
1423 }
1424
1425 return retval;
1426 }
1427
1428 return [];
1429 };
1430 } )() :
1431 function() {
1432 // On browsers implementing the W3C range, we simply
1433 // tranform the native ranges in CKEDITOR.dom.range
1434 // instances.
1435
1436 var ranges = [],
1437 range,
1438 sel = this.getNative();
1439
1440 if ( !sel )
1441 return ranges;
1442
1443 for ( var i = 0; i < sel.rangeCount; i++ ) {
1444 var nativeRange = sel.getRangeAt( i );
1445
1446 range = new CKEDITOR.dom.range( this.root );
1447
1448 range.setStart( new CKEDITOR.dom.node( nativeRange.startContainer ), nativeRange.startOffset );
1449 range.setEnd( new CKEDITOR.dom.node( nativeRange.endContainer ), nativeRange.endOffset );
1450 ranges.push( range );
1451 }
1452 return ranges;
1453 };
1454
1455 return function( onlyEditables ) {
1456 var cache = this._.cache,
1457 ranges = cache.ranges;
1458
1459 if ( !ranges )
1460 cache.ranges = ranges = new CKEDITOR.dom.rangeList( func.call( this ) );
1461
1462 if ( !onlyEditables )
1463 return ranges;
1464
1465 // Split range into multiple by read-only nodes.
1466 // Clone ranges array to avoid changing cached ranges (#11493).
1467 return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) );
1468 };
1469 } )(),
1470
1471 /**
1472 * Gets the DOM element in which the selection starts.
1473 *
1474 * var element = editor.getSelection().getStartElement();
1475 * alert( element.getName() );
1476 *
1477 * @returns {CKEDITOR.dom.element} The element at the beginning of the selection.
1478 */
1479 getStartElement: function() {
1480 var cache = this._.cache;
1481 if ( cache.startElement !== undefined )
1482 return cache.startElement;
1483
1484 var node;
1485
1486 switch ( this.getType() ) {
1487 case CKEDITOR.SELECTION_ELEMENT:
1488 return this.getSelectedElement();
1489
1490 case CKEDITOR.SELECTION_TEXT:
1491
1492 var range = this.getRanges()[ 0 ];
1493
1494 if ( range ) {
1495 if ( !range.collapsed ) {
1496 range.optimize();
1497
1498 // Decrease the range content to exclude particial
1499 // selected node on the start which doesn't have
1500 // visual impact. ( #3231 )
1501 while ( 1 ) {
1502 var startContainer = range.startContainer,
1503 startOffset = range.startOffset;
1504 // Limit the fix only to non-block elements.(#3950)
1505 if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() )
1506 range.setStartAfter( startContainer );
1507 else
1508 break;
1509 }
1510
1511 node = range.startContainer;
1512
1513 if ( node.type != CKEDITOR.NODE_ELEMENT )
1514 return node.getParent();
1515
1516 node = node.getChild( range.startOffset );
1517
1518 if ( !node || node.type != CKEDITOR.NODE_ELEMENT )
1519 node = range.startContainer;
1520 else {
1521 var child = node.getFirst();
1522 while ( child && child.type == CKEDITOR.NODE_ELEMENT ) {
1523 node = child;
1524 child = child.getFirst();
1525 }
1526 }
1527 } else {
1528 node = range.startContainer;
1529 if ( node.type != CKEDITOR.NODE_ELEMENT )
1530 node = node.getParent();
1531 }
1532
1533 node = node.$;
1534 }
1535 }
1536
1537 return cache.startElement = ( node ? new CKEDITOR.dom.element( node ) : null );
1538 },
1539
1540 /**
1541 * Gets the currently selected element.
1542 *
1543 * var element = editor.getSelection().getSelectedElement();
1544 * alert( element.getName() );
1545 *
1546 * @returns {CKEDITOR.dom.element} The selected element. Null if no
1547 * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}.
1548 */
1549 getSelectedElement: function() {
1550 var cache = this._.cache;
1551 if ( cache.selectedElement !== undefined )
1552 return cache.selectedElement;
1553
1554 var self = this;
1555
1556 var node = CKEDITOR.tools.tryThese(
1557 // Is it native IE control type selection?
1558 function() {
1559 return self.getNative().createRange().item( 0 );
1560 },
1561 // Figure it out by checking if there's a single enclosed
1562 // node of the range.
1563 function() {
1564 var range = self.getRanges()[ 0 ].clone(),
1565 enclosed, selected;
1566
1567 // Check first any enclosed element, e.g. <ul>[<li><a href="#">item</a></li>]</ul>
1568 for ( var i = 2; i && !( ( enclosed = range.getEnclosedNode() ) && ( enclosed.type == CKEDITOR.NODE_ELEMENT ) && styleObjectElements[ enclosed.getName() ] && ( selected = enclosed ) ); i-- ) {
1569 // Then check any deep wrapped element, e.g. [<b><i><img /></i></b>]
1570 range.shrink( CKEDITOR.SHRINK_ELEMENT );
1571 }
1572
1573 return selected && selected.$;
1574 }
1575 );
1576
1577 return cache.selectedElement = ( node ? new CKEDITOR.dom.element( node ) : null );
1578 },
1579
1580 /**
1581 * Retrieves the text contained within the range. An empty string is returned for non-text selection.
1582 *
1583 * var text = editor.getSelection().getSelectedText();
1584 * alert( text );
1585 *
1586 * @since 3.6.1
1587 * @returns {String} A string of text within the current selection.
1588 */
1589 getSelectedText: function() {
1590 var cache = this._.cache;
1591 if ( cache.selectedText !== undefined )
1592 return cache.selectedText;
1593
1594 var nativeSel = this.getNative(),
1595 text = isMSSelection ? nativeSel.type == 'Control' ? '' : nativeSel.createRange().text : nativeSel.toString();
1596
1597 return ( cache.selectedText = text );
1598 },
1599
1600 /**
1601 * Locks the selection made in the editor in order to make it possible to
1602 * manipulate it without browser interference. A locked selection is
1603 * cached and remains unchanged until it is released with the {@link #unlock} method.
1604 *
1605 * editor.getSelection().lock();
1606 */
1607 lock: function() {
1608 // Call all cacheable function.
1609 this.getRanges();
1610 this.getStartElement();
1611 this.getSelectedElement();
1612 this.getSelectedText();
1613
1614 // The native selection is not available when locked.
1615 this._.cache.nativeSel = null;
1616
1617 this.isLocked = 1;
1618 },
1619
1620 /**
1621 * @todo
1622 */
1623 unlock: function( restore ) {
1624 if ( !this.isLocked )
1625 return;
1626
1627 if ( restore ) {
1628 var selectedElement = this.getSelectedElement(),
1629 ranges = !selectedElement && this.getRanges(),
1630 faked = this.isFake;
1631 }
1632
1633 this.isLocked = 0;
1634 this.reset();
1635
1636 if ( restore ) {
1637 // Saved selection may be outdated (e.g. anchored in offline nodes).
1638 // Avoid getting broken by such.
1639 var common = selectedElement || ranges[ 0 ] && ranges[ 0 ].getCommonAncestor();
1640 if ( !( common && common.getAscendant( 'body', 1 ) ) )
1641 return;
1642
1643 if ( faked )
1644 this.fake( selectedElement );
1645 else if ( selectedElement )
1646 this.selectElement( selectedElement );
1647 else
1648 this.selectRanges( ranges );
1649 }
1650 },
1651
1652 /**
1653 * Clears the selection cache.
1654 *
1655 * editor.getSelection().reset();
1656 */
1657 reset: function() {
1658 this._.cache = {};
1659 this.isFake = 0;
1660
1661 var editor = this.root.editor;
1662
1663 // Invalidate any fake selection available in the editor.
1664 if ( editor && editor._.fakeSelection ) {
1665 // Test whether this selection is the one that was
1666 // faked or its clone.
1667 if ( this.rev == editor._.fakeSelection.rev ) {
1668 delete editor._.fakeSelection;
1669
1670 removeHiddenSelectionContainer( editor );
1671 }
1672 else {
1673 CKEDITOR.warn( 'selection-fake-reset' );
1674 }
1675 }
1676
1677 this.rev = nextRev++;
1678 },
1679
1680 /**
1681 * Makes the current selection of type {@link CKEDITOR#SELECTION_ELEMENT} by enclosing the specified element.
1682 *
1683 * var element = editor.document.getById( 'sampleElement' );
1684 * editor.getSelection().selectElement( element );
1685 *
1686 * @param {CKEDITOR.dom.element} element The element to enclose in the selection.
1687 */
1688 selectElement: function( element ) {
1689 var range = new CKEDITOR.dom.range( this.root );
1690 range.setStartBefore( element );
1691 range.setEndAfter( element );
1692 this.selectRanges( [ range ] );
1693 },
1694
1695 /**
1696 * Clears the original selection and adds the specified ranges to the document selection.
1697 *
1698 * // Move selection to the end of the editable element.
1699 * var range = editor.createRange();
1700 * range.moveToPosition( range.root, CKEDITOR.POSITION_BEFORE_END );
1701 * editor.getSelection().selectRanges( [ ranges ] );
1702 *
1703 * @param {Array} ranges An array of {@link CKEDITOR.dom.range} instances
1704 * representing ranges to be added to the document.
1705 */
1706 selectRanges: function( ranges ) {
1707 var editor = this.root.editor,
1708 hadHiddenSelectionContainer = editor && editor._.hiddenSelectionContainer;
1709
1710 this.reset();
1711
1712 // Check if there's a hiddenSelectionContainer in editable at some index.
1713 // Some ranges may be anchored after the hiddenSelectionContainer and,
1714 // once the container is removed while resetting the selection, they
1715 // may need new endOffset (one element less within the range) (#11021 #11393).
1716 if ( hadHiddenSelectionContainer )
1717 fixRangesAfterHiddenSelectionContainer( ranges, this.root );
1718
1719 if ( !ranges.length )
1720 return;
1721
1722 // Refresh the locked selection.
1723 if ( this.isLocked ) {
1724 // making a new DOM selection will force the focus on editable in certain situation,
1725 // we have to save the currently focused element for later recovery.
1726 var focused = CKEDITOR.document.getActive();
1727 this.unlock();
1728 this.selectRanges( ranges );
1729 this.lock();
1730 // Return to the previously focused element.
1731 focused && !focused.equals( this.root ) && focused.focus();
1732 return;
1733 }
1734
1735 // Handle special case - automatic fake selection on non-editable elements.
1736 var receiver = getNonEditableFakeSelectionReceiver( ranges );
1737
1738 if ( receiver ) {
1739 this.fake( receiver );
1740 return;
1741 }
1742
1743 if ( isMSSelection ) {
1744 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
1745 fillerTextRegex = /\ufeff|\u00a0/,
1746 nonCells = { table: 1, tbody: 1, tr: 1 };
1747
1748 if ( ranges.length > 1 ) {
1749 // IE doesn't accept multiple ranges selection, so we join all into one.
1750 var last = ranges[ ranges.length - 1 ];
1751 ranges[ 0 ].setEnd( last.endContainer, last.endOffset );
1752 }
1753
1754 var range = ranges[ 0 ];
1755 var collapsed = range.collapsed,
1756 isStartMarkerAlone, dummySpan, ieRange;
1757
1758 // Try to make a object selection, be careful with selecting phase element in IE
1759 // will breaks the selection in non-framed environment.
1760 var selected = range.getEnclosedNode();
1761 if ( selected && selected.type == CKEDITOR.NODE_ELEMENT && selected.getName() in styleObjectElements &&
1762 !( selected.is( 'a' ) && selected.getText() ) ) {
1763 try {
1764 ieRange = selected.$.createControlRange();
1765 ieRange.addElement( selected.$ );
1766 ieRange.select();
1767 return;
1768 } catch ( er ) {}
1769 }
1770
1771 // IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g.
1772 // <table><tbody><tr>[<td>cell</b></td>... => <table><tbody><tr><td>[cell</td>...
1773 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells ||
1774 range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) {
1775 range.shrink( CKEDITOR.NODE_ELEMENT, true );
1776 // The range might get collapsed (#7975). Update cached variable.
1777 collapsed = range.collapsed;
1778 }
1779
1780 var bookmark = range.createBookmark();
1781
1782 // Create marker tags for the start and end boundaries.
1783 var startNode = bookmark.startNode;
1784
1785 var endNode;
1786 if ( !collapsed )
1787 endNode = bookmark.endNode;
1788
1789 // Create the main range which will be used for the selection.
1790 ieRange = range.document.$.body.createTextRange();
1791
1792 // Position the range at the start boundary.
1793 ieRange.moveToElementText( startNode.$ );
1794 ieRange.moveStart( 'character', 1 );
1795
1796 if ( endNode ) {
1797 // Create a tool range for the end.
1798 var ieRangeEnd = range.document.$.body.createTextRange();
1799
1800 // Position the tool range at the end.
1801 ieRangeEnd.moveToElementText( endNode.$ );
1802
1803 // Move the end boundary of the main range to match the tool range.
1804 ieRange.setEndPoint( 'EndToEnd', ieRangeEnd );
1805 ieRange.moveEnd( 'character', -1 );
1806 } else {
1807 // The isStartMarkerAlone logic comes from V2. It guarantees that the lines
1808 // will expand and that the cursor will be blinking on the right place.
1809 // Actually, we are using this flag just to avoid using this hack in all
1810 // situations, but just on those needed.
1811 var next = startNode.getNext( notWhitespaces );
1812 var inPre = startNode.hasAscendant( 'pre' );
1813 isStartMarkerAlone = ( !( next && next.getText && next.getText().match( fillerTextRegex ) ) && // already a filler there?
1814 ( inPre || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) );
1815
1816 // Append a temporary <span>&#65279;</span> before the selection.
1817 // This is needed to avoid IE destroying selections inside empty
1818 // inline elements, like <b></b> (#253).
1819 // It is also needed when placing the selection right after an inline
1820 // element to avoid the selection moving inside of it.
1821 dummySpan = range.document.createElement( 'span' );
1822 dummySpan.setHtml( '&#65279;' ); // Zero Width No-Break Space (U+FEFF). See #1359.
1823 dummySpan.insertBefore( startNode );
1824
1825 if ( isStartMarkerAlone ) {
1826 // To expand empty blocks or line spaces after <br>, we need
1827 // instead to have any char, which will be later deleted using the
1828 // selection.
1829 // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359)
1830 range.document.createText( '\ufeff' ).insertBefore( startNode );
1831 }
1832 }
1833
1834 // Remove the markers (reset the position, because of the changes in the DOM tree).
1835 range.setStartBefore( startNode );
1836 startNode.remove();
1837
1838 if ( collapsed ) {
1839 if ( isStartMarkerAlone ) {
1840 // Move the selection start to include the temporary \ufeff.
1841 ieRange.moveStart( 'character', -1 );
1842
1843 ieRange.select();
1844
1845 // Remove our temporary stuff.
1846 range.document.$.selection.clear();
1847 } else {
1848 ieRange.select();
1849 }
1850
1851 range.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START );
1852 dummySpan.remove();
1853 } else {
1854 range.setEndBefore( endNode );
1855 endNode.remove();
1856 ieRange.select();
1857 }
1858 } else {
1859 var sel = this.getNative();
1860
1861 // getNative() returns null if iframe is "display:none" in FF. (#6577)
1862 if ( !sel )
1863 return;
1864
1865 this.removeAllRanges();
1866
1867 for ( var i = 0; i < ranges.length; i++ ) {
1868 // Joining sequential ranges introduced by
1869 // readonly elements protection.
1870 if ( i < ranges.length - 1 ) {
1871 var left = ranges[ i ],
1872 right = ranges[ i + 1 ],
1873 between = left.clone();
1874 between.setStart( left.endContainer, left.endOffset );
1875 between.setEnd( right.startContainer, right.startOffset );
1876
1877 // Don't confused by Firefox adjancent multi-ranges
1878 // introduced by table cells selection.
1879 if ( !between.collapsed ) {
1880 between.shrink( CKEDITOR.NODE_ELEMENT, true );
1881 var ancestor = between.getCommonAncestor(),
1882 enclosed = between.getEnclosedNode();
1883
1884 // The following cases has to be considered:
1885 // 1. <span contenteditable="false">[placeholder]</span>
1886 // 2. <input contenteditable="false" type="radio"/> (#6621)
1887 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) {
1888 right.setStart( left.startContainer, left.startOffset );
1889 ranges.splice( i--, 1 );
1890 continue;
1891 }
1892 }
1893 }
1894
1895 range = ranges[ i ];
1896
1897 var nativeRange = this.document.$.createRange();
1898
1899 if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) {
1900 // Append a zero-width space so WebKit will not try to
1901 // move the selection by itself (#1272).
1902 var fillingChar = createFillingChar( this.root );
1903 range.insertNode( fillingChar );
1904
1905 next = fillingChar.getNext();
1906
1907 // If the filling char is followed by a <br>, whithout
1908 // having something before it, it'll not blink.
1909 // Let's remove it in this case.
1910 if ( next && !fillingChar.getPrevious() && next.type == CKEDITOR.NODE_ELEMENT && next.getName() == 'br' ) {
1911 removeFillingChar( this.root );
1912 range.moveToPosition( next, CKEDITOR.POSITION_BEFORE_START );
1913 } else {
1914 range.moveToPosition( fillingChar, CKEDITOR.POSITION_AFTER_END );
1915 }
1916 }
1917
1918 nativeRange.setStart( range.startContainer.$, range.startOffset );
1919
1920 try {
1921 nativeRange.setEnd( range.endContainer.$, range.endOffset );
1922 } catch ( e ) {
1923 // There is a bug in Firefox implementation (it would be too easy
1924 // otherwise). The new start can't be after the end (W3C says it can).
1925 // So, let's create a new range and collapse it to the desired point.
1926 if ( e.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 ) {
1927 range.collapse( 1 );
1928 nativeRange.setEnd( range.endContainer.$, range.endOffset );
1929 } else {
1930 throw e;
1931 }
1932 }
1933
1934 // Select the range.
1935 sel.addRange( nativeRange );
1936 }
1937 }
1938
1939 this.reset();
1940
1941 // Fakes the IE DOM event "selectionchange" on editable.
1942 this.root.fire( 'selectionchange' );
1943 },
1944
1945 /**
1946 * Makes a "fake selection" of an element.
1947 *
1948 * A fake selection does not render UI artifacts over the selected
1949 * element. Additionally, the browser native selection system is not
1950 * aware of the fake selection. In practice, the native selection is
1951 * moved to a hidden place where no native selection UI artifacts are
1952 * displayed to the user.
1953 *
1954 * @param {CKEDITOR.dom.element} element The element to be "selected".
1955 */
1956 fake: function( element ) {
1957 var editor = this.root.editor;
1958
1959 // Cleanup after previous selection - e.g. remove hidden sel container.
1960 this.reset();
1961
1962 hideSelection( editor );
1963
1964 // Set this value after executing hiseSelection, because it may
1965 // cause reset() which overwrites cache.
1966 var cache = this._.cache;
1967
1968 // Caches a range than holds the element.
1969 var range = new CKEDITOR.dom.range( this.root );
1970 range.setStartBefore( element );
1971 range.setEndAfter( element );
1972 cache.ranges = new CKEDITOR.dom.rangeList( range );
1973
1974 // Put this element in the cache.
1975 cache.selectedElement = cache.startElement = element;
1976 cache.type = CKEDITOR.SELECTION_ELEMENT;
1977
1978 // Properties that will not be available when isFake.
1979 cache.selectedText = cache.nativeSel = null;
1980
1981 this.isFake = 1;
1982 this.rev = nextRev++;
1983
1984 // Save this selection, so it can be returned by editor.getSelection().
1985 editor._.fakeSelection = this;
1986
1987 // Fire selectionchange, just like a normal selection.
1988 this.root.fire( 'selectionchange' );
1989 },
1990
1991 /**
1992 * Checks whether selection is placed in hidden element.
1993 *
1994 * This method is to be used to verify whether fake selection
1995 * (see {@link #fake}) is still hidden.
1996 *
1997 * **Note:** this method should be executed on real selection - e.g.:
1998 *
1999 * editor.getSelection( true ).isHidden();
2000 *
2001 * @returns {Boolean}
2002 */
2003 isHidden: function() {
2004 var el = this.getCommonAncestor();
2005
2006 if ( el && el.type == CKEDITOR.NODE_TEXT )
2007 el = el.getParent();
2008
2009 return !!( el && el.data( 'cke-hidden-sel' ) );
2010 },
2011
2012 /**
2013 * Creates a bookmark for each range of this selection (from {@link #getRanges})
2014 * by calling the {@link CKEDITOR.dom.range#createBookmark} method,
2015 * with extra care taken to avoid interference among those ranges. The arguments
2016 * received are the same as with the underlying range method.
2017 *
2018 * var bookmarks = editor.getSelection().createBookmarks();
2019 *
2020 * @returns {Array} Array of bookmarks for each range.
2021 */
2022 createBookmarks: function( serializable ) {
2023 var bookmark = this.getRanges().createBookmarks( serializable );
2024 this.isFake && ( bookmark.isFake = 1 );
2025 return bookmark;
2026 },
2027
2028 /**
2029 * Creates a bookmark for each range of this selection (from {@link #getRanges})
2030 * by calling the {@link CKEDITOR.dom.range#createBookmark2} method,
2031 * with extra care taken to avoid interference among those ranges. The arguments
2032 * received are the same as with the underlying range method.
2033 *
2034 * var bookmarks = editor.getSelection().createBookmarks2();
2035 *
2036 * @returns {Array} Array of bookmarks for each range.
2037 */
2038 createBookmarks2: function( normalized ) {
2039 var bookmark = this.getRanges().createBookmarks2( normalized );
2040 this.isFake && ( bookmark.isFake = 1 );
2041 return bookmark;
2042 },
2043
2044 /**
2045 * Selects the virtual ranges denoted by the bookmarks by calling {@link #selectRanges}.
2046 *
2047 * var bookmarks = editor.getSelection().createBookmarks();
2048 * editor.getSelection().selectBookmarks( bookmarks );
2049 *
2050 * @param {Array} bookmarks The bookmarks representing ranges to be selected.
2051 * @returns {CKEDITOR.dom.selection} This selection object, after the ranges were selected.
2052 */
2053 selectBookmarks: function( bookmarks ) {
2054 var ranges = [],
2055 node;
2056
2057 for ( var i = 0; i < bookmarks.length; i++ ) {
2058 var range = new CKEDITOR.dom.range( this.root );
2059 range.moveToBookmark( bookmarks[ i ] );
2060 ranges.push( range );
2061 }
2062
2063 // It may happen that the content change during loading, before selection is set so bookmark leads to text node.
2064 if ( bookmarks.isFake ) {
2065 node = ranges[ 0 ].getEnclosedNode();
2066 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) {
2067 CKEDITOR.warn( 'selection-not-fake' );
2068 bookmarks.isFake = 0;
2069 }
2070 }
2071
2072 if ( bookmarks.isFake )
2073 this.fake( node );
2074 else
2075 this.selectRanges( ranges );
2076
2077 return this;
2078 },
2079
2080 /**
2081 * Retrieves the common ancestor node of the first range and the last range.
2082 *
2083 * var ancestor = editor.getSelection().getCommonAncestor();
2084 *
2085 * @returns {CKEDITOR.dom.element} The common ancestor of the selection or `null` if selection is empty.
2086 */
2087 getCommonAncestor: function() {
2088 var ranges = this.getRanges();
2089 if ( !ranges.length )
2090 return null;
2091
2092 var startNode = ranges[ 0 ].startContainer,
2093 endNode = ranges[ ranges.length - 1 ].endContainer;
2094 return startNode.getCommonAncestor( endNode );
2095 },
2096
2097 /**
2098 * Moves the scrollbar to the starting position of the current selection.
2099 *
2100 * editor.getSelection().scrollIntoView();
2101 */
2102 scrollIntoView: function() {
2103 // Scrolls the first range into view.
2104 if ( this.type != CKEDITOR.SELECTION_NONE )
2105 this.getRanges()[ 0 ].scrollIntoView();
2106 },
2107
2108 /**
2109 * Remove all the selection ranges from the document.
2110 */
2111 removeAllRanges: function() {
2112 // Don't clear selection outside this selection's root (#11500).
2113 if ( this.getType() == CKEDITOR.SELECTION_NONE )
2114 return;
2115
2116 var nativ = this.getNative();
2117
2118 try {
2119 nativ && nativ[ isMSSelection ? 'empty' : 'removeAllRanges' ]();
2120 } catch ( er ) {}
2121
2122 this.reset();
2123 }
2124 };
2125
2126} )();
2127
2128
2129/**
2130 * Fired when selection inside editor has been changed. Note that this event
2131 * is fired only when selection's start element (container of a selecion start)
2132 * changes, not on every possible selection change. Thanks to that `selectionChange`
2133 * is fired less frequently, but on every context
2134 * (the {@link CKEDITOR.editor#elementPath elements path} holding selection's start) change.
2135 *
2136 * @event selectionChange
2137 * @member CKEDITOR.editor
2138 * @param {CKEDITOR.editor} editor This editor instance.
2139 * @param data
2140 * @param {CKEDITOR.dom.selection} data.selection
2141 * @param {CKEDITOR.dom.elementPath} data.path
2142 */
2143
2144/**
2145 * Selection's revision. This value is incremented every time new
2146 * selection is created or existing one is modified.
2147 *
2148 * @since 4.3
2149 * @readonly
2150 * @property {Number} rev
2151 */
2152
2153/**
2154 * Document in which selection is anchored.
2155 *
2156 * @readonly
2157 * @property {CKEDITOR.dom.document} document
2158 */
2159
2160/**
2161 * Selection's root element.
2162 *
2163 * @readonly
2164 * @property {CKEDITOR.dom.element} root
2165 */
2166
2167/**
2168 * Whether selection is locked (cannot be modified).
2169 *
2170 * See {@link #lock} and {@link #unlock} methods.
2171 *
2172 * @readonly
2173 * @property {Boolean} isLocked
2174 */
2175
2176/**
2177 * Whether selection is a fake selection.
2178 *
2179 * See {@link #fake} method.
2180 *
2181 * @readonly
2182 * @property {Boolean} isFake
2183 */
diff --git a/sources/core/skin.js b/sources/core/skin.js
new file mode 100644
index 00000000..143a7346
--- /dev/null
+++ b/sources/core/skin.js
@@ -0,0 +1,350 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 00000000..619358b5
--- /dev/null
+++ b/sources/core/style.js
@@ -0,0 +1,2089 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8/**
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 */
17CKEDITOR.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 */
28CKEDITOR.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 */
39CKEDITOR.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 &mdash; 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}` &ndash; 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}` &ndash; 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} &ndash; Note that both methods reuse the same logic,
690 * * {@link CKEDITOR.style#checkActive checkActive} &ndash; Reuses
691 * {@link CKEDITOR.style#checkElementMatch checkElementMatch},
692 * * {@link CKEDITOR.style#toAllowedContentRules toAllowedContentRules} &ndash; 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 &nbsp;
1431 blockHtml = replace( blockHtml, /^[ \t]+|[ \t]+$/g, function( match, offset ) {
1432 if ( match.length == 1 ) // one space, preserve it
1433 return '&nbsp;';
1434 else if ( !offset ) // beginning of block
1435 return CKEDITOR.tools.repeat( '&nbsp;', match.length - 1 ) + ' ';
1436 else // end of block
1437 return ' ' + CKEDITOR.tools.repeat( '&nbsp;', match.length - 1 );
1438 } );
1439
1440 // 3. Convert \n to <BR>.
1441 // 4. Convert contiguous (i.e. non-singular) spaces or tabs to &nbsp;
1442 blockHtml = blockHtml.replace( /\n/g, '<br>' );
1443 blockHtml = blockHtml.replace( /[ \t]{2,}/g, function( match ) {
1444 return CKEDITOR.tools.repeat( '&nbsp;', 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 &nbsp; to spaces since &nbsp; is no longer needed in <PRE>.
1475 preHtml = preHtml.replace( /([ \t\n\r]+|&nbsp;)/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 */
1838CKEDITOR.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 */
1850CKEDITOR.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 */
1889CKEDITOR.stylesSet = new CKEDITOR.resourceManager( '', 'stylesSet' );
1890
1891// Backward compatibility (#5025).
1892CKEDITOR.addStylesSet = CKEDITOR.tools.bind( CKEDITOR.stylesSet.add, CKEDITOR.stylesSet );
1893CKEDITOR.loadStylesSet = function( name, url, callback ) {
1894 CKEDITOR.stylesSet.addExternal( name, url, '' );
1895 CKEDITOR.stylesSet.load( name, callback );
1896};
1897
1898CKEDITOR.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 00000000..d3fe0b90
--- /dev/null
+++ b/sources/core/template.js
@@ -0,0 +1,68 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 00000000..0cf824f9
--- /dev/null
+++ b/sources/core/tools.js
@@ -0,0 +1,1386 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 &gt; B &amp; C &lt; 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, '&amp;' ).replace( gtRegex, '&gt;' ).replace( ltRegex, '&lt;' );
379 },
380
381 /**
382 * Decodes HTML entities that browsers tend to encode when used in text nodes.
383 *
384 * alert( CKEDITOR.tools.htmlDecode( '&lt;a &amp; b &gt;' ) ); // '<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 '&amp;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 >' ) ); // '&lt;a &quot; b &gt;'
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, '&quot;' );
410 },
411
412 /**
413 * Decodes HTML entities that browsers tend to encode when used in attributes.
414 *
415 * alert( CKEDITOR.tools.htmlDecodeAttr( '&lt;a &quot; b&gt;' ) ); // '<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 -> &nbsp x 4;
441 html = html.replace( /\t/g, '&nbsp;&nbsp; &nbsp;' );
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, '&nbsp;' );
468
469 // Finally, preserve whitespaces that are to be lost.
470 html = html.replace( /(>|\s) /g, function( match, before ) {
471 return before + '&nbsp;';
472 } ).replace( / (?=<)/g, '&nbsp;' );
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( /&quot;/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 &mdash; `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 00000000..985a4f82
--- /dev/null
+++ b/sources/core/ui.js
@@ -0,0 +1,185 @@
1/**
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * 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 */
14CKEDITOR.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
36CKEDITOR.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
138CKEDITOR.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' )` &mdash; 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 */