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.js318
-rw-r--r--sources/core/ckeditor_basic.js94
-rw-r--r--sources/core/command.js275
-rw-r--r--sources/core/commanddefinition.js162
-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.js2183
-rw-r--r--sources/core/dom/elementpath.js251
-rw-r--r--sources/core/dom/event.js208
-rw-r--r--sources/core/dom/iterator.js565
-rw-r--r--sources/core/dom/node.js902
-rw-r--r--sources/core/dom/nodelist.js43
-rw-r--r--sources/core/dom/range.js2978
-rw-r--r--sources/core/dom/rangelist.js199
-rw-r--r--sources/core/dom/text.js135
-rw-r--r--sources/core/dom/walker.js652
-rw-r--r--sources/core/dom/window.js95
-rw-r--r--sources/core/dtd.js349
-rw-r--r--sources/core/editable.js3266
-rw-r--r--sources/core/editor.js2039
-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.js2540
-rw-r--r--sources/core/focusmanager.js281
-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.js568
-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.js202
-rw-r--r--sources/core/selection.js2204
-rw-r--r--sources/core/skin.js350
-rw-r--r--sources/core/style.js2102
-rw-r--r--sources/core/template.js68
-rw-r--r--sources/core/tools.js1916
-rw-r--r--sources/core/ui.js185
59 files changed, 32196 insertions, 0 deletions
diff --git a/sources/core/_bootstrap.js b/sources/core/_bootstrap.js
new file mode 100644
index 0000000..0163912
--- /dev/null
+++ b/sources/core/_bootstrap.js
@@ -0,0 +1,74 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview API initialization code.
8 */
9
10( function() {
11 // Disable HC detection in WebKit. (#5429)
12 if ( CKEDITOR.env.webkit )
13 CKEDITOR.env.hc = false;
14 else {
15 // Check whether high contrast is active by creating a colored border.
16 var hcDetect = CKEDITOR.dom.element.createFromHtml( '<div style="width:0;height:0;position:absolute;left:-10000px;' +
17 'border:1px solid;border-color:red blue"></div>', CKEDITOR.document );
18
19 hcDetect.appendTo( CKEDITOR.document.getHead() );
20
21 // Update CKEDITOR.env.
22 // Catch exception needed sometimes for FF. (#4230)
23 try {
24 var top = hcDetect.getComputedStyle( 'border-top-color' ),
25 right = hcDetect.getComputedStyle( 'border-right-color' );
26
27 // We need to check if getComputedStyle returned any value, because on FF
28 // it returnes empty string if CKEditor is loaded in hidden iframe. (#11121)
29 CKEDITOR.env.hc = !!( top && top == right );
30 } catch ( e ) {
31 CKEDITOR.env.hc = false;
32 }
33
34 hcDetect.remove();
35 }
36
37 if ( CKEDITOR.env.hc )
38 CKEDITOR.env.cssClass += ' cke_hc';
39
40 // Initially hide UI spaces when relevant skins are loading, later restored by skin css.
41 CKEDITOR.document.appendStyleText( '.cke{visibility:hidden;}' );
42
43 // Mark the editor as fully loaded.
44 CKEDITOR.status = 'loaded';
45 CKEDITOR.fireOnce( 'loaded' );
46
47 // Process all instances created by the "basic" implementation.
48 var pending = CKEDITOR._.pending;
49 if ( pending ) {
50 delete CKEDITOR._.pending;
51
52 for ( var i = 0; i < pending.length; i++ ) {
53 CKEDITOR.editor.prototype.constructor.apply( pending[ i ][ 0 ], pending[ i ][ 1 ] );
54 CKEDITOR.add( pending[ i ][ 0 ] );
55 }
56 }
57} )();
58
59/**
60 * Indicates that CKEditor is running on a High Contrast environment.
61 *
62 * if ( CKEDITOR.env.hc )
63 * alert( 'You\'re running on High Contrast mode. The editor interface will get adapted to provide you a better experience.' );
64 *
65 * @property {Boolean} hc
66 * @member CKEDITOR.env
67 */
68
69/**
70 * Fired when a CKEDITOR core object is fully loaded and ready for interaction.
71 *
72 * @event loaded
73 * @member CKEDITOR
74 */
diff --git a/sources/core/ckeditor.js b/sources/core/ckeditor.js
new file mode 100644
index 0000000..2756204
--- /dev/null
+++ b/sources/core/ckeditor.js
@@ -0,0 +1,204 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview 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 0000000..8c36b11
--- /dev/null
+++ b/sources/core/ckeditor_base.js
@@ -0,0 +1,318 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview 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 * **Note:** There is [a known issue where "icons.png" does not include
37 * timestamp](http://dev.ckeditor.com/ticket/10685) and might get cached.
38 * We are working on having it fixed.
39 *
40 * alert( CKEDITOR.timestamp ); // e.g. '87dm'
41 */
42 timestamp: '', // %REMOVE_LINE%
43 /* // %REMOVE_LINE%
44 // The production implementation contains a fixed timestamp, unique
45 // for each release and generated by the releaser.
46 // (Base 36 value of each component of YYMMDDHH - 4 chars total - e.g. 87bm == 08071122)
47 timestamp: '%TIMESTAMP%',
48 // %REMOVE_LINE% */
49
50 /**
51 * Contains the CKEditor version number.
52 *
53 * alert( CKEDITOR.version ); // e.g. 'CKEditor 3.4.1'
54 */
55 version: '%VERSION%',
56
57 /**
58 * Contains the CKEditor revision number.
59 * The revision number is incremented automatically, following each
60 * modification to the CKEditor source code.
61 *
62 * alert( CKEDITOR.revision ); // e.g. '3975'
63 */
64 revision: '%REV%',
65
66 /**
67 * A 3-digit random integer, valid for the entire life of the CKEDITOR object.
68 *
69 * alert( CKEDITOR.rnd ); // e.g. 319
70 *
71 * @property {Number}
72 */
73 rnd: Math.floor( Math.random() * ( 999 /*Max*/ - 100 /*Min*/ + 1 ) ) + 100 /*Min*/,
74
75 /**
76 * Private object used to hold core stuff. It should not be used outside of
77 * the API code as properties defined here may change at any time
78 * without notice.
79 *
80 * @private
81 */
82 _: {
83 pending: [],
84 basePathSrcPattern: basePathSrcPattern
85 },
86
87 /**
88 * Indicates the API loading status. The following statuses are available:
89 *
90 * * **unloaded**: the API is not yet loaded.
91 * * **basic_loaded**: the basic API features are available.
92 * * **basic_ready**: the basic API is ready to load the full core code.
93 * * **loaded**: the API can be fully used.
94 *
95 * Example:
96 *
97 * if ( CKEDITOR.status == 'loaded' ) {
98 * // The API can now be fully used.
99 * doSomething();
100 * } else {
101 * // Wait for the full core to be loaded and fire its loading.
102 * CKEDITOR.on( 'load', doSomething );
103 * CKEDITOR.loadFullCore && CKEDITOR.loadFullCore();
104 * }
105 */
106 status: 'unloaded',
107
108 /**
109 * The full URL for the CKEditor installation directory.
110 * It is possible to manually provide the base path by setting a
111 * global variable named `CKEDITOR_BASEPATH`. This global variable
112 * must be set **before** the editor script loading.
113 *
114 * alert( CKEDITOR.basePath ); // e.g. 'http://www.example.com/ckeditor/'
115 *
116 * @property {String}
117 */
118 basePath: ( function() {
119 // Find out the editor directory path, based on its <script> tag.
120 var path = window.CKEDITOR_BASEPATH || '';
121
122 if ( !path ) {
123 var scripts = document.getElementsByTagName( 'script' );
124
125 for ( var i = 0; i < scripts.length; i++ ) {
126 var match = scripts[ i ].src.match( basePathSrcPattern );
127
128 if ( match ) {
129 path = match[ 1 ];
130 break;
131 }
132 }
133 }
134
135 // In IE (only) the script.src string is the raw value entered in the
136 // HTML source. Other browsers return the full resolved URL instead.
137 if ( path.indexOf( ':/' ) == -1 && path.slice( 0, 2 ) != '//' ) {
138 // Absolute path.
139 if ( path.indexOf( '/' ) === 0 )
140 path = location.href.match( /^.*?:\/\/[^\/]*/ )[ 0 ] + path;
141 // Relative path.
142 else
143 path = location.href.match( /^[^\?]*\/(?:)/ )[ 0 ] + path;
144 }
145
146 if ( !path )
147 throw 'The CKEditor installation path could not be automatically detected. Please set the global variable "CKEDITOR_BASEPATH" before creating editor instances.';
148
149 return path;
150 } )(),
151
152 /**
153 * Gets the full URL for CKEditor resources. By default, URLs
154 * returned by this function contain a querystring parameter ("t")
155 * set to the {@link CKEDITOR#timestamp} value.
156 *
157 * It is possible to provide a custom implementation of this
158 * function by setting a global variable named `CKEDITOR_GETURL`.
159 * This global variable must be set **before** the editor script
160 * loading. If the custom implementation returns nothing (`==null`), the
161 * default implementation is used.
162 *
163 * // e.g. 'http://www.example.com/ckeditor/skins/default/editor.css?t=87dm'
164 * alert( CKEDITOR.getUrl( 'skins/default/editor.css' ) );
165 *
166 * // e.g. 'http://www.example.com/skins/default/editor.css?t=87dm'
167 * alert( CKEDITOR.getUrl( '/skins/default/editor.css' ) );
168 *
169 * // e.g. 'http://www.somesite.com/skins/default/editor.css?t=87dm'
170 * alert( CKEDITOR.getUrl( 'http://www.somesite.com/skins/default/editor.css' ) );
171 *
172 * @param {String} resource The resource whose full URL we want to get.
173 * It may be a full, absolute, or relative URL.
174 * @returns {String} The full URL.
175 */
176 getUrl: function( resource ) {
177 // If this is not a full or absolute path.
178 if ( resource.indexOf( ':/' ) == -1 && resource.indexOf( '/' ) !== 0 )
179 resource = this.basePath + resource;
180
181 // Add the timestamp, except for directories.
182 if ( this.timestamp && resource.charAt( resource.length - 1 ) != '/' && !( /[&?]t=/ ).test( resource ) )
183 resource += ( resource.indexOf( '?' ) >= 0 ? '&' : '?' ) + 't=' + this.timestamp;
184
185 return resource;
186 },
187
188 /**
189 * Specify a function to execute when the DOM is fully loaded.
190 *
191 * If called after the DOM has been initialized, the function passed in will
192 * be executed immediately.
193 *
194 * @method
195 * @todo
196 */
197 domReady: ( function() {
198 // Based on the original jQuery code (available under the MIT license, see LICENSE.md).
199
200 var callbacks = [];
201
202 function onReady() {
203 try {
204 // Cleanup functions for the document ready method
205 if ( document.addEventListener ) {
206 document.removeEventListener( 'DOMContentLoaded', onReady, false );
207 executeCallbacks();
208 }
209 // Make sure body exists, at least, in case IE gets a little overzealous.
210 else if ( document.attachEvent && document.readyState === 'complete' ) {
211 document.detachEvent( 'onreadystatechange', onReady );
212 executeCallbacks();
213 }
214 } catch ( er ) {}
215 }
216
217 function executeCallbacks() {
218 var i;
219 while ( ( i = callbacks.shift() ) )
220 i();
221 }
222
223 return function( fn ) {
224 callbacks.push( fn );
225
226 // Catch cases where this is called after the
227 // browser event has already occurred.
228 if ( document.readyState === 'complete' )
229 // Handle it asynchronously to allow scripts the opportunity to delay ready
230 setTimeout( onReady, 1 );
231
232 // Run below once on demand only.
233 if ( callbacks.length != 1 )
234 return;
235
236 // For IE>8, Firefox, Opera and Webkit.
237 if ( document.addEventListener ) {
238 // Use the handy event callback
239 document.addEventListener( 'DOMContentLoaded', onReady, false );
240
241 // A fallback to window.onload, that will always work
242 window.addEventListener( 'load', onReady, false );
243
244 }
245 // If old IE event model is used
246 else if ( document.attachEvent ) {
247 // ensure firing before onload,
248 // maybe late but safe also for iframes
249 document.attachEvent( 'onreadystatechange', onReady );
250
251 // A fallback to window.onload, that will always work
252 window.attachEvent( 'onload', onReady );
253
254 // If IE and not a frame
255 // continually check to see if the document is ready
256 // use the trick by Diego Perini
257 // http://javascript.nwbox.com/IEContentLoaded/
258 var toplevel = false;
259
260 try {
261 toplevel = !window.frameElement;
262 } catch ( e ) {}
263
264 if ( document.documentElement.doScroll && toplevel ) {
265 scrollCheck();
266 }
267 }
268
269 function scrollCheck() {
270 try {
271 document.documentElement.doScroll( 'left' );
272 } catch ( e ) {
273 setTimeout( scrollCheck, 1 );
274 return;
275 }
276 onReady();
277 }
278 };
279
280 } )()
281 };
282
283 // Make it possible to override the "url" function with a custom
284 // implementation pointing to a global named CKEDITOR_GETURL.
285 var newGetUrl = window.CKEDITOR_GETURL;
286 if ( newGetUrl ) {
287 var originalGetUrl = CKEDITOR.getUrl;
288 CKEDITOR.getUrl = function( resource ) {
289 return newGetUrl.call( CKEDITOR, resource ) || originalGetUrl.call( CKEDITOR, resource );
290 };
291 }
292
293 return CKEDITOR;
294 } )();
295}
296
297/**
298 * Function called upon loading a custom configuration file that can
299 * modify the editor instance configuration ({@link CKEDITOR.editor#config}).
300 * It is usually defined inside the custom configuration files that can
301 * include developer defined settings.
302 *
303 * // This is supposed to be placed in the config.js file.
304 * CKEDITOR.editorConfig = function( config ) {
305 * // Define changes to default configuration here. For example:
306 * config.language = 'fr';
307 * config.uiColor = '#AADC6E';
308 * };
309 *
310 * @method editorConfig
311 * @param {CKEDITOR.config} config A configuration object containing the
312 * settings defined for a {@link CKEDITOR.editor} instance up to this
313 * function call. Note that not all settings may still be available. See
314 * [Configuration Loading Order](http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Setting_Configurations#Configuration_Loading_Order)
315 * for details.
316 */
317
318// PACKAGER_RENAME( CKEDITOR )
diff --git a/sources/core/ckeditor_basic.js b/sources/core/ckeditor_basic.js
new file mode 100644
index 0000000..c07e4d9
--- /dev/null
+++ b/sources/core/ckeditor_basic.js
@@ -0,0 +1,94 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview 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 0000000..0446d24
--- /dev/null
+++ b/sources/core/command.js
@@ -0,0 +1,275 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * Represents a 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 0000000..10a040f
--- /dev/null
+++ b/sources/core/commanddefinition.js
@@ -0,0 +1,162 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the "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 to 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 needs 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 * var cmd = this;
58 * // Asynchronous operation below.
59 * CKEDITOR.ajax.loadXml( 'data.xml', function() {
60 * editor.fire( 'afterCommandExec', {
61 * name: 'loadoptions',
62 * command: cmd
63 * } );
64 * } );
65 * },
66 * async: true // The command needs some time to complete after the exec function returns.
67 * } );
68 *
69 * @property {Boolean} [async=false]
70 */
71
72/**
73 * Whether the command should give focus to the editor before execution.
74 *
75 * editorInstance.addCommand( 'maximize', {
76 * exec: function( editor ) {
77 * // ...
78 * },
79 * editorFocus: false // The command does not require focusing the editing document.
80 * } );
81 *
82 * See also {@link CKEDITOR.command#editorFocus}.
83 *
84 * @property {Boolean} [editorFocus=true]
85 */
86
87
88/**
89 * Whether the command state should be set to {@link CKEDITOR#TRISTATE_DISABLED} on startup.
90 *
91 * editorInstance.addCommand( 'unlink', {
92 * exec: function( editor ) {
93 * // ...
94 * },
95 * startDisabled: true // The command is unavailable until the selection is inside a link.
96 * } );
97 *
98 * @property {Boolean} [startDisabled=false]
99 */
100
101/**
102 * Indicates that this command is sensitive to the selection context.
103 * If `true`, the {@link CKEDITOR.command#method-refresh} method will be
104 * called for this command on selection changes, with a single parameter
105 * representing the current elements path.
106 *
107 * @property {Boolean} [contextSensitive=true]
108 */
109
110/**
111 * Defined by the command definition, a function to determine the command state. It will be invoked
112 * when the editor has its `states` or `selection` changed.
113 *
114 * **Note:** The function provided must be calling {@link CKEDITOR.command#setState} in all circumstances
115 * if it is intended to update the command state.
116 *
117 * @method refresh
118 * @param {CKEDITOR.editor} editor
119 * @param {CKEDITOR.dom.elementPath} path
120 */
121
122/**
123 * Sets the element name used to reflect the command state on selection changes.
124 * If the selection is in a place where the element is not allowed, the command
125 * will be disabled.
126 * Setting this property overrides {@link #contextSensitive} to `true`.
127 *
128 * @property {Boolean} [context=true]
129 */
130
131/**
132 * The editor modes within which the command can be executed. The execution
133 * will have no action if the current mode is not listed in this property.
134 *
135 * editorInstance.addCommand( 'link', {
136 * exec: function( editor ) {
137 * // ...
138 * },
139 * modes: { wysiwyg:1 } // The command is available in wysiwyg mode only.
140 * } );
141 *
142 * See also {@link CKEDITOR.command#modes}.
143 *
144 * @property {Object} [modes={ wysiwyg:1 }]
145 */
146
147/**
148 * Whether the command should be enabled in the {@link CKEDITOR.editor#setReadOnly read-only mode}.
149 *
150 * @since 4.0
151 * @property {Boolean} [readOnly=false]
152 */
153
154/**
155 * A property that should be set when a command has no keystroke assigned by {@link CKEDITOR.editor#setKeystroke}, but
156 * the keystroke is still supported. For example: `cut`, `copy` and `paste` commands are handled that way.
157 * This property is used when displaying keystroke information in tooltips and context menus. It is used by
158 * {@link CKEDITOR.editor#getCommandKeystroke}.
159 *
160 * @since 4.6.0
161 * @property {Number} fakeKeystroke
162 */
diff --git a/sources/core/config.js b/sources/core/config.js
new file mode 100644
index 0000000..f0f4aec
--- /dev/null
+++ b/sources/core/config.js
@@ -0,0 +1,451 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 0000000..adb402f
--- /dev/null
+++ b/sources/core/creators/inline.js
@@ -0,0 +1,157 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /** @class CKEDITOR */
8
9 /**
10 * Turns a DOM element with the `contenteditable` attribute set to `true` into a
11 * CKEditor instance. Check {@link CKEDITOR.dtd#$editable} for a list of
12 * allowed element names.
13 *
14 * **Note:** If the DOM element for which inline editing is being enabled does not have
15 * the `contenteditable` attribute set to `true`, the editor will start in read-only mode.
16 *
17 * <div contenteditable="true" id="content">...</div>
18 * ...
19 * CKEDITOR.inline( 'content' );
20 *
21 * It is also possible to create an inline editor from the `<textarea>` element.
22 * If you do so, an additional `<div>` element with editable content will be created
23 * directly after the `<textarea>` element and the `<textarea>` element will be hidden.
24 *
25 * @param {Object/String} element The DOM element or its ID.
26 * @param {Object} [instanceConfig] The specific configurations to apply to this editor instance.
27 * See {@link CKEDITOR.config}.
28 * @returns {CKEDITOR.editor} The editor instance created.
29 */
30 CKEDITOR.inline = function( element, instanceConfig ) {
31 if ( !CKEDITOR.env.isCompatible )
32 return null;
33
34 element = CKEDITOR.dom.element.get( element );
35
36 // Avoid multiple inline editor instances on the same element.
37 if ( element.getEditor() )
38 throw 'The editor instance "' + element.getEditor().name + '" is already attached to the provided element.';
39
40 var editor = new CKEDITOR.editor( instanceConfig, element, CKEDITOR.ELEMENT_MODE_INLINE ),
41 textarea = element.is( 'textarea' ) ? element : null;
42
43 if ( textarea ) {
44 editor.setData( textarea.getValue(), null, true );
45
46 //Change element from textarea to div
47 element = CKEDITOR.dom.element.createFromHtml(
48 '<div contenteditable="' + !!editor.readOnly + '" class="cke_textarea_inline">' +
49 textarea.getValue() +
50 '</div>',
51 CKEDITOR.document );
52
53 element.insertAfter( textarea );
54 textarea.hide();
55
56 // Attaching the concrete form.
57 if ( textarea.$.form )
58 editor._attachToForm();
59 } else {
60 // Initial editor data is simply loaded from the page element content to make
61 // data retrieval possible immediately after the editor creation.
62 editor.setData( element.getHtml(), null, true );
63 }
64
65 // Once the editor is loaded, start the UI.
66 editor.on( 'loaded', function() {
67 editor.fire( 'uiReady' );
68
69 // Enable editing on the element.
70 editor.editable( element );
71
72 // Editable itself is the outermost element.
73 editor.container = element;
74 editor.ui.contentsElement = element;
75
76 // Load and process editor data.
77 editor.setData( editor.getData( 1 ) );
78
79 // Clean on startup.
80 editor.resetDirty();
81
82 editor.fire( 'contentDom' );
83
84 // Inline editing defaults to "wysiwyg" mode, so plugins don't
85 // need to make special handling for this "mode-less" environment.
86 editor.mode = 'wysiwyg';
87 editor.fire( 'mode' );
88
89 // The editor is completely loaded for interaction.
90 editor.status = 'ready';
91 editor.fireOnce( 'instanceReady' );
92 CKEDITOR.fire( 'instanceReady', null, editor );
93
94 // give priority to plugins that relay on editor#loaded for bootstrapping.
95 }, null, null, 10000 );
96
97 // Handle editor destroying.
98 editor.on( 'destroy', function() {
99 // Remove container from DOM if inline-textarea editor.
100 // Show <textarea> back again.
101 if ( textarea ) {
102 editor.container.clearCustomData();
103 editor.container.remove();
104 textarea.show();
105 }
106
107 editor.element.clearCustomData();
108
109 delete editor.element;
110 } );
111
112 return editor;
113 };
114
115 /**
116 * Calls {@link CKEDITOR#inline} for all page elements with
117 * the `contenteditable` attribute set to `true`.
118 *
119 */
120 CKEDITOR.inlineAll = function() {
121 var el, data;
122
123 for ( var name in CKEDITOR.dtd.$editable ) {
124 var elements = CKEDITOR.document.getElementsByTag( name );
125
126 for ( var i = 0, len = elements.count(); i < len; i++ ) {
127 el = elements.getItem( i );
128
129 if ( el.getAttribute( 'contenteditable' ) == 'true' ) {
130 // Fire the "inline" event, making it possible to customize
131 // the instance settings and eventually cancel the creation.
132
133 data = {
134 element: el,
135 config: {}
136 };
137
138 if ( CKEDITOR.fire( 'inline', data ) !== false )
139 CKEDITOR.inline( el, data.config );
140 }
141 }
142 }
143 };
144
145 CKEDITOR.domReady( function() {
146 !CKEDITOR.disableAutoInline && CKEDITOR.inlineAll();
147 } );
148} )();
149
150/**
151 * Disables creating the inline editor automatically for elements with
152 * the `contenteditable` attribute set to `true`.
153 *
154 * CKEDITOR.disableAutoInline = true;
155 *
156 * @cfg {Boolean} [disableAutoInline=false]
157 */
diff --git a/sources/core/creators/themedui.js b/sources/core/creators/themedui.js
new file mode 100644
index 0000000..04927d0
--- /dev/null
+++ b/sources/core/creators/themedui.js
@@ -0,0 +1,541 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 /** @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. It is recommended to use `<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 a custom evaluation function.
83 * CKEDITOR.replaceAll( function( textarea, config ) {
84 * // A function that needs to be evaluated for the <textarea>
85 * // to be replaced. It must explicitly 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} [evaluator] An evaluation 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 evaluation 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 [Editor Resize](http://ckeditor.com/addon/resize) plugin, which adds
313 * a UI handle that can be used 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 editor content.
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 0000000..c42eab4
--- /dev/null
+++ b/sources/core/dataprocessor.js
@@ -0,0 +1,70 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the "virtual" {@link CKEDITOR.dataProcessor} class, which
8 * defines the basic structure of data processor objects to be
9 * set to {@link CKEDITOR.editor.dataProcessor}.
10 */
11
12/**
13 * If defined, points to the data processor which is responsible for translating
14 * and transforming the editor data on input and output.
15 * Generally it will point to an instance of {@link CKEDITOR.htmlDataProcessor},
16 * which handles HTML data. The editor may also handle other data formats by
17 * using different data processors provided by specific plugins.
18 *
19 * @property {CKEDITOR.dataProcessor} dataProcessor
20 * @member CKEDITOR.editor
21 */
22
23/**
24 * Represents a data processor which is responsible for translating and
25 * transforming the editor data on input and output.
26 *
27 * This class is here for documentation purposes only and is not really part of
28 * the API. It serves as the base ("interface") for data processor implementations.
29 *
30 * @class CKEDITOR.dataProcessor
31 * @abstract
32 */
33
34/**
35 * Transforms input data into HTML to be loaded into the editor.
36 * While the editor is able to handle non-HTML data (like BBCode), it can only
37 * handle HTML data at runtime. The role of the data processor is to transform
38 * the input data into HTML through this function.
39 *
40 * // Tranforming BBCode data, with a custom BBCode data processor available.
41 * var data = 'This is [b]an example[/b].';
42 * var html = editor.dataProcessor.toHtml( data ); // '<p>This is <b>an example</b>.</p>'
43 *
44 * @method toHtml
45 * @param {String} data The input data to be transformed.
46 * @param {String} [fixForBody] The tag name to be used if the data must be
47 * fixed because it is supposed to be loaded direcly into the `<body>`
48 * tag. This is generally not used by non-HTML data processors.
49 * @todo fixForBody type - compare to htmlDataProcessor.
50 */
51
52/**
53 * Transforms HTML into data to be output by the editor, in the format
54 * expected by the data processor.
55 *
56 * While the editor is able to handle non-HTML data (like BBCode), it can only
57 * handle HTML data at runtime. The role of the data processor is to transform
58 * the HTML data containined by the editor into a specific data format through
59 * this function.
60 *
61 * // Tranforming into BBCode data, with a custom BBCode data processor available.
62 * var html = '<p>This is <b>an example</b>.</p>';
63 * var data = editor.dataProcessor.toDataFormat( html ); // 'This is [b]an example[/b].'
64 *
65 * @method toDataFormat
66 * @param {String} html The HTML to be transformed.
67 * @param {String} fixForBody The tag name to be used if the output data is
68 * coming from the `<body>` element and may be eventually fixed for it. This is
69 * generally not used by non-HTML data processors.
70 */
diff --git a/sources/core/dom.js b/sources/core/dom.js
new file mode 100644
index 0000000..84aa21d
--- /dev/null
+++ b/sources/core/dom.js
@@ -0,0 +1,13 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom} 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 0000000..4abb453
--- /dev/null
+++ b/sources/core/dom/comment.js
@@ -0,0 +1,53 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.comment} class, which represents
8 * a DOM comment node.
9 */
10
11/**
12 * Represents a DOM comment node.
13 *
14 * var nativeNode = document.createComment( 'Example' );
15 * var comment = new CKEDITOR.dom.comment( nativeNode );
16 *
17 * var comment = new CKEDITOR.dom.comment( 'Example' );
18 *
19 * @class
20 * @extends CKEDITOR.dom.node
21 * @constructor Creates a comment class instance.
22 * @param {Object/String} comment A native DOM comment node or a string containing
23 * the text to use to create a new comment node.
24 * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
25 * the node in case of new node creation. Defaults to the current document.
26 */
27CKEDITOR.dom.comment = function( comment, ownerDocument ) {
28 if ( typeof comment == 'string' )
29 comment = ( ownerDocument ? ownerDocument.$ : document ).createComment( comment );
30
31 CKEDITOR.dom.domObject.call( this, comment );
32};
33
34CKEDITOR.dom.comment.prototype = new CKEDITOR.dom.node();
35
36CKEDITOR.tools.extend( CKEDITOR.dom.comment.prototype, {
37 /**
38 * The node type. This is a constant value set to {@link CKEDITOR#NODE_COMMENT}.
39 *
40 * @readonly
41 * @property {Number} [=CKEDITOR.NODE_COMMENT]
42 */
43 type: CKEDITOR.NODE_COMMENT,
44
45 /**
46 * Gets the outer HTML of this comment.
47 *
48 * @returns {String} The HTML `<!-- comment value -->`.
49 */
50 getOuterHtml: function() {
51 return '<!--' + this.$.nodeValue + '-->';
52 }
53} );
diff --git a/sources/core/dom/document.js b/sources/core/dom/document.js
new file mode 100644
index 0000000..51c5b19
--- /dev/null
+++ b/sources/core/dom/document.js
@@ -0,0 +1,326 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.document} class which
8 * represents a DOM document.
9 */
10
11/**
12 * Represents a DOM document.
13 *
14 * var document = new CKEDITOR.dom.document( document );
15 *
16 * @class
17 * @extends CKEDITOR.dom.domObject
18 * @constructor Creates a document class instance.
19 * @param {Object} domDocument A native DOM document.
20 */
21CKEDITOR.dom.document = function( domDocument ) {
22 CKEDITOR.dom.domObject.call( this, domDocument );
23};
24
25// PACKAGER_RENAME( CKEDITOR.dom.document )
26
27CKEDITOR.dom.document.prototype = new CKEDITOR.dom.domObject();
28
29CKEDITOR.tools.extend( CKEDITOR.dom.document.prototype, {
30 /**
31 * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT}.
32 *
33 * @readonly
34 * @property {Number} [=CKEDITOR.NODE_DOCUMENT]
35 */
36 type: CKEDITOR.NODE_DOCUMENT,
37
38 /**
39 * Appends a CSS file to the document.
40 *
41 * CKEDITOR.document.appendStyleSheet( '/mystyles.css' );
42 *
43 * @param {String} cssFileUrl The CSS file URL.
44 */
45 appendStyleSheet: function( cssFileUrl ) {
46 if ( this.$.createStyleSheet )
47 this.$.createStyleSheet( cssFileUrl );
48 else {
49 var link = new CKEDITOR.dom.element( 'link' );
50 link.setAttributes( {
51 rel: 'stylesheet',
52 type: 'text/css',
53 href: cssFileUrl
54 } );
55
56 this.getHead().append( link );
57 }
58 },
59
60 /**
61 * Creates a CSS stylesheet and inserts it into the document.
62 *
63 * @param cssStyleText {String} CSS style text.
64 * @returns {Object} The created DOM native stylesheet object.
65 */
66 appendStyleText: function( cssStyleText ) {
67 if ( this.$.createStyleSheet ) {
68 var styleSheet = this.$.createStyleSheet( '' );
69 styleSheet.cssText = cssStyleText;
70 } else {
71 var style = new CKEDITOR.dom.element( 'style', this );
72 style.append( new CKEDITOR.dom.text( cssStyleText, this ) );
73 this.getHead().append( style );
74 }
75
76 return styleSheet || style.$.sheet;
77 },
78
79 /**
80 * Creates a {@link CKEDITOR.dom.element} instance in this document.
81 *
82 * @param {String} name The name of the element.
83 * @param {Object} [attributesAndStyles]
84 * @param {Object} [attributesAndStyles.attributes] Attributes that will be set.
85 * @param {Object} [attributesAndStyles.styles] Styles that will be set.
86 * @returns {CKEDITOR.dom.element}
87 */
88 createElement: function( name, attribsAndStyles ) {
89 var element = new CKEDITOR.dom.element( name, this );
90
91 if ( attribsAndStyles ) {
92 if ( attribsAndStyles.attributes )
93 element.setAttributes( attribsAndStyles.attributes );
94
95 if ( attribsAndStyles.styles )
96 element.setStyles( attribsAndStyles.styles );
97 }
98
99 return element;
100 },
101
102 /**
103 * Creates a {@link CKEDITOR.dom.text} instance in this document.
104 *
105 * @param {String} text Value of the text node.
106 * @returns {CKEDITOR.dom.element}
107 */
108 createText: function( text ) {
109 return new CKEDITOR.dom.text( text, this );
110 },
111
112 /**
113 * Moves the selection focus to this document's window.
114 */
115 focus: function() {
116 this.getWindow().focus();
117 },
118
119 /**
120 * Returns the element that is currently designated as the active element in the document.
121 *
122 * **Note:** Only one element can be active at a time in a document.
123 * An active element does not necessarily have focus,
124 * but an element with focus is always the active element in a document.
125 *
126 * @returns {CKEDITOR.dom.element} Active element or `null` if an IE8-9 bug is encountered.
127 * See [#10030](http://dev.ckeditor.com/ticket/10030).
128 */
129 getActive: function() {
130 var $active;
131 try {
132 $active = this.$.activeElement;
133 } catch ( e ) {
134 return null;
135 }
136 return new CKEDITOR.dom.element( $active );
137 },
138
139 /**
140 * Gets an element based on its ID.
141 *
142 * var element = CKEDITOR.document.getById( 'myElement' );
143 * alert( element.getId() ); // 'myElement'
144 *
145 * @param {String} elementId The element ID.
146 * @returns {CKEDITOR.dom.element} The element instance, or `null` if not found.
147 */
148 getById: function( elementId ) {
149 var $ = this.$.getElementById( elementId );
150 return $ ? new CKEDITOR.dom.element( $ ) : null;
151 },
152
153 /**
154 * Gets a node based on its address. See {@link CKEDITOR.dom.node#getAddress}.
155 *
156 * @param {Array} address
157 * @param {Boolean} [normalized=false]
158 */
159 getByAddress: function( address, normalized ) {
160 var $ = this.$.documentElement;
161
162 for ( var i = 0; $ && i < address.length; i++ ) {
163 var target = address[ i ];
164
165 if ( !normalized ) {
166 $ = $.childNodes[ target ];
167 continue;
168 }
169
170 var currentIndex = -1;
171
172 for ( var j = 0; j < $.childNodes.length; j++ ) {
173 var candidate = $.childNodes[ j ];
174
175 if ( normalized === true && candidate.nodeType == 3 && candidate.previousSibling && candidate.previousSibling.nodeType == 3 )
176 continue;
177
178 currentIndex++;
179
180 if ( currentIndex == target ) {
181 $ = candidate;
182 break;
183 }
184 }
185 }
186
187 return $ ? new CKEDITOR.dom.node( $ ) : null;
188 },
189
190 /**
191 * Gets elements list based on a given tag name.
192 *
193 * @param {String} tagName The element tag name.
194 * @returns {CKEDITOR.dom.nodeList} The nodes list.
195 */
196 getElementsByTag: function( tagName, namespace ) {
197 if ( !( CKEDITOR.env.ie && ( document.documentMode <= 8 ) ) && namespace )
198 tagName = namespace + ':' + tagName;
199 return new CKEDITOR.dom.nodeList( this.$.getElementsByTagName( tagName ) );
200 },
201
202 /**
203 * Gets the `<head>` element for this document.
204 *
205 * var element = CKEDITOR.document.getHead();
206 * alert( element.getName() ); // 'head'
207 *
208 * @returns {CKEDITOR.dom.element} The `<head>` element.
209 */
210 getHead: function() {
211 var head = this.$.getElementsByTagName( 'head' )[ 0 ];
212 if ( !head )
213 head = this.getDocumentElement().append( new CKEDITOR.dom.element( 'head' ), true );
214 else
215 head = new CKEDITOR.dom.element( head );
216
217 return head;
218 },
219
220 /**
221 * Gets the `<body>` element for this document.
222 *
223 * var element = CKEDITOR.document.getBody();
224 * alert( element.getName() ); // 'body'
225 *
226 * @returns {CKEDITOR.dom.element} The `<body>` element.
227 */
228 getBody: function() {
229 return new CKEDITOR.dom.element( this.$.body );
230 },
231
232 /**
233 * Gets the DOM document element for this document.
234 *
235 * @returns {CKEDITOR.dom.element} The DOM document element.
236 */
237 getDocumentElement: function() {
238 return new CKEDITOR.dom.element( this.$.documentElement );
239 },
240
241 /**
242 * Gets the window object that stores this document.
243 *
244 * @returns {CKEDITOR.dom.window} The window object.
245 */
246 getWindow: function() {
247 return new CKEDITOR.dom.window( this.$.parentWindow || this.$.defaultView );
248 },
249
250 /**
251 * Defines the document content through `document.write`. Note that the
252 * previous document content will be lost (cleaned).
253 *
254 * document.write(
255 * '<html>' +
256 * '<head><title>Sample Document</title></head>' +
257 * '<body>Document content created by code.</body>' +
258 * '</html>'
259 * );
260 *
261 * @since 3.5
262 * @param {String} html The HTML defining the document content.
263 */
264 write: function( html ) {
265 // Don't leave any history log in IE. (#5657)
266 this.$.open( 'text/html', 'replace' );
267
268 // Support for custom document.domain in IE.
269 //
270 // The script must be appended because if placed before the
271 // doctype, IE will go into quirks mode and mess with
272 // the editable, e.g. by changing its default height.
273 if ( CKEDITOR.env.ie )
274 html = html.replace( /(?:^\s*<!DOCTYPE[^>]*?>)|^/i, '$&\n<script data-cke-temp="1">(' + CKEDITOR.tools.fixDomain + ')();</script>' );
275
276 this.$.write( html );
277 this.$.close();
278 },
279
280 /**
281 * Wrapper for `querySelectorAll`. Returns a list of elements within this document that match
282 * the specified `selector`.
283 *
284 * **Note:** The returned list is not a live collection (like the result of native `querySelectorAll`).
285 *
286 * @since 4.3
287 * @param {String} selector
288 * @returns {CKEDITOR.dom.nodeList}
289 */
290 find: function( selector ) {
291 return new CKEDITOR.dom.nodeList( this.$.querySelectorAll( selector ) );
292 },
293
294 /**
295 * Wrapper for `querySelector`. Returns the first element within this document that matches
296 * the specified `selector`.
297 *
298 * @since 4.3
299 * @param {String} selector
300 * @returns {CKEDITOR.dom.element}
301 */
302 findOne: function( selector ) {
303 var el = this.$.querySelector( selector );
304
305 return el ? new CKEDITOR.dom.element( el ) : null;
306 },
307
308 /**
309 * Internet Explorer 8 only method. It returns a document fragment which has all HTML5 elements enabled.
310 *
311 * @since 4.3
312 * @private
313 * @returns DocumentFragment
314 */
315 _getHtml5ShivFrag: function() {
316 var $frag = this.getCustomData( 'html5ShivFrag' );
317
318 if ( !$frag ) {
319 $frag = this.$.createDocumentFragment();
320 CKEDITOR.tools.enableHtml5Elements( $frag, true );
321 this.setCustomData( 'html5ShivFrag', $frag );
322 }
323
324 return $frag;
325 }
326} );
diff --git a/sources/core/dom/documentfragment.js b/sources/core/dom/documentfragment.js
new file mode 100644
index 0000000..1058144
--- /dev/null
+++ b/sources/core/dom/documentfragment.js
@@ -0,0 +1,62 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * DocumentFragment is a "lightweight" or "minimal" Document object. It is
8 * commonly used to extract a portion of a document's tree or to create a new
9 * fragment of a document. Various operations may take document fragment objects
10 * as arguments and result in all the child nodes of the document fragment being
11 * moved to the child list of this node.
12 *
13 * @class
14 * @constructor Creates a document fragment class instance.
15 * @param {CKEDITOR.dom.document/DocumentFragment} [nodeOrDoc=CKEDITOR.document]
16 */
17CKEDITOR.dom.documentFragment = function( nodeOrDoc ) {
18 nodeOrDoc = nodeOrDoc || CKEDITOR.document;
19
20 if ( nodeOrDoc.type == CKEDITOR.NODE_DOCUMENT )
21 this.$ = nodeOrDoc.$.createDocumentFragment();
22 else
23 this.$ = nodeOrDoc;
24};
25
26CKEDITOR.tools.extend( CKEDITOR.dom.documentFragment.prototype, CKEDITOR.dom.element.prototype, {
27 /**
28 * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
29 *
30 * @readonly
31 * @property {Number} [=CKEDITOR.NODE_DOCUMENT_FRAGMENT]
32 */
33 type: CKEDITOR.NODE_DOCUMENT_FRAGMENT,
34
35 /**
36 * Inserts the document fragment content after the specified node.
37 *
38 * @param {CKEDITOR.dom.node} node
39 */
40 insertAfterNode: function( node ) {
41 node = node.$;
42 node.parentNode.insertBefore( this.$, node.nextSibling );
43 },
44
45 /**
46 * Gets HTML of this document fragment's children.
47 *
48 * @since 4.5
49 * @returns {String} The HTML of this document fragment's children.
50 */
51 getHtml: function() {
52 var container = new CKEDITOR.dom.element( 'div' );
53
54 this.clone( 1, 1 ).appendTo( container );
55
56 return container.getHtml().replace( /\s*data-cke-expando=".*?"/g, '' );
57 }
58}, true, {
59 'append': 1, 'appendBogus': 1, 'clone': 1, 'getFirst': 1, 'getHtml': 1, 'getLast': 1, 'getParent': 1, 'getNext': 1, 'getPrevious': 1,
60 'appendTo': 1, 'moveChildren': 1, 'insertBefore': 1, 'insertAfterNode': 1, 'replace': 1, 'trim': 1, 'type': 1,
61 'ltrim': 1, 'rtrim': 1, 'getDocument': 1, 'getChildCount': 1, 'getChild': 1, 'getChildren': 1
62} );
diff --git a/sources/core/dom/domobject.js b/sources/core/dom/domobject.js
new file mode 100644
index 0000000..4c593ff
--- /dev/null
+++ b/sources/core/dom/domobject.js
@@ -0,0 +1,266 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.editor} class, which is the base
8 * for other classes representing DOM objects.
9 */
10
11/**
12 * Represents a DOM object. This class is not intended to be used directly. It
13 * serves as the base class for other classes representing specific DOM
14 * objects.
15 *
16 * @class
17 * @mixins CKEDITOR.event
18 * @constructor Creates a domObject class instance.
19 * @param {Object} nativeDomObject A native DOM object.
20 */
21CKEDITOR.dom.domObject = function( nativeDomObject ) {
22 if ( nativeDomObject ) {
23 /**
24 * The native DOM object represented by this class instance.
25 *
26 * var element = new CKEDITOR.dom.element( 'span' );
27 * alert( element.$.nodeType ); // '1'
28 *
29 * @readonly
30 * @property {Object}
31 */
32 this.$ = nativeDomObject;
33 }
34};
35
36CKEDITOR.dom.domObject.prototype = ( function() {
37 // Do not define other local variables here. We want to keep the native
38 // listener closures as clean as possible.
39
40 var getNativeListener = function( domObject, eventName ) {
41 return function( domEvent ) {
42 // In FF, when reloading the page with the editor focused, it may
43 // throw an error because the CKEDITOR global is not anymore
44 // available. So, we check it here first. (#2923)
45 if ( typeof CKEDITOR != 'undefined' )
46 domObject.fire( eventName, new CKEDITOR.dom.event( domEvent ) );
47 };
48 };
49
50 return {
51
52 /**
53 * Gets the private `_` object which is bound to the native
54 * DOM object using {@link #getCustomData}.
55 *
56 * var elementA = new CKEDITOR.dom.element( nativeElement );
57 * elementA.getPrivate().value = 1;
58 * ...
59 * var elementB = new CKEDITOR.dom.element( nativeElement );
60 * elementB.getPrivate().value; // 1
61 *
62 * @returns {Object} The private object.
63 */
64 getPrivate: function() {
65 var priv;
66
67 // Get the main private object from the custom data. Create it if not defined.
68 if ( !( priv = this.getCustomData( '_' ) ) )
69 this.setCustomData( '_', ( priv = {} ) );
70
71 return priv;
72 },
73
74 // Docs inherited from event.
75 on: function( eventName ) {
76 // We customize the "on" function here. The basic idea is that we'll have
77 // only one listener for a native event, which will then call all listeners
78 // set to the event.
79
80 // Get the listeners holder object.
81 var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
82
83 if ( !nativeListeners ) {
84 nativeListeners = {};
85 this.setCustomData( '_cke_nativeListeners', nativeListeners );
86 }
87
88 // Check if we have a listener for that event.
89 if ( !nativeListeners[ eventName ] ) {
90 var listener = nativeListeners[ eventName ] = getNativeListener( this, eventName );
91
92 if ( this.$.addEventListener )
93 this.$.addEventListener( eventName, listener, !!CKEDITOR.event.useCapture );
94 else if ( this.$.attachEvent )
95 this.$.attachEvent( 'on' + eventName, listener );
96 }
97
98 // Call the original implementation.
99 return CKEDITOR.event.prototype.on.apply( this, arguments );
100 },
101
102 // Docs inherited from event.
103 removeListener: function( eventName ) {
104 // Call the original implementation.
105 CKEDITOR.event.prototype.removeListener.apply( this, arguments );
106
107 // If we don't have listeners for this event, clean the DOM up.
108 if ( !this.hasListeners( eventName ) ) {
109 var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
110 var listener = nativeListeners && nativeListeners[ eventName ];
111 if ( listener ) {
112 if ( this.$.removeEventListener )
113 this.$.removeEventListener( eventName, listener, false );
114 else if ( this.$.detachEvent )
115 this.$.detachEvent( 'on' + eventName, listener );
116
117 delete nativeListeners[ eventName ];
118 }
119 }
120 },
121
122 /**
123 * Removes any listener set on this object.
124 *
125 * To avoid memory leaks we must assure that there are no
126 * references left after the object is no longer needed.
127 */
128 removeAllListeners: function() {
129 var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
130 for ( var eventName in nativeListeners ) {
131 var listener = nativeListeners[ eventName ];
132 if ( this.$.detachEvent )
133 this.$.detachEvent( 'on' + eventName, listener );
134 else if ( this.$.removeEventListener )
135 this.$.removeEventListener( eventName, listener, false );
136
137 delete nativeListeners[ eventName ];
138 }
139
140 // Remove events from events object so fire() method will not call
141 // listeners (#11400).
142 CKEDITOR.event.prototype.removeAllListeners.call( this );
143 }
144 };
145} )();
146
147( function( domObjectProto ) {
148 var customData = {};
149
150 CKEDITOR.on( 'reset', function() {
151 customData = {};
152 } );
153
154 /**
155 * Determines whether the specified object is equal to the current object.
156 *
157 * var doc = new CKEDITOR.dom.document( document );
158 * alert( doc.equals( CKEDITOR.document ) ); // true
159 * alert( doc == CKEDITOR.document ); // false
160 *
161 * @param {Object} object The object to compare with the current object.
162 * @returns {Boolean} `true` if the object is equal.
163 */
164 domObjectProto.equals = function( object ) {
165 // Try/Catch to avoid IE permission error when object is from different document.
166 try {
167 return ( object && object.$ === this.$ );
168 } catch ( er ) {
169 return false;
170 }
171 };
172
173 /**
174 * Sets a data slot value for this object. These values are shared by all
175 * instances pointing to that same DOM object.
176 *
177 * **Note:** The created data slot is only guaranteed to be available on this unique DOM node,
178 * thus any wish to continue access to it from other element clones (either created by
179 * clone node or from `innerHtml`) will fail. For such usage please use
180 * {@link CKEDITOR.dom.element#setAttribute} instead.
181 *
182 * **Note**: This method does not work on text nodes prior to Internet Explorer 9.
183 *
184 * var element = new CKEDITOR.dom.element( 'span' );
185 * element.setCustomData( 'hasCustomData', true );
186 *
187 * @param {String} key A key used to identify the data slot.
188 * @param {Object} value The value to set to the data slot.
189 * @returns {CKEDITOR.dom.domObject} This DOM object instance.
190 * @chainable
191 */
192 domObjectProto.setCustomData = function( key, value ) {
193 var expandoNumber = this.getUniqueId(),
194 dataSlot = customData[ expandoNumber ] || ( customData[ expandoNumber ] = {} );
195
196 dataSlot[ key ] = value;
197
198 return this;
199 };
200
201 /**
202 * Gets the value set to a data slot in this object.
203 *
204 * var element = new CKEDITOR.dom.element( 'span' );
205 * alert( element.getCustomData( 'hasCustomData' ) ); // e.g. 'true'
206 * alert( element.getCustomData( 'nonExistingKey' ) ); // null
207 *
208 * @param {String} key The key used to identify the data slot.
209 * @returns {Object} This value set to the data slot.
210 */
211 domObjectProto.getCustomData = function( key ) {
212 var expandoNumber = this.$[ 'data-cke-expando' ],
213 dataSlot = expandoNumber && customData[ expandoNumber ];
214
215 return ( dataSlot && key in dataSlot ) ? dataSlot[ key ] : null;
216 };
217
218 /**
219 * Removes the value in the data slot under the given `key`.
220 *
221 * @param {String} key
222 * @returns {Object} Removed value or `null` if not found.
223 */
224 domObjectProto.removeCustomData = function( key ) {
225 var expandoNumber = this.$[ 'data-cke-expando' ],
226 dataSlot = expandoNumber && customData[ expandoNumber ],
227 retval, hadKey;
228
229 if ( dataSlot ) {
230 retval = dataSlot[ key ];
231 hadKey = key in dataSlot;
232 delete dataSlot[ key ];
233 }
234
235 return hadKey ? retval : null;
236 };
237
238 /**
239 * Removes any data stored in this object.
240 * To avoid memory leaks we must assure that there are no
241 * references left after the object is no longer needed.
242 */
243 domObjectProto.clearCustomData = function() {
244 // Clear all event listeners
245 this.removeAllListeners();
246
247 var expandoNumber = this.$[ 'data-cke-expando' ];
248 expandoNumber && delete customData[ expandoNumber ];
249 };
250
251 /**
252 * Gets an ID that can be used to identify this DOM object in
253 * the running session.
254 *
255 * **Note**: This method does not work on text nodes prior to Internet Explorer 9.
256 *
257 * @returns {Number} A unique ID.
258 */
259 domObjectProto.getUniqueId = function() {
260 return this.$[ 'data-cke-expando' ] || ( this.$[ 'data-cke-expando' ] = CKEDITOR.tools.getNextNumber() );
261 };
262
263 // Implement CKEDITOR.event.
264 CKEDITOR.event.implementOn( domObjectProto );
265
266} )( CKEDITOR.dom.domObject.prototype );
diff --git a/sources/core/dom/element.js b/sources/core/dom/element.js
new file mode 100644
index 0000000..e02ff17
--- /dev/null
+++ b/sources/core/dom/element.js
@@ -0,0 +1,2183 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.element} class, which
8 * represents a DOM element.
9 */
10
11/**
12 * Represents a DOM element.
13 *
14 * // Create a new <span> element.
15 * var element = new CKEDITOR.dom.element( 'span' );
16 *
17 * // Create an element based on a native DOM element.
18 * var element = new CKEDITOR.dom.element( document.getElementById( 'myId' ) );
19 *
20 * @class
21 * @extends CKEDITOR.dom.node
22 * @constructor Creates an element class instance.
23 * @param {Object/String} element A native DOM element or the element name for
24 * new elements.
25 * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
26 * the element in case of element creation.
27 */
28CKEDITOR.dom.element = function( element, ownerDocument ) {
29 if ( typeof element == 'string' )
30 element = ( ownerDocument ? ownerDocument.$ : document ).createElement( element );
31
32 // Call the base constructor (we must not call CKEDITOR.dom.node).
33 CKEDITOR.dom.domObject.call( this, element );
34};
35
36// PACKAGER_RENAME( CKEDITOR.dom.element )
37/**
38 * The the {@link CKEDITOR.dom.element} representing and element. If the
39 * element is a native DOM element, it will be transformed into a valid
40 * CKEDITOR.dom.element object.
41 *
42 * var element = new CKEDITOR.dom.element( 'span' );
43 * alert( element == CKEDITOR.dom.element.get( element ) ); // true
44 *
45 * var element = document.getElementById( 'myElement' );
46 * alert( CKEDITOR.dom.element.get( element ).getName() ); // (e.g.) 'p'
47 *
48 * @static
49 * @param {String/Object} element Element's id or name or native DOM element.
50 * @returns {CKEDITOR.dom.element} The transformed element.
51 */
52CKEDITOR.dom.element.get = function( element ) {
53 var el = typeof element == 'string' ? document.getElementById( element ) || document.getElementsByName( element )[ 0 ] : element;
54
55 return el && ( el.$ ? el : new CKEDITOR.dom.element( el ) );
56};
57
58CKEDITOR.dom.element.prototype = new CKEDITOR.dom.node();
59
60/**
61 * Creates an instance of the {@link CKEDITOR.dom.element} class based on the
62 * HTML representation of an element.
63 *
64 * var element = CKEDITOR.dom.element.createFromHtml( '<strong class="anyclass">My element</strong>' );
65 * alert( element.getName() ); // 'strong'
66 *
67 * @static
68 * @param {String} html The element HTML. It should define only one element in
69 * the "root" level. The "root" element can have child nodes, but not siblings.
70 * @returns {CKEDITOR.dom.element} The element instance.
71 */
72CKEDITOR.dom.element.createFromHtml = function( html, ownerDocument ) {
73 var temp = new CKEDITOR.dom.element( 'div', ownerDocument );
74 temp.setHtml( html );
75
76 // When returning the node, remove it from its parent to detach it.
77 return temp.getFirst().remove();
78};
79
80/**
81 * Sets {@link CKEDITOR.dom.element#setCustomData custom data} on an element in a way that it is later
82 * possible to {@link #clearAllMarkers clear all data} set on all elements sharing the same database.
83 *
84 * This mechanism is very useful when processing some portion of DOM. All markers can later be removed
85 * by calling the {@link #clearAllMarkers} method, hence markers will not leak to second pass of this algorithm.
86 *
87 * var database = {};
88 * CKEDITOR.dom.element.setMarker( database, element1, 'foo', 'bar' );
89 * CKEDITOR.dom.element.setMarker( database, element2, 'oof', [ 1, 2, 3 ] );
90 *
91 * element1.getCustomData( 'foo' ); // 'bar'
92 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
93 *
94 * CKEDITOR.dom.element.clearAllMarkers( database );
95 *
96 * element1.getCustomData( 'foo' ); // null
97 *
98 * @static
99 * @param {Object} database
100 * @param {CKEDITOR.dom.element} element
101 * @param {String} name
102 * @param {Object} value
103 * @returns {CKEDITOR.dom.element} The element.
104 */
105CKEDITOR.dom.element.setMarker = function( database, element, name, value ) {
106 var id = element.getCustomData( 'list_marker_id' ) || ( element.setCustomData( 'list_marker_id', CKEDITOR.tools.getNextNumber() ).getCustomData( 'list_marker_id' ) ),
107 markerNames = element.getCustomData( 'list_marker_names' ) || ( element.setCustomData( 'list_marker_names', {} ).getCustomData( 'list_marker_names' ) );
108 database[ id ] = element;
109 markerNames[ name ] = 1;
110
111 return element.setCustomData( name, value );
112};
113
114/**
115 * Removes all markers added using this database. See the {@link #setMarker} method for more information.
116 *
117 * @param {Object} database
118 * @static
119 */
120CKEDITOR.dom.element.clearAllMarkers = function( database ) {
121 for ( var i in database )
122 CKEDITOR.dom.element.clearMarkers( database, database[ i ], 1 );
123};
124
125/**
126 * Removes all markers added to this element and removes it from the database if
127 * `removeFromDatabase` was passed. See the {@link #setMarker} method for more information.
128 *
129 * var database = {};
130 * CKEDITOR.dom.element.setMarker( database, element1, 'foo', 'bar' );
131 * CKEDITOR.dom.element.setMarker( database, element2, 'oof', [ 1, 2, 3 ] );
132 *
133 * element1.getCustomData( 'foo' ); // 'bar'
134 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
135 *
136 * CKEDITOR.dom.element.clearMarkers( database, element1, true );
137 *
138 * element1.getCustomData( 'foo' ); // null
139 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
140 *
141 * @param {Object} database
142 * @static
143 */
144CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatabase ) {
145 var names = element.getCustomData( 'list_marker_names' ),
146 id = element.getCustomData( 'list_marker_id' );
147 for ( var i in names )
148 element.removeCustomData( i );
149 element.removeCustomData( 'list_marker_names' );
150 if ( removeFromDatabase ) {
151 element.removeCustomData( 'list_marker_id' );
152 delete database[ id ];
153 }
154};
155
156( function() {
157 var elementsClassList = document.createElement( '_' ).classList,
158 supportsClassLists = typeof elementsClassList !== 'undefined' && String( elementsClassList.add ).match( /\[Native code\]/gi ) !== null,
159 rclass = /[\n\t\r]/g;
160
161 function hasClass( classNames, className ) {
162 // Source: jQuery.
163 return ( ' ' + classNames + ' ' ).replace( rclass, ' ' ).indexOf( ' ' + className + ' ' ) > -1;
164 }
165
166 CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, {
167 /**
168 * The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}.
169 *
170 * @readonly
171 * @property {Number} [=CKEDITOR.NODE_ELEMENT]
172 */
173 type: CKEDITOR.NODE_ELEMENT,
174
175 /**
176 * Adds a CSS class to the element. It appends the class to the
177 * already existing names.
178 *
179 * var element = new CKEDITOR.dom.element( 'div' );
180 * element.addClass( 'classA' ); // <div class="classA">
181 * element.addClass( 'classB' ); // <div class="classA classB">
182 * element.addClass( 'classA' ); // <div class="classA classB">
183 *
184 * **Note:** Since CKEditor 4.5 this method cannot be used with multiple classes (`'classA classB'`).
185 *
186 * @chainable
187 * @method addClass
188 * @param {String} className The name of the class to be added.
189 */
190 addClass: supportsClassLists ?
191 function( className ) {
192 this.$.classList.add( className );
193
194 return this;
195 } : function( className ) {
196 var c = this.$.className;
197 if ( c ) {
198 if ( !hasClass( c, className ) )
199 c += ' ' + className;
200 }
201 this.$.className = c || className;
202
203 return this;
204 },
205
206 /**
207 * Removes a CSS class name from the elements classes. Other classes
208 * remain untouched.
209 *
210 * var element = new CKEDITOR.dom.element( 'div' );
211 * element.addClass( 'classA' ); // <div class="classA">
212 * element.addClass( 'classB' ); // <div class="classA classB">
213 * element.removeClass( 'classA' ); // <div class="classB">
214 * element.removeClass( 'classB' ); // <div>
215 *
216 * @chainable
217 * @method removeClass
218 * @param {String} className The name of the class to remove.
219 */
220 removeClass: supportsClassLists ?
221 function( className ) {
222 var $ = this.$;
223 $.classList.remove( className );
224
225 if ( !$.className )
226 $.removeAttribute( 'class' );
227
228 return this;
229 } : function( className ) {
230 var c = this.getAttribute( 'class' );
231 if ( c && hasClass( c, className ) ) {
232 c = c
233 .replace( new RegExp( '(?:^|\\s+)' + className + '(?=\\s|$)' ), '' )
234 .replace( /^\s+/, '' );
235
236 if ( c )
237 this.setAttribute( 'class', c );
238 else
239 this.removeAttribute( 'class' );
240 }
241
242 return this;
243 },
244
245 /**
246 * Checks if element has class name.
247 *
248 * @param {String} className
249 * @returns {Boolean}
250 */
251 hasClass: function( className ) {
252 return hasClass( this.$.className, className );
253 },
254
255 /**
256 * Append a node as a child of this element.
257 *
258 * var p = new CKEDITOR.dom.element( 'p' );
259 *
260 * var strong = new CKEDITOR.dom.element( 'strong' );
261 * p.append( strong );
262 *
263 * var em = p.append( 'em' );
264 *
265 * // Result: '<p><strong></strong><em></em></p>'
266 *
267 * @param {CKEDITOR.dom.node/String} node The node or element name to be appended.
268 * @param {Boolean} [toStart=false] Indicates that the element is to be appended at the start.
269 * @returns {CKEDITOR.dom.node} The appended node.
270 */
271 append: function( node, toStart ) {
272 if ( typeof node == 'string' )
273 node = this.getDocument().createElement( node );
274
275 if ( toStart )
276 this.$.insertBefore( node.$, this.$.firstChild );
277 else
278 this.$.appendChild( node.$ );
279
280 return node;
281 },
282
283 /**
284 * Append HTML as a child(ren) of this element.
285 *
286 * @param {String} html
287 */
288 appendHtml: function( html ) {
289 if ( !this.$.childNodes.length )
290 this.setHtml( html );
291 else {
292 var temp = new CKEDITOR.dom.element( 'div', this.getDocument() );
293 temp.setHtml( html );
294 temp.moveChildren( this );
295 }
296 },
297
298 /**
299 * Append text to this element.
300 *
301 * var p = new CKEDITOR.dom.element( 'p' );
302 * p.appendText( 'This is' );
303 * p.appendText( ' some text' );
304 *
305 * // Result: '<p>This is some text</p>'
306 *
307 * @param {String} text The text to be appended.
308 */
309 appendText: function( text ) {
310 // On IE8 it is impossible to append node to script tag, so we use its text.
311 // On the contrary, on Safari the text property is unpredictable in links. (#13232)
312 if ( this.$.text != null && CKEDITOR.env.ie && CKEDITOR.env.version < 9 )
313 this.$.text += text;
314 else
315 this.append( new CKEDITOR.dom.text( text ) );
316 },
317
318 /**
319 * Appends a `<br>` filler element to this element if the filler is not present already.
320 * By default filler is appended only if {@link CKEDITOR.env#needsBrFiller} is `true`,
321 * however when `force` is set to `true` filler will be appended regardless of the environment.
322 *
323 * @param {Boolean} [force] Append filler regardless of the environment.
324 */
325 appendBogus: function( force ) {
326 if ( !force && !CKEDITOR.env.needsBrFiller )
327 return;
328
329 var lastChild = this.getLast();
330
331 // Ignore empty/spaces text.
332 while ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.rtrim( lastChild.getText() ) )
333 lastChild = lastChild.getPrevious();
334 if ( !lastChild || !lastChild.is || !lastChild.is( 'br' ) ) {
335 var bogus = this.getDocument().createElement( 'br' );
336
337 CKEDITOR.env.gecko && bogus.setAttribute( 'type', '_moz' );
338
339 this.append( bogus );
340 }
341 },
342
343 /**
344 * Breaks one of the ancestor element in the element position, moving
345 * this element between the broken parts.
346 *
347 * // Before breaking:
348 * // <b>This <i>is some<span /> sample</i> test text</b>
349 * // If "element" is <span /> and "parent" is <i>:
350 * // <b>This <i>is some</i><span /><i> sample</i> test text</b>
351 * element.breakParent( parent );
352 *
353 * // Before breaking:
354 * // <b>This <i>is some<span /> sample</i> test text</b>
355 * // If "element" is <span /> and "parent" is <b>:
356 * // <b>This <i>is some</i></b><span /><b><i> sample</i> test text</b>
357 * element.breakParent( parent );
358 *
359 * @param {CKEDITOR.dom.element} parent The anscestor element to get broken.
360 * @param {Boolean} [cloneId=false] Whether to preserve ancestor ID attributes while breaking.
361 */
362 breakParent: function( parent, cloneId ) {
363 var range = new CKEDITOR.dom.range( this.getDocument() );
364
365 // We'll be extracting part of this element, so let's use our
366 // range to get the correct piece.
367 range.setStartAfter( this );
368 range.setEndAfter( parent );
369
370 // Extract it.
371 var docFrag = range.extractContents( false, cloneId || false ),
372 tmpElement,
373 current;
374
375 // Move the element outside the broken element.
376 range.insertNode( this.remove() );
377
378 // In case of Internet Explorer, we must check if there is no background-color
379 // added to the element. In such case, we have to overwrite it to prevent "switching it off"
380 // by a browser (#14667).
381 if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) {
382 tmpElement = new CKEDITOR.dom.element( 'div' );
383
384 while ( current = docFrag.getFirst() ) {
385 if ( current.$.style.backgroundColor ) {
386 // This is a necessary hack to make sure that IE will track backgroundColor CSS property, see
387 // http://dev.ckeditor.com/ticket/14667#comment:8 for more details.
388 current.$.style.backgroundColor = current.$.style.backgroundColor;
389 }
390
391 tmpElement.append( current );
392 }
393
394 // Re-insert the extracted piece after the element.
395 tmpElement.insertAfter( this );
396 tmpElement.remove( true );
397 } else {
398 // Re-insert the extracted piece after the element.
399 docFrag.insertAfterNode( this );
400 }
401 },
402
403 /**
404 * Checks if this element contains given node.
405 *
406 * @method
407 * @param {CKEDITOR.dom.node} node
408 * @returns {Boolean}
409 */
410 contains: !document.compareDocumentPosition ?
411 function( node ) {
412 var $ = this.$;
413
414 return node.type != CKEDITOR.NODE_ELEMENT ? $.contains( node.getParent().$ ) : $ != node.$ && $.contains( node.$ );
415 } : function( node ) {
416 return !!( this.$.compareDocumentPosition( node.$ ) & 16 );
417 },
418
419 /**
420 * Moves the selection focus to this element.
421 *
422 * var element = CKEDITOR.document.getById( 'myTextarea' );
423 * element.focus();
424 *
425 * @method
426 * @param {Boolean} defer Whether to asynchronously defer the
427 * execution by 100 ms.
428 */
429 focus: ( function() {
430 function exec() {
431 // IE throws error if the element is not visible.
432 try {
433 this.$.focus();
434 } catch ( e ) {}
435 }
436
437 return function( defer ) {
438 if ( defer )
439 CKEDITOR.tools.setTimeout( exec, 100, this );
440 else
441 exec.call( this );
442 };
443 } )(),
444
445 /**
446 * Gets the inner HTML of this element.
447 *
448 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
449 * alert( element.getHtml() ); // '<b>Example</b>'
450 *
451 * @returns {String} The inner HTML of this element.
452 */
453 getHtml: function() {
454 var retval = this.$.innerHTML;
455 // Strip <?xml:namespace> tags in IE. (#3341).
456 return CKEDITOR.env.ie ? retval.replace( /<\?[^>]*>/g, '' ) : retval;
457 },
458
459 /**
460 * Gets the outer (inner plus tags) HTML of this element.
461 *
462 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="bold"><b>Example</b></div>' );
463 * alert( element.getOuterHtml() ); // '<div class="bold"><b>Example</b></div>'
464 *
465 * @returns {String} The outer HTML of this element.
466 */
467 getOuterHtml: function() {
468 if ( this.$.outerHTML ) {
469 // IE includes the <?xml:namespace> tag in the outerHTML of
470 // namespaced element. So, we must strip it here. (#3341)
471 return this.$.outerHTML.replace( /<\?[^>]*>/, '' );
472 }
473
474 var tmpDiv = this.$.ownerDocument.createElement( 'div' );
475 tmpDiv.appendChild( this.$.cloneNode( true ) );
476 return tmpDiv.innerHTML;
477 },
478
479 /**
480 * Retrieve the bounding rectangle of the current element, in pixels,
481 * relative to the upper-left corner of the browser's client area.
482 *
483 * @returns {Object} The dimensions of the DOM element including
484 * `left`, `top`, `right`, `bottom`, `width` and `height`.
485 */
486 getClientRect: function() {
487 // http://help.dottoro.com/ljvmcrrn.php
488 var rect = CKEDITOR.tools.extend( {}, this.$.getBoundingClientRect() );
489
490 !rect.width && ( rect.width = rect.right - rect.left );
491 !rect.height && ( rect.height = rect.bottom - rect.top );
492
493 return rect;
494 },
495
496 /**
497 * Sets the inner HTML of this element.
498 *
499 * var p = new CKEDITOR.dom.element( 'p' );
500 * p.setHtml( '<b>Inner</b> HTML' );
501 *
502 * // Result: '<p><b>Inner</b> HTML</p>'
503 *
504 * @method
505 * @param {String} html The HTML to be set for this element.
506 * @returns {String} The inserted HTML.
507 */
508 setHtml: ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) ?
509 // old IEs throws error on HTML manipulation (through the "innerHTML" property)
510 // on the element which resides in an DTD invalid position, e.g. <span><div></div></span>
511 // fortunately it can be worked around with DOM manipulation.
512 function( html ) {
513 try {
514 var $ = this.$;
515
516 // Fix the case when setHtml is called on detached element.
517 // HTML5 shiv used for document in which this element was created
518 // won't affect that detached element. So get document fragment with
519 // all HTML5 elements enabled and set innerHTML while this element is appended to it.
520 if ( this.getParent() )
521 return ( $.innerHTML = html );
522 else {
523 var $frag = this.getDocument()._getHtml5ShivFrag();
524 $frag.appendChild( $ );
525 $.innerHTML = html;
526 $frag.removeChild( $ );
527
528 return html;
529 }
530 }
531 catch ( e ) {
532 this.$.innerHTML = '';
533
534 var temp = new CKEDITOR.dom.element( 'body', this.getDocument() );
535 temp.$.innerHTML = html;
536
537 var children = temp.getChildren();
538 while ( children.count() )
539 this.append( children.getItem( 0 ) );
540
541 return html;
542 }
543 } : function( html ) {
544 return ( this.$.innerHTML = html );
545 },
546
547 /**
548 * Sets the element contents as plain text.
549 *
550 * var element = new CKEDITOR.dom.element( 'div' );
551 * element.setText( 'A > B & C < D' );
552 * alert( element.innerHTML ); // 'A &gt; B &amp; C &lt; D'
553 *
554 * @param {String} text The text to be set.
555 * @returns {String} The inserted text.
556 */
557 setText: ( function() {
558 var supportsTextContent = document.createElement( 'p' );
559 supportsTextContent.innerHTML = 'x';
560 supportsTextContent = supportsTextContent.textContent;
561
562 return function( text ) {
563 this.$[ supportsTextContent ? 'textContent' : 'innerText' ] = text;
564 };
565 } )(),
566
567 /**
568 * Gets the value of an element attribute.
569 *
570 * var element = CKEDITOR.dom.element.createFromHtml( '<input type="text" />' );
571 * alert( element.getAttribute( 'type' ) ); // 'text'
572 *
573 * @method
574 * @param {String} name The attribute name.
575 * @returns {String} The attribute value or null if not defined.
576 */
577 getAttribute: ( function() {
578 var standard = function( name ) {
579 return this.$.getAttribute( name, 2 );
580 };
581
582 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
583 return function( name ) {
584 switch ( name ) {
585 case 'class':
586 name = 'className';
587 break;
588
589 case 'http-equiv':
590 name = 'httpEquiv';
591 break;
592
593 case 'name':
594 return this.$.name;
595
596 case 'tabindex':
597 var tabIndex = standard.call( this, name );
598
599 // IE returns tabIndex=0 by default for all
600 // elements. For those elements,
601 // getAtrribute( 'tabindex', 2 ) returns 32768
602 // instead. So, we must make this check to give a
603 // uniform result among all browsers.
604 if ( tabIndex !== 0 && this.$.tabIndex === 0 )
605 tabIndex = null;
606
607 return tabIndex;
608
609 case 'checked':
610 var attr = this.$.attributes.getNamedItem( name ),
611 attrValue = attr.specified ? attr.nodeValue // For value given by parser.
612 : this.$.checked; // For value created via DOM interface.
613
614 return attrValue ? 'checked' : null;
615
616 case 'hspace':
617 case 'value':
618 return this.$[ name ];
619
620 case 'style':
621 // IE does not return inline styles via getAttribute(). See #2947.
622 return this.$.style.cssText;
623
624 case 'contenteditable':
625 case 'contentEditable':
626 return this.$.attributes.getNamedItem( 'contentEditable' ).specified ? this.$.getAttribute( 'contentEditable' ) : null;
627 }
628
629 return standard.call( this, name );
630 };
631 } else {
632 return standard;
633 }
634 } )(),
635
636 /**
637 * Gets the values of all element attributes.
638 *
639 * @param {Array} exclude The names of attributes to be excluded from the returned object.
640 * @return {Object} An object containing all element attributes with their values.
641 */
642 getAttributes: function( exclude ) {
643 var attributes = {},
644 attrDefs = this.$.attributes,
645 i;
646
647 exclude = CKEDITOR.tools.isArray( exclude ) ? exclude : [];
648
649 for ( i = 0; i < attrDefs.length; i++ ) {
650 if ( CKEDITOR.tools.indexOf( exclude, attrDefs[ i ].name ) === -1 ) {
651 attributes[ attrDefs[ i ].name ] = attrDefs[ i ].value;
652 }
653 }
654
655 return attributes;
656 },
657
658 /**
659 * Gets the nodes list containing all children of this element.
660 *
661 * @returns {CKEDITOR.dom.nodeList}
662 */
663 getChildren: function() {
664 return new CKEDITOR.dom.nodeList( this.$.childNodes );
665 },
666
667 /**
668 * Gets the current computed value of one of the element CSS style
669 * properties.
670 *
671 * var element = new CKEDITOR.dom.element( 'span' );
672 * alert( element.getComputedStyle( 'display' ) ); // 'inline'
673 *
674 * @method
675 * @param {String} propertyName The style property name.
676 * @returns {String} The property value.
677 */
678 getComputedStyle: ( document.defaultView && document.defaultView.getComputedStyle ) ?
679 function( propertyName ) {
680 var style = this.getWindow().$.getComputedStyle( this.$, null );
681
682 // Firefox may return null if we call the above on a hidden iframe. (#9117)
683 return style ? style.getPropertyValue( propertyName ) : '';
684 } : function( propertyName ) {
685 return this.$.currentStyle[ CKEDITOR.tools.cssStyleToDomStyle( propertyName ) ];
686 },
687
688 /**
689 * Gets the DTD entries for this element.
690 *
691 * @returns {Object} An object containing the list of elements accepted
692 * by this element.
693 */
694 getDtd: function() {
695 var dtd = CKEDITOR.dtd[ this.getName() ];
696
697 this.getDtd = function() {
698 return dtd;
699 };
700
701 return dtd;
702 },
703
704 /**
705 * Gets all this element's descendants having given tag name.
706 *
707 * @method
708 * @param {String} tagName
709 */
710 getElementsByTag: CKEDITOR.dom.document.prototype.getElementsByTag,
711
712 /**
713 * Gets the computed tabindex for this element.
714 *
715 * var element = CKEDITOR.document.getById( 'myDiv' );
716 * alert( element.getTabIndex() ); // (e.g.) '-1'
717 *
718 * @method
719 * @returns {Number} The tabindex value.
720 */
721 getTabIndex: function() {
722 var tabIndex = this.$.tabIndex;
723
724 // IE returns tabIndex=0 by default for all elements. In
725 // those cases we must check that the element really has
726 // the tabindex attribute set to zero, or it is one of
727 // those element that should have zero by default.
728 if ( tabIndex === 0 && !CKEDITOR.dtd.$tabIndex[ this.getName() ] && parseInt( this.getAttribute( 'tabindex' ), 10 ) !== 0 )
729 return -1;
730
731 return tabIndex;
732 },
733
734 /**
735 * Gets the text value of this element.
736 *
737 * Only in IE (which uses innerText), `<br>` will cause linebreaks,
738 * and sucessive whitespaces (including line breaks) will be reduced to
739 * a single space. This behavior is ok for us, for now. It may change
740 * in the future.
741 *
742 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Sample <i>text</i>.</div>' );
743 * alert( <b>element.getText()</b> ); // 'Sample text.'
744 *
745 * @returns {String} The text value.
746 */
747 getText: function() {
748 return this.$.textContent || this.$.innerText || '';
749 },
750
751 /**
752 * Gets the window object that contains this element.
753 *
754 * @returns {CKEDITOR.dom.window} The window object.
755 */
756 getWindow: function() {
757 return this.getDocument().getWindow();
758 },
759
760 /**
761 * Gets the value of the `id` attribute of this element.
762 *
763 * var element = CKEDITOR.dom.element.createFromHtml( '<p id="myId"></p>' );
764 * alert( element.getId() ); // 'myId'
765 *
766 * @returns {String} The element id, or null if not available.
767 */
768 getId: function() {
769 return this.$.id || null;
770 },
771
772 /**
773 * Gets the value of the `name` attribute of this element.
774 *
775 * var element = CKEDITOR.dom.element.createFromHtml( '<input name="myName"></input>' );
776 * alert( <b>element.getNameAtt()</b> ); // 'myName'
777 *
778 * @returns {String} The element name, or null if not available.
779 */
780 getNameAtt: function() {
781 return this.$.name || null;
782 },
783
784 /**
785 * Gets the element name (tag name). The returned name is guaranteed to
786 * be always full lowercased.
787 *
788 * var element = new CKEDITOR.dom.element( 'span' );
789 * alert( element.getName() ); // 'span'
790 *
791 * @returns {String} The element name.
792 */
793 getName: function() {
794 // Cache the lowercased name inside a closure.
795 var nodeName = this.$.nodeName.toLowerCase();
796
797 if ( CKEDITOR.env.ie && ( document.documentMode <= 8 ) ) {
798 var scopeName = this.$.scopeName;
799 if ( scopeName != 'HTML' )
800 nodeName = scopeName.toLowerCase() + ':' + nodeName;
801 }
802
803 this.getName = function() {
804 return nodeName;
805 };
806
807 return this.getName();
808 },
809
810 /**
811 * Gets the value set to this element. This value is usually available
812 * for form field elements.
813 *
814 * @returns {String} The element value.
815 */
816 getValue: function() {
817 return this.$.value;
818 },
819
820 /**
821 * Gets the first child node of this element.
822 *
823 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
824 * var first = element.getFirst();
825 * alert( first.getName() ); // 'b'
826 *
827 * @param {Function} evaluator Filtering the result node.
828 * @returns {CKEDITOR.dom.node} The first child node or null if not available.
829 */
830 getFirst: function( evaluator ) {
831 var first = this.$.firstChild,
832 retval = first && new CKEDITOR.dom.node( first );
833 if ( retval && evaluator && !evaluator( retval ) )
834 retval = retval.getNext( evaluator );
835
836 return retval;
837 },
838
839 /**
840 * See {@link #getFirst}.
841 *
842 * @param {Function} evaluator Filtering the result node.
843 * @returns {CKEDITOR.dom.node}
844 */
845 getLast: function( evaluator ) {
846 var last = this.$.lastChild,
847 retval = last && new CKEDITOR.dom.node( last );
848 if ( retval && evaluator && !evaluator( retval ) )
849 retval = retval.getPrevious( evaluator );
850
851 return retval;
852 },
853
854 /**
855 * Gets CSS style value.
856 *
857 * @param {String} name The CSS property name.
858 * @returns {String} Style value.
859 */
860 getStyle: function( name ) {
861 return this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ];
862 },
863
864 /**
865 * Checks if the element name matches the specified criteria.
866 *
867 * var element = new CKEDITOR.element( 'span' );
868 * alert( element.is( 'span' ) ); // true
869 * alert( element.is( 'p', 'span' ) ); // true
870 * alert( element.is( 'p' ) ); // false
871 * alert( element.is( 'p', 'div' ) ); // false
872 * alert( element.is( { p:1,span:1 } ) ); // true
873 *
874 * @param {String.../Object} name One or more names to be checked, or a {@link CKEDITOR.dtd} object.
875 * @returns {Boolean} `true` if the element name matches any of the names.
876 */
877 is: function() {
878 var name = this.getName();
879
880 // Check against the specified DTD liternal.
881 if ( typeof arguments[ 0 ] == 'object' )
882 return !!arguments[ 0 ][ name ];
883
884 // Check for tag names
885 for ( var i = 0; i < arguments.length; i++ ) {
886 if ( arguments[ i ] == name )
887 return true;
888 }
889 return false;
890 },
891
892 /**
893 * Decide whether one element is able to receive cursor.
894 *
895 * @param {Boolean} [textCursor=true] Only consider element that could receive text child.
896 */
897 isEditable: function( textCursor ) {
898 var name = this.getName();
899
900 if ( this.isReadOnly() || this.getComputedStyle( 'display' ) == 'none' ||
901 this.getComputedStyle( 'visibility' ) == 'hidden' ||
902 CKEDITOR.dtd.$nonEditable[ name ] ||
903 CKEDITOR.dtd.$empty[ name ] ||
904 ( this.is( 'a' ) &&
905 ( this.data( 'cke-saved-name' ) || this.hasAttribute( 'name' ) ) &&
906 !this.getChildCount()
907 ) ) {
908 return false;
909 }
910
911 if ( textCursor !== false ) {
912 // Get the element DTD (defaults to span for unknown elements).
913 var dtd = CKEDITOR.dtd[ name ] || CKEDITOR.dtd.span;
914 // In the DTD # == text node.
915 return !!( dtd && dtd[ '#' ] );
916 }
917
918 return true;
919 },
920
921 /**
922 * Compare this element's inner html, tag name, attributes, etc. with other one.
923 *
924 * See [W3C's DOM Level 3 spec - node#isEqualNode](http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-isEqualNode)
925 * for more details.
926 *
927 * @param {CKEDITOR.dom.element} otherElement Element to compare.
928 * @returns {Boolean}
929 */
930 isIdentical: function( otherElement ) {
931 // do shallow clones, but with IDs
932 var thisEl = this.clone( 0, 1 ),
933 otherEl = otherElement.clone( 0, 1 );
934
935 // Remove distractions.
936 thisEl.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] );
937 otherEl.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] );
938
939 // Native comparison available.
940 if ( thisEl.$.isEqualNode ) {
941 // Styles order matters.
942 thisEl.$.style.cssText = CKEDITOR.tools.normalizeCssText( thisEl.$.style.cssText );
943 otherEl.$.style.cssText = CKEDITOR.tools.normalizeCssText( otherEl.$.style.cssText );
944 return thisEl.$.isEqualNode( otherEl.$ );
945 } else {
946 thisEl = thisEl.getOuterHtml();
947 otherEl = otherEl.getOuterHtml();
948
949 // Fix tiny difference between link href in older IEs.
950 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && this.is( 'a' ) ) {
951 var parent = this.getParent();
952 if ( parent.type == CKEDITOR.NODE_ELEMENT ) {
953 var el = parent.clone();
954 el.setHtml( thisEl ), thisEl = el.getHtml();
955 el.setHtml( otherEl ), otherEl = el.getHtml();
956 }
957 }
958
959 return thisEl == otherEl;
960 }
961 },
962
963 /**
964 * Checks if this element is visible. May not work if the element is
965 * child of an element with visibility set to `hidden`, but works well
966 * on the great majority of cases.
967 *
968 * @returns {Boolean} True if the element is visible.
969 */
970 isVisible: function() {
971 var isVisible = ( this.$.offsetHeight || this.$.offsetWidth ) && this.getComputedStyle( 'visibility' ) != 'hidden',
972 elementWindow, elementWindowFrame;
973
974 // Webkit and Opera report non-zero offsetHeight despite that
975 // element is inside an invisible iframe. (#4542)
976 if ( isVisible && CKEDITOR.env.webkit ) {
977 elementWindow = this.getWindow();
978
979 if ( !elementWindow.equals( CKEDITOR.document.getWindow() ) && ( elementWindowFrame = elementWindow.$.frameElement ) )
980 isVisible = new CKEDITOR.dom.element( elementWindowFrame ).isVisible();
981
982 }
983
984 return !!isVisible;
985 },
986
987 /**
988 * Whether it's an empty inline elements which has no visual impact when removed.
989 *
990 * @returns {Boolean}
991 */
992 isEmptyInlineRemoveable: function() {
993 if ( !CKEDITOR.dtd.$removeEmpty[ this.getName() ] )
994 return false;
995
996 var children = this.getChildren();
997 for ( var i = 0, count = children.count(); i < count; i++ ) {
998 var child = children.getItem( i );
999
1000 if ( child.type == CKEDITOR.NODE_ELEMENT && child.data( 'cke-bookmark' ) )
1001 continue;
1002
1003 if ( child.type == CKEDITOR.NODE_ELEMENT && !child.isEmptyInlineRemoveable() || child.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( child.getText() ) )
1004 return false;
1005
1006 }
1007 return true;
1008 },
1009
1010 /**
1011 * Checks if the element has any defined attributes.
1012 *
1013 * var element = CKEDITOR.dom.element.createFromHtml( '<div title="Test">Example</div>' );
1014 * alert( element.hasAttributes() ); // true
1015 *
1016 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Example</div>' );
1017 * alert( element.hasAttributes() ); // false
1018 *
1019 * @method
1020 * @returns {Boolean} True if the element has attributes.
1021 */
1022 hasAttributes: CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ?
1023 function() {
1024 var attributes = this.$.attributes;
1025
1026 for ( var i = 0; i < attributes.length; i++ ) {
1027 var attribute = attributes[ i ];
1028
1029 switch ( attribute.nodeName ) {
1030 case 'class':
1031 // IE has a strange bug. If calling removeAttribute('className'),
1032 // the attributes collection will still contain the "class"
1033 // attribute, which will be marked as "specified", even if the
1034 // outerHTML of the element is not displaying the class attribute.
1035 // Note : I was not able to reproduce it outside the editor,
1036 // but I've faced it while working on the TC of #1391.
1037 if ( this.getAttribute( 'class' ) ) {
1038 return true;
1039 }
1040
1041 // Attributes to be ignored.
1042 /* falls through */
1043 case 'data-cke-expando':
1044 continue;
1045
1046
1047 /* falls through */
1048 default:
1049 if ( attribute.specified ) {
1050 return true;
1051 }
1052 }
1053 }
1054
1055 return false;
1056 } : function() {
1057 var attrs = this.$.attributes,
1058 attrsNum = attrs.length;
1059
1060 // The _moz_dirty attribute might get into the element after pasting (#5455)
1061 var execludeAttrs = { 'data-cke-expando': 1, _moz_dirty: 1 };
1062
1063 return attrsNum > 0 && ( attrsNum > 2 || !execludeAttrs[ attrs[ 0 ].nodeName ] || ( attrsNum == 2 && !execludeAttrs[ attrs[ 1 ].nodeName ] ) );
1064 },
1065
1066 /**
1067 * Checks if the specified attribute is defined for this element.
1068 *
1069 * @method
1070 * @param {String} name The attribute name.
1071 * @returns {Boolean} `true` if the specified attribute is defined.
1072 */
1073 hasAttribute: ( function() {
1074 function ieHasAttribute( name ) {
1075 var $attr = this.$.attributes.getNamedItem( name );
1076
1077 if ( this.getName() == 'input' ) {
1078 switch ( name ) {
1079 case 'class':
1080 return this.$.className.length > 0;
1081 case 'checked':
1082 return !!this.$.checked;
1083 case 'value':
1084 var type = this.getAttribute( 'type' );
1085 return type == 'checkbox' || type == 'radio' ? this.$.value != 'on' : !!this.$.value;
1086 }
1087 }
1088
1089 if ( !$attr )
1090 return false;
1091
1092 return $attr.specified;
1093 }
1094
1095 if ( CKEDITOR.env.ie ) {
1096 if ( CKEDITOR.env.version < 8 ) {
1097 return function( name ) {
1098 // On IE < 8 the name attribute cannot be retrieved
1099 // right after the element creation and setting the
1100 // name with setAttribute.
1101 if ( name == 'name' )
1102 return !!this.$.name;
1103
1104 return ieHasAttribute.call( this, name );
1105 };
1106 } else {
1107 return ieHasAttribute;
1108 }
1109 } else {
1110 return function( name ) {
1111 // On other browsers specified property is deprecated and return always true,
1112 // but fortunately $.attributes contains only specified attributes.
1113 return !!this.$.attributes.getNamedItem( name );
1114 };
1115 }
1116 } )(),
1117
1118 /**
1119 * Hides this element (sets `display: none`).
1120 *
1121 * var element = CKEDITOR.document.getById( 'myElement' );
1122 * element.hide();
1123 */
1124 hide: function() {
1125 this.setStyle( 'display', 'none' );
1126 },
1127
1128 /**
1129 * Moves this element's children to the target element.
1130 *
1131 * @param {CKEDITOR.dom.element} target
1132 * @param {Boolean} [toStart=false] Insert moved children at the
1133 * beginning of the target element.
1134 */
1135 moveChildren: function( target, toStart ) {
1136 var $ = this.$;
1137 target = target.$;
1138
1139 if ( $ == target )
1140 return;
1141
1142 var child;
1143
1144 if ( toStart ) {
1145 while ( ( child = $.lastChild ) )
1146 target.insertBefore( $.removeChild( child ), target.firstChild );
1147 } else {
1148 while ( ( child = $.firstChild ) )
1149 target.appendChild( $.removeChild( child ) );
1150 }
1151 },
1152
1153 /**
1154 * Merges sibling elements that are identical to this one.
1155 *
1156 * Identical child elements are also merged. For example:
1157 *
1158 * <b><i></i></b><b><i></i></b> => <b><i></i></b>
1159 *
1160 * @method
1161 * @param {Boolean} [inlineOnly=true] Allow only inline elements to be merged.
1162 */
1163 mergeSiblings: ( function() {
1164 function mergeElements( element, sibling, isNext ) {
1165 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) {
1166 // Jumping over bookmark nodes and empty inline elements, e.g. <b><i></i></b>,
1167 // queuing them to be moved later. (#5567)
1168 var pendingNodes = [];
1169
1170 while ( sibling.data( 'cke-bookmark' ) || sibling.isEmptyInlineRemoveable() ) {
1171 pendingNodes.push( sibling );
1172 sibling = isNext ? sibling.getNext() : sibling.getPrevious();
1173 if ( !sibling || sibling.type != CKEDITOR.NODE_ELEMENT )
1174 return;
1175 }
1176
1177 if ( element.isIdentical( sibling ) ) {
1178 // Save the last child to be checked too, to merge things like
1179 // <b><i></i></b><b><i></i></b> => <b><i></i></b>
1180 var innerSibling = isNext ? element.getLast() : element.getFirst();
1181
1182 // Move pending nodes first into the target element.
1183 while ( pendingNodes.length )
1184 pendingNodes.shift().move( element, !isNext );
1185
1186 sibling.moveChildren( element, !isNext );
1187 sibling.remove();
1188
1189 // Now check the last inner child (see two comments above).
1190 if ( innerSibling && innerSibling.type == CKEDITOR.NODE_ELEMENT )
1191 innerSibling.mergeSiblings();
1192 }
1193 }
1194 }
1195
1196 return function( inlineOnly ) {
1197 // Merge empty links and anchors also. (#5567)
1198 if ( !( inlineOnly === false || CKEDITOR.dtd.$removeEmpty[ this.getName() ] || this.is( 'a' ) ) ) {
1199 return;
1200 }
1201
1202 mergeElements( this, this.getNext(), true );
1203 mergeElements( this, this.getPrevious() );
1204 };
1205 } )(),
1206
1207 /**
1208 * Shows this element (displays it).
1209 *
1210 * var element = CKEDITOR.document.getById( 'myElement' );
1211 * element.show();
1212 */
1213 show: function() {
1214 this.setStyles( {
1215 display: '',
1216 visibility: ''
1217 } );
1218 },
1219
1220 /**
1221 * Sets the value of an element attribute.
1222 *
1223 * var element = CKEDITOR.document.getById( 'myElement' );
1224 * element.setAttribute( 'class', 'myClass' );
1225 * element.setAttribute( 'title', 'This is an example' );
1226 *
1227 * @method
1228 * @param {String} name The name of the attribute.
1229 * @param {String} value The value to be set to the attribute.
1230 * @returns {CKEDITOR.dom.element} This element instance.
1231 */
1232 setAttribute: ( function() {
1233 var standard = function( name, value ) {
1234 this.$.setAttribute( name, value );
1235 return this;
1236 };
1237
1238 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
1239 return function( name, value ) {
1240 if ( name == 'class' )
1241 this.$.className = value;
1242 else if ( name == 'style' )
1243 this.$.style.cssText = value;
1244 else if ( name == 'tabindex' ) // Case sensitive.
1245 this.$.tabIndex = value;
1246 else if ( name == 'checked' )
1247 this.$.checked = value;
1248 else if ( name == 'contenteditable' )
1249 standard.call( this, 'contentEditable', value );
1250 else
1251 standard.apply( this, arguments );
1252 return this;
1253 };
1254 } else if ( CKEDITOR.env.ie8Compat && CKEDITOR.env.secure ) {
1255 return function( name, value ) {
1256 // IE8 throws error when setting src attribute to non-ssl value. (#7847)
1257 if ( name == 'src' && value.match( /^http:\/\// ) ) {
1258 try {
1259 standard.apply( this, arguments );
1260 } catch ( e ) {}
1261 } else {
1262 standard.apply( this, arguments );
1263 }
1264 return this;
1265 };
1266 } else {
1267 return standard;
1268 }
1269 } )(),
1270
1271 /**
1272 * Sets the value of several element attributes.
1273 *
1274 * var element = CKEDITOR.document.getById( 'myElement' );
1275 * element.setAttributes( {
1276 * 'class': 'myClass',
1277 * title: 'This is an example'
1278 * } );
1279 *
1280 * @chainable
1281 * @param {Object} attributesPairs An object containing the names and
1282 * values of the attributes.
1283 * @returns {CKEDITOR.dom.element} This element instance.
1284 */
1285 setAttributes: function( attributesPairs ) {
1286 for ( var name in attributesPairs )
1287 this.setAttribute( name, attributesPairs[ name ] );
1288 return this;
1289 },
1290
1291 /**
1292 * Sets the element value. This function is usually used with form
1293 * field element.
1294 *
1295 * @chainable
1296 * @param {String} value The element value.
1297 * @returns {CKEDITOR.dom.element} This element instance.
1298 */
1299 setValue: function( value ) {
1300 this.$.value = value;
1301 return this;
1302 },
1303
1304 /**
1305 * Removes an attribute from the element.
1306 *
1307 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="classA"></div>' );
1308 * element.removeAttribute( 'class' );
1309 *
1310 * @method
1311 * @param {String} name The attribute name.
1312 */
1313 removeAttribute: ( function() {
1314 var standard = function( name ) {
1315 this.$.removeAttribute( name );
1316 };
1317
1318 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
1319 return function( name ) {
1320 if ( name == 'class' )
1321 name = 'className';
1322 else if ( name == 'tabindex' )
1323 name = 'tabIndex';
1324 else if ( name == 'contenteditable' )
1325 name = 'contentEditable';
1326 standard.call( this, name );
1327 };
1328 } else {
1329 return standard;
1330 }
1331 } )(),
1332
1333 /**
1334 * Removes all element's attributes or just given ones.
1335 *
1336 * @param {Array} [attributes] The array with attributes names.
1337 */
1338 removeAttributes: function( attributes ) {
1339 if ( CKEDITOR.tools.isArray( attributes ) ) {
1340 for ( var i = 0; i < attributes.length; i++ ) {
1341 this.removeAttribute( attributes[ i ] );
1342 }
1343 } else {
1344 attributes = attributes || this.getAttributes();
1345
1346 for ( var attr in attributes ) {
1347 attributes.hasOwnProperty( attr ) && this.removeAttribute( attr );
1348 }
1349 }
1350 },
1351
1352 /**
1353 * Removes a style from the element.
1354 *
1355 * var element = CKEDITOR.dom.element.createFromHtml( '<div style="display:none"></div>' );
1356 * element.removeStyle( 'display' );
1357 *
1358 * @method
1359 * @param {String} name The style name.
1360 */
1361 removeStyle: function( name ) {
1362 // Removes the specified property from the current style object.
1363 var $ = this.$.style;
1364
1365 // "removeProperty" need to be specific on the following styles.
1366 if ( !$.removeProperty && ( name == 'border' || name == 'margin' || name == 'padding' ) ) {
1367 var names = expandedRules( name );
1368 for ( var i = 0 ; i < names.length ; i++ )
1369 this.removeStyle( names[ i ] );
1370 return;
1371 }
1372
1373 $.removeProperty ? $.removeProperty( name ) : $.removeAttribute( CKEDITOR.tools.cssStyleToDomStyle( name ) );
1374
1375 // Eventually remove empty style attribute.
1376 if ( !this.$.style.cssText )
1377 this.removeAttribute( 'style' );
1378 },
1379
1380 /**
1381 * Sets the value of an element style.
1382 *
1383 * var element = CKEDITOR.document.getById( 'myElement' );
1384 * element.setStyle( 'background-color', '#ff0000' );
1385 * element.setStyle( 'margin-top', '10px' );
1386 * element.setStyle( 'float', 'right' );
1387 *
1388 * @param {String} name The name of the style. The CSS naming notation
1389 * must be used (e.g. `background-color`).
1390 * @param {String} value The value to be set to the style.
1391 * @returns {CKEDITOR.dom.element} This element instance.
1392 */
1393 setStyle: function( name, value ) {
1394 this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ] = value;
1395 return this;
1396 },
1397
1398 /**
1399 * Sets the value of several element styles.
1400 *
1401 * var element = CKEDITOR.document.getById( 'myElement' );
1402 * element.setStyles( {
1403 * position: 'absolute',
1404 * float: 'right'
1405 * } );
1406 *
1407 * @param {Object} stylesPairs An object containing the names and
1408 * values of the styles.
1409 * @returns {CKEDITOR.dom.element} This element instance.
1410 */
1411 setStyles: function( stylesPairs ) {
1412 for ( var name in stylesPairs )
1413 this.setStyle( name, stylesPairs[ name ] );
1414 return this;
1415 },
1416
1417 /**
1418 * Sets the opacity of an element.
1419 *
1420 * var element = CKEDITOR.document.getById( 'myElement' );
1421 * element.setOpacity( 0.75 );
1422 *
1423 * @param {Number} opacity A number within the range `[0.0, 1.0]`.
1424 */
1425 setOpacity: function( opacity ) {
1426 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
1427 opacity = Math.round( opacity * 100 );
1428 this.setStyle( 'filter', opacity >= 100 ? '' : 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + opacity + ')' );
1429 } else {
1430 this.setStyle( 'opacity', opacity );
1431 }
1432 },
1433
1434 /**
1435 * Makes the element and its children unselectable.
1436 *
1437 * var element = CKEDITOR.document.getById( 'myElement' );
1438 * element.unselectable();
1439 *
1440 * @method
1441 */
1442 unselectable: function() {
1443 // CSS unselectable.
1444 this.setStyles( CKEDITOR.tools.cssVendorPrefix( 'user-select', 'none' ) );
1445
1446 // For IE/Opera which doesn't support for the above CSS style,
1447 // the unselectable="on" attribute only specifies the selection
1448 // process cannot start in the element itself, and it doesn't inherit.
1449 if ( CKEDITOR.env.ie ) {
1450 this.setAttribute( 'unselectable', 'on' );
1451
1452 var element,
1453 elements = this.getElementsByTag( '*' );
1454
1455 for ( var i = 0, count = elements.count() ; i < count ; i++ ) {
1456 element = elements.getItem( i );
1457 element.setAttribute( 'unselectable', 'on' );
1458 }
1459 }
1460 },
1461
1462 /**
1463 * Gets closest positioned (`position != static`) ancestor.
1464 *
1465 * @returns {CKEDITOR.dom.element} Positioned ancestor or `null`.
1466 */
1467 getPositionedAncestor: function() {
1468 var current = this;
1469 while ( current.getName() != 'html' ) {
1470 if ( current.getComputedStyle( 'position' ) != 'static' )
1471 return current;
1472
1473 current = current.getParent();
1474 }
1475 return null;
1476 },
1477
1478 /**
1479 * Gets this element's position in document.
1480 *
1481 * @param {CKEDITOR.dom.document} [refDocument]
1482 * @returns {Object} Element's position.
1483 * @returns {Number} return.x
1484 * @returns {Number} return.y
1485 * @todo refDocument
1486 */
1487 getDocumentPosition: function( refDocument ) {
1488 var x = 0,
1489 y = 0,
1490 doc = this.getDocument(),
1491 body = doc.getBody(),
1492 quirks = doc.$.compatMode == 'BackCompat';
1493
1494 if ( document.documentElement.getBoundingClientRect &&
1495 ( CKEDITOR.env.ie ? CKEDITOR.env.version !== 8 : true ) ) {
1496 var box = this.$.getBoundingClientRect(),
1497 $doc = doc.$,
1498 $docElem = $doc.documentElement;
1499
1500 var clientTop = $docElem.clientTop || body.$.clientTop || 0,
1501 clientLeft = $docElem.clientLeft || body.$.clientLeft || 0,
1502 needAdjustScrollAndBorders = true;
1503
1504 // #3804: getBoundingClientRect() works differently on IE and non-IE
1505 // browsers, regarding scroll positions.
1506 //
1507 // On IE, the top position of the <html> element is always 0, no matter
1508 // how much you scrolled down.
1509 //
1510 // On other browsers, the top position of the <html> element is negative
1511 // scrollTop.
1512 if ( CKEDITOR.env.ie ) {
1513 var inDocElem = doc.getDocumentElement().contains( this ),
1514 inBody = doc.getBody().contains( this );
1515
1516 needAdjustScrollAndBorders = ( quirks && inBody ) || ( !quirks && inDocElem );
1517 }
1518
1519 // #12747.
1520 if ( needAdjustScrollAndBorders ) {
1521 var scrollRelativeLeft,
1522 scrollRelativeTop;
1523
1524 // See #12758 to know more about document.(documentElement|body).scroll(Left|Top) in Webkit.
1525 if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version >= 12 ) ) {
1526 scrollRelativeLeft = body.$.scrollLeft || $docElem.scrollLeft;
1527 scrollRelativeTop = body.$.scrollTop || $docElem.scrollTop;
1528 } else {
1529 var scrollRelativeElement = quirks ? body.$ : $docElem;
1530
1531 scrollRelativeLeft = scrollRelativeElement.scrollLeft;
1532 scrollRelativeTop = scrollRelativeElement.scrollTop;
1533 }
1534
1535 x = box.left + scrollRelativeLeft - clientLeft;
1536 y = box.top + scrollRelativeTop - clientTop;
1537 }
1538 } else {
1539 var current = this,
1540 previous = null,
1541 offsetParent;
1542 while ( current && !( current.getName() == 'body' || current.getName() == 'html' ) ) {
1543 x += current.$.offsetLeft - current.$.scrollLeft;
1544 y += current.$.offsetTop - current.$.scrollTop;
1545
1546 // Opera includes clientTop|Left into offsetTop|Left.
1547 if ( !current.equals( this ) ) {
1548 x += ( current.$.clientLeft || 0 );
1549 y += ( current.$.clientTop || 0 );
1550 }
1551
1552 var scrollElement = previous;
1553 while ( scrollElement && !scrollElement.equals( current ) ) {
1554 x -= scrollElement.$.scrollLeft;
1555 y -= scrollElement.$.scrollTop;
1556 scrollElement = scrollElement.getParent();
1557 }
1558
1559 previous = current;
1560 current = ( offsetParent = current.$.offsetParent ) ? new CKEDITOR.dom.element( offsetParent ) : null;
1561 }
1562 }
1563
1564 if ( refDocument ) {
1565 var currentWindow = this.getWindow(),
1566 refWindow = refDocument.getWindow();
1567
1568 if ( !currentWindow.equals( refWindow ) && currentWindow.$.frameElement ) {
1569 var iframePosition = ( new CKEDITOR.dom.element( currentWindow.$.frameElement ) ).getDocumentPosition( refDocument );
1570
1571 x += iframePosition.x;
1572 y += iframePosition.y;
1573 }
1574 }
1575
1576 if ( !document.documentElement.getBoundingClientRect ) {
1577 // In Firefox, we'll endup one pixel before the element positions,
1578 // so we must add it here.
1579 if ( CKEDITOR.env.gecko && !quirks ) {
1580 x += this.$.clientLeft ? 1 : 0;
1581 y += this.$.clientTop ? 1 : 0;
1582 }
1583 }
1584
1585 return { x: x, y: y };
1586 },
1587
1588 /**
1589 * Make any page element visible inside the browser viewport.
1590 *
1591 * @param {Boolean} [alignToTop=false]
1592 */
1593 scrollIntoView: function( alignToTop ) {
1594 var parent = this.getParent();
1595 if ( !parent )
1596 return;
1597
1598 // Scroll the element into parent container from the inner out.
1599 do {
1600 // Check ancestors that overflows.
1601 var overflowed =
1602 parent.$.clientWidth && parent.$.clientWidth < parent.$.scrollWidth ||
1603 parent.$.clientHeight && parent.$.clientHeight < parent.$.scrollHeight;
1604
1605 // Skip body element, which will report wrong clientHeight when containing
1606 // floated content. (#9523)
1607 if ( overflowed && !parent.is( 'body' ) )
1608 this.scrollIntoParent( parent, alignToTop, 1 );
1609
1610 // Walk across the frame.
1611 if ( parent.is( 'html' ) ) {
1612 var win = parent.getWindow();
1613
1614 // Avoid security error.
1615 try {
1616 var iframe = win.$.frameElement;
1617 iframe && ( parent = new CKEDITOR.dom.element( iframe ) );
1618 } catch ( er ) {}
1619 }
1620 }
1621 while ( ( parent = parent.getParent() ) );
1622 },
1623
1624 /**
1625 * Make any page element visible inside one of the ancestors by scrolling the parent.
1626 *
1627 * @param {CKEDITOR.dom.element/CKEDITOR.dom.window} parent The container to scroll into.
1628 * @param {Boolean} [alignToTop] Align the element's top side with the container's
1629 * when `true` is specified; align the bottom with viewport bottom when
1630 * `false` is specified. Otherwise scroll on either side with the minimum
1631 * amount to show the element.
1632 * @param {Boolean} [hscroll] Whether horizontal overflow should be considered.
1633 */
1634 scrollIntoParent: function( parent, alignToTop, hscroll ) {
1635 !parent && ( parent = this.getWindow() );
1636
1637 var doc = parent.getDocument();
1638 var isQuirks = doc.$.compatMode == 'BackCompat';
1639
1640 // On window <html> is scrolled while quirks scrolls <body>.
1641 if ( parent instanceof CKEDITOR.dom.window )
1642 parent = isQuirks ? doc.getBody() : doc.getDocumentElement();
1643
1644 // Scroll the parent by the specified amount.
1645 function scrollBy( x, y ) {
1646 // Webkit doesn't support "scrollTop/scrollLeft"
1647 // on documentElement/body element.
1648 if ( /body|html/.test( parent.getName() ) )
1649 parent.getWindow().$.scrollBy( x, y );
1650 else {
1651 parent.$.scrollLeft += x;
1652 parent.$.scrollTop += y;
1653 }
1654 }
1655
1656 // Figure out the element position relative to the specified window.
1657 function screenPos( element, refWin ) {
1658 var pos = { x: 0, y: 0 };
1659
1660 if ( !( element.is( isQuirks ? 'body' : 'html' ) ) ) {
1661 var box = element.$.getBoundingClientRect();
1662 pos.x = box.left, pos.y = box.top;
1663 }
1664
1665 var win = element.getWindow();
1666 if ( !win.equals( refWin ) ) {
1667 var outerPos = screenPos( CKEDITOR.dom.element.get( win.$.frameElement ), refWin );
1668 pos.x += outerPos.x, pos.y += outerPos.y;
1669 }
1670
1671 return pos;
1672 }
1673
1674 // calculated margin size.
1675 function margin( element, side ) {
1676 return parseInt( element.getComputedStyle( 'margin-' + side ) || 0, 10 ) || 0;
1677 }
1678
1679 // [WebKit] Reset stored scrollTop value to not break scrollIntoView() method flow.
1680 // Scrolling breaks when range.select() is used right after element.scrollIntoView(). (#14659)
1681 if ( CKEDITOR.env.webkit ) {
1682 var editor = this.getEditor( false );
1683
1684 if ( editor ) {
1685 editor._.previousScrollTop = null;
1686 }
1687 }
1688
1689 var win = parent.getWindow();
1690
1691 var thisPos = screenPos( this, win ),
1692 parentPos = screenPos( parent, win ),
1693 eh = this.$.offsetHeight,
1694 ew = this.$.offsetWidth,
1695 ch = parent.$.clientHeight,
1696 cw = parent.$.clientWidth,
1697 lt, br;
1698
1699 // Left-top margins.
1700 lt = {
1701 x: thisPos.x - margin( this, 'left' ) - parentPos.x || 0,
1702 y: thisPos.y - margin( this, 'top' ) - parentPos.y || 0
1703 };
1704
1705 // Bottom-right margins.
1706 br = {
1707 x: thisPos.x + ew + margin( this, 'right' ) - ( ( parentPos.x ) + cw ) || 0,
1708 y: thisPos.y + eh + margin( this, 'bottom' ) - ( ( parentPos.y ) + ch ) || 0
1709 };
1710
1711 // 1. Do the specified alignment as much as possible;
1712 // 2. Otherwise be smart to scroll only the minimum amount;
1713 // 3. Never cut at the top;
1714 // 4. DO NOT scroll when already visible.
1715 if ( lt.y < 0 || br.y > 0 )
1716 scrollBy( 0, alignToTop === true ? lt.y : alignToTop === false ? br.y : lt.y < 0 ? lt.y : br.y );
1717
1718 if ( hscroll && ( lt.x < 0 || br.x > 0 ) )
1719 scrollBy( lt.x < 0 ? lt.x : br.x, 0 );
1720 },
1721
1722 /**
1723 * Switch the `class` attribute to reflect one of the triple states of an
1724 * element in one of {@link CKEDITOR#TRISTATE_ON}, {@link CKEDITOR#TRISTATE_OFF}
1725 * or {@link CKEDITOR#TRISTATE_DISABLED}.
1726 *
1727 * link.setState( CKEDITOR.TRISTATE_ON );
1728 * // <a class="cke_on" aria-pressed="true">...</a>
1729 * link.setState( CKEDITOR.TRISTATE_OFF );
1730 * // <a class="cke_off">...</a>
1731 * link.setState( CKEDITOR.TRISTATE_DISABLED );
1732 * // <a class="cke_disabled" aria-disabled="true">...</a>
1733 *
1734 * span.setState( CKEDITOR.TRISTATE_ON, 'cke_button' );
1735 * // <span class="cke_button_on">...</span>
1736 *
1737 * @param {Number} state Indicate the element state. One of {@link CKEDITOR#TRISTATE_ON},
1738 * {@link CKEDITOR#TRISTATE_OFF}, {@link CKEDITOR#TRISTATE_DISABLED}.
1739 * @param [base='cke'] The prefix apply to each of the state class name.
1740 * @param [useAria=true] Whether toggle the ARIA state attributes besides of class name change.
1741 */
1742 setState: function( state, base, useAria ) {
1743 base = base || 'cke';
1744
1745 switch ( state ) {
1746 case CKEDITOR.TRISTATE_ON:
1747 this.addClass( base + '_on' );
1748 this.removeClass( base + '_off' );
1749 this.removeClass( base + '_disabled' );
1750 useAria && this.setAttribute( 'aria-pressed', true );
1751 useAria && this.removeAttribute( 'aria-disabled' );
1752 break;
1753
1754 case CKEDITOR.TRISTATE_DISABLED:
1755 this.addClass( base + '_disabled' );
1756 this.removeClass( base + '_off' );
1757 this.removeClass( base + '_on' );
1758 useAria && this.setAttribute( 'aria-disabled', true );
1759 useAria && this.removeAttribute( 'aria-pressed' );
1760 break;
1761
1762 default:
1763 this.addClass( base + '_off' );
1764 this.removeClass( base + '_on' );
1765 this.removeClass( base + '_disabled' );
1766 useAria && this.removeAttribute( 'aria-pressed' );
1767 useAria && this.removeAttribute( 'aria-disabled' );
1768 break;
1769 }
1770 },
1771
1772 /**
1773 * Returns the inner document of this `<iframe>` element.
1774 *
1775 * @returns {CKEDITOR.dom.document} The inner document.
1776 */
1777 getFrameDocument: function() {
1778 var $ = this.$;
1779
1780 try {
1781 // In IE, with custom document.domain, it may happen that
1782 // the iframe is not yet available, resulting in "Access
1783 // Denied" for the following property access.
1784 $.contentWindow.document;
1785 } catch ( e ) {
1786 // Trick to solve this issue, forcing the iframe to get ready
1787 // by simply setting its "src" property.
1788 $.src = $.src;
1789 }
1790
1791 return $ && new CKEDITOR.dom.document( $.contentWindow.document );
1792 },
1793
1794 /**
1795 * Copy all the attributes from one node to the other, kinda like a clone
1796 * skipAttributes is an object with the attributes that must **not** be copied.
1797 *
1798 * @param {CKEDITOR.dom.element} dest The destination element.
1799 * @param {Object} skipAttributes A dictionary of attributes to skip.
1800 */
1801 copyAttributes: function( dest, skipAttributes ) {
1802 var attributes = this.$.attributes;
1803 skipAttributes = skipAttributes || {};
1804
1805 for ( var n = 0; n < attributes.length; n++ ) {
1806 var attribute = attributes[ n ];
1807
1808 // Lowercase attribute name hard rule is broken for
1809 // some attribute on IE, e.g. CHECKED.
1810 var attrName = attribute.nodeName.toLowerCase(),
1811 attrValue;
1812
1813 // We can set the type only once, so do it with the proper value, not copying it.
1814 if ( attrName in skipAttributes )
1815 continue;
1816
1817 if ( attrName == 'checked' && ( attrValue = this.getAttribute( attrName ) ) )
1818 dest.setAttribute( attrName, attrValue );
1819 // IE contains not specified attributes in $.attributes so we need to check
1820 // if elements attribute is specified using hasAttribute.
1821 else if ( !CKEDITOR.env.ie || this.hasAttribute( attrName ) ) {
1822 attrValue = this.getAttribute( attrName );
1823 if ( attrValue === null )
1824 attrValue = attribute.nodeValue;
1825
1826 dest.setAttribute( attrName, attrValue );
1827 }
1828 }
1829
1830 // The style:
1831 if ( this.$.style.cssText !== '' )
1832 dest.$.style.cssText = this.$.style.cssText;
1833 },
1834
1835 /**
1836 * Changes the tag name of the current element.
1837 *
1838 * @param {String} newTag The new tag for the element.
1839 */
1840 renameNode: function( newTag ) {
1841 // If it's already correct exit here.
1842 if ( this.getName() == newTag )
1843 return;
1844
1845 var doc = this.getDocument();
1846
1847 // Create the new node.
1848 var newNode = new CKEDITOR.dom.element( newTag, doc );
1849
1850 // Copy all attributes.
1851 this.copyAttributes( newNode );
1852
1853 // Move children to the new node.
1854 this.moveChildren( newNode );
1855
1856 // Replace the node.
1857 this.getParent( true ) && this.$.parentNode.replaceChild( newNode.$, this.$ );
1858 newNode.$[ 'data-cke-expando' ] = this.$[ 'data-cke-expando' ];
1859 this.$ = newNode.$;
1860 // Bust getName's cache. (#8663)
1861 delete this.getName;
1862 },
1863
1864 /**
1865 * Gets a DOM tree descendant under the current node.
1866 *
1867 * var strong = p.getChild( 0 );
1868 *
1869 * @method
1870 * @param {Array/Number} indices The child index or array of child indices under the node.
1871 * @returns {CKEDITOR.dom.node} The specified DOM child under the current node. Null if child does not exist.
1872 */
1873 getChild: ( function() {
1874 function getChild( rawNode, index ) {
1875 var childNodes = rawNode.childNodes;
1876
1877 if ( index >= 0 && index < childNodes.length )
1878 return childNodes[ index ];
1879 }
1880
1881 return function( indices ) {
1882 var rawNode = this.$;
1883
1884 if ( !indices.slice )
1885 rawNode = getChild( rawNode, indices );
1886 else {
1887 indices = indices.slice();
1888 while ( indices.length > 0 && rawNode )
1889 rawNode = getChild( rawNode, indices.shift() );
1890 }
1891
1892 return rawNode ? new CKEDITOR.dom.node( rawNode ) : null;
1893 };
1894 } )(),
1895
1896 /**
1897 * Gets number of element's children.
1898 *
1899 * @returns {Number}
1900 */
1901 getChildCount: function() {
1902 return this.$.childNodes.length;
1903 },
1904
1905 /**
1906 * Disables browser's context menu in this element.
1907 */
1908 disableContextMenu: function() {
1909 this.on( 'contextmenu', function( evt ) {
1910 // Cancel the browser context menu.
1911 if ( !evt.data.getTarget().getAscendant( enablesContextMenu, true ) )
1912 evt.data.preventDefault();
1913 } );
1914
1915 function enablesContextMenu( node ) {
1916 return node.type == CKEDITOR.NODE_ELEMENT && node.hasClass( 'cke_enable_context_menu' );
1917 }
1918 },
1919
1920 /**
1921 * Gets element's direction. Supports both CSS `direction` prop and `dir` attr.
1922 */
1923 getDirection: function( useComputed ) {
1924 if ( useComputed ) {
1925 return this.getComputedStyle( 'direction' ) ||
1926 this.getDirection() ||
1927 this.getParent() && this.getParent().getDirection( 1 ) ||
1928 this.getDocument().$.dir ||
1929 'ltr';
1930 }
1931 else {
1932 return this.getStyle( 'direction' ) || this.getAttribute( 'dir' );
1933 }
1934 },
1935
1936 /**
1937 * Gets, sets and removes custom data to be stored as HTML5 data-* attributes.
1938 *
1939 * element.data( 'extra-info', 'test' ); // Appended the attribute data-extra-info="test" to the element.
1940 * alert( element.data( 'extra-info' ) ); // 'test'
1941 * element.data( 'extra-info', false ); // Remove the data-extra-info attribute from the element.
1942 *
1943 * @param {String} name The name of the attribute, excluding the `data-` part.
1944 * @param {String} [value] The value to set. If set to false, the attribute will be removed.
1945 */
1946 data: function( name, value ) {
1947 name = 'data-' + name;
1948 if ( value === undefined )
1949 return this.getAttribute( name );
1950 else if ( value === false )
1951 this.removeAttribute( name );
1952 else
1953 this.setAttribute( name, value );
1954
1955 return null;
1956 },
1957
1958 /**
1959 * Retrieves an editor instance which is based on this element (if any).
1960 * It basically loops over {@link CKEDITOR#instances} in search for an instance
1961 * that uses the element.
1962 *
1963 * var element = new CKEDITOR.dom.element( 'div' );
1964 * element.appendTo( CKEDITOR.document.getBody() );
1965 * CKEDITOR.replace( element );
1966 * alert( element.getEditor().name ); // 'editor1'
1967 *
1968 * By default this method considers only original DOM elements upon which the editor
1969 * was created. Setting `optimized` parameter to `false` will consider editor editable
1970 * and its children.
1971 *
1972 * @param {Boolean} [optimized=true] If set to `false` it will scan every editor editable.
1973 * @returns {CKEDITOR.editor} An editor instance or null if nothing has been found.
1974 */
1975 getEditor: function( optimized ) {
1976 var instances = CKEDITOR.instances,
1977 name, instance, editable;
1978
1979 optimized = optimized || optimized === undefined;
1980
1981 for ( name in instances ) {
1982 instance = instances[ name ];
1983
1984 if ( instance.element.equals( this ) && instance.elementMode != CKEDITOR.ELEMENT_MODE_APPENDTO )
1985 return instance;
1986
1987 if ( !optimized ) {
1988 editable = instance.editable();
1989
1990 if ( editable && ( editable.equals( this ) || editable.contains( this ) ) ) {
1991 return instance;
1992 }
1993 }
1994 }
1995
1996 return null;
1997 },
1998
1999 /**
2000 * Returns list of elements within this element that match specified `selector`.
2001 *
2002 * **Notes:**
2003 *
2004 * * Not available in IE7.
2005 * * Returned list is not a live collection (like a result of native `querySelectorAll`).
2006 * * Unlike native `querySelectorAll` this method ensures selector contextualization. This is:
2007 *
2008 * HTML: '<body><div><i>foo</i></div></body>'
2009 * Native: div.querySelectorAll( 'body i' ) // -> [ <i>foo</i> ]
2010 * Method: div.find( 'body i' ) // -> []
2011 * div.find( 'i' ) // -> [ <i>foo</i> ]
2012 *
2013 * @since 4.3
2014 * @param {String} selector
2015 * @returns {CKEDITOR.dom.nodeList}
2016 */
2017 find: function( selector ) {
2018 var removeTmpId = createTmpId( this ),
2019 list = new CKEDITOR.dom.nodeList(
2020 this.$.querySelectorAll( getContextualizedSelector( this, selector ) )
2021 );
2022
2023 removeTmpId();
2024
2025 return list;
2026 },
2027
2028 /**
2029 * Returns first element within this element that matches specified `selector`.
2030 *
2031 * **Notes:**
2032 *
2033 * * Not available in IE7.
2034 * * Unlike native `querySelectorAll` this method ensures selector contextualization. This is:
2035 *
2036 * HTML: '<body><div><i>foo</i></div></body>'
2037 * Native: div.querySelector( 'body i' ) // -> <i>foo</i>
2038 * Method: div.findOne( 'body i' ) // -> null
2039 * div.findOne( 'i' ) // -> <i>foo</i>
2040 *
2041 * @since 4.3
2042 * @param {String} selector
2043 * @returns {CKEDITOR.dom.element}
2044 */
2045 findOne: function( selector ) {
2046 var removeTmpId = createTmpId( this ),
2047 found = this.$.querySelector( getContextualizedSelector( this, selector ) );
2048
2049 removeTmpId();
2050
2051 return found ? new CKEDITOR.dom.element( found ) : null;
2052 },
2053
2054 /**
2055 * Traverse the DOM of this element (inclusive), executing a callback for
2056 * each node.
2057 *
2058 * var element = CKEDITOR.dom.element.createFromHtml( '<div><p>foo<b>bar</b>bom</p></div>' );
2059 * element.forEach( function( node ) {
2060 * console.log( node );
2061 * } );
2062 * // Will log:
2063 * // 1. <div> element,
2064 * // 2. <p> element,
2065 * // 3. "foo" text node,
2066 * // 4. <b> element,
2067 * // 5. "bar" text node,
2068 * // 6. "bom" text node.
2069 *
2070 * @since 4.3
2071 * @param {Function} callback Function to be executed on every node.
2072 * If `callback` returns `false` descendants of the node will be ignored.
2073 * @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument.
2074 * @param {Number} [type] If specified `callback` will be executed only on
2075 * nodes of this type.
2076 * @param {Boolean} [skipRoot] Don't execute `callback` on this element.
2077 */
2078 forEach: function( callback, type, skipRoot ) {
2079 if ( !skipRoot && ( !type || this.type == type ) )
2080 var ret = callback( this );
2081
2082 // Do not filter children if callback returned false.
2083 if ( ret === false )
2084 return;
2085
2086 var children = this.getChildren(),
2087 node,
2088 i = 0;
2089
2090 // We do not cache the size, because the live list of nodes may be changed by the callback.
2091 for ( ; i < children.count(); i++ ) {
2092 node = children.getItem( i );
2093 if ( node.type == CKEDITOR.NODE_ELEMENT )
2094 node.forEach( callback, type );
2095 else if ( !type || node.type == type )
2096 callback( node );
2097 }
2098 }
2099 } );
2100
2101 function createTmpId( element ) {
2102 var hadId = true;
2103
2104 if ( !element.$.id ) {
2105 element.$.id = 'cke_tmp_' + CKEDITOR.tools.getNextNumber();
2106 hadId = false;
2107 }
2108
2109 return function() {
2110 if ( !hadId )
2111 element.removeAttribute( 'id' );
2112 };
2113 }
2114
2115 function getContextualizedSelector( element, selector ) {
2116 var id = CKEDITOR.tools.escapeCss( element.$.id );
2117 return '#' + id + ' ' + selector.split( /,\s*/ ).join( ', #' + id + ' ' );
2118 }
2119
2120 var sides = {
2121 width: [ 'border-left-width', 'border-right-width', 'padding-left', 'padding-right' ],
2122 height: [ 'border-top-width', 'border-bottom-width', 'padding-top', 'padding-bottom' ]
2123 };
2124
2125 // Generate list of specific style rules, applicable to margin/padding/border.
2126 function expandedRules( style ) {
2127 var sides = [ 'top', 'left', 'right', 'bottom' ], components;
2128
2129 if ( style == 'border' )
2130 components = [ 'color', 'style', 'width' ];
2131
2132 var styles = [];
2133 for ( var i = 0 ; i < sides.length ; i++ ) {
2134
2135 if ( components ) {
2136 for ( var j = 0 ; j < components.length ; j++ )
2137 styles.push( [ style, sides[ i ], components[ j ] ].join( '-' ) );
2138 } else {
2139 styles.push( [ style, sides[ i ] ].join( '-' ) );
2140 }
2141 }
2142
2143 return styles;
2144 }
2145
2146 function marginAndPaddingSize( type ) {
2147 var adjustment = 0;
2148 for ( var i = 0, len = sides[ type ].length; i < len; i++ )
2149 adjustment += parseFloat( this.getComputedStyle( sides[ type ][ i ] ) || 0, 10 ) || 0;
2150 return adjustment;
2151 }
2152
2153 /**
2154 * Sets the element size considering the box model.
2155 *
2156 * @param {'width'/'height'} type The dimension to set.
2157 * @param {Number} size The length unit in px.
2158 * @param {Boolean} isBorderBox Apply the size based on the border box model.
2159 */
2160 CKEDITOR.dom.element.prototype.setSize = function( type, size, isBorderBox ) {
2161 if ( typeof size == 'number' ) {
2162 if ( isBorderBox && !( CKEDITOR.env.ie && CKEDITOR.env.quirks ) )
2163 size -= marginAndPaddingSize.call( this, type );
2164
2165 this.setStyle( type, size + 'px' );
2166 }
2167 };
2168
2169 /**
2170 * Gets the element size, possibly considering the box model.
2171 *
2172 * @param {'width'/'height'} type The dimension to get.
2173 * @param {Boolean} isBorderBox Get the size based on the border box model.
2174 */
2175 CKEDITOR.dom.element.prototype.getSize = function( type, isBorderBox ) {
2176 var size = Math.max( this.$[ 'offset' + CKEDITOR.tools.capitalize( type ) ], this.$[ 'client' + CKEDITOR.tools.capitalize( type ) ] ) || 0;
2177
2178 if ( isBorderBox )
2179 size -= marginAndPaddingSize.call( this, type );
2180
2181 return size;
2182 };
2183} )();
diff --git a/sources/core/dom/elementpath.js b/sources/core/dom/elementpath.js
new file mode 100644
index 0000000..1a3aed0
--- /dev/null
+++ b/sources/core/dom/elementpath.js
@@ -0,0 +1,251 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8( function() {
9
10 var pathBlockLimitElements = {},
11 pathBlockElements = {},
12 tag;
13
14 // Elements that are considered the "Block limit" in an element path.
15 for ( tag in CKEDITOR.dtd.$blockLimit ) {
16 // Exclude from list roots.
17 if ( !( tag in CKEDITOR.dtd.$list ) )
18 pathBlockLimitElements[ tag ] = 1;
19 }
20
21 // Elements that are considered the "End level Block" in an element path.
22 for ( tag in CKEDITOR.dtd.$block ) {
23 // Exclude block limits, and empty block element, e.g. hr.
24 if ( !( tag in CKEDITOR.dtd.$blockLimit || tag in CKEDITOR.dtd.$empty ) )
25 pathBlockElements[ tag ] = 1;
26 }
27
28 // Check if an element contains any block element.
29 function checkHasBlock( element ) {
30 var childNodes = element.getChildren();
31
32 for ( var i = 0, count = childNodes.count(); i < count; i++ ) {
33 var child = childNodes.getItem( i );
34
35 if ( child.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$block[ child.getName() ] )
36 return true;
37 }
38
39 return false;
40 }
41
42 /**
43 * Retrieve the list of nodes walked from the start node up to the editable element of the editor.
44 *
45 * @class
46 * @constructor Creates an element path class instance.
47 * @param {CKEDITOR.dom.element} startNode From which the path should start.
48 * @param {CKEDITOR.dom.element} root To which element the path should stop, defaults to the `body` element.
49 */
50 CKEDITOR.dom.elementPath = function( startNode, root ) {
51 var block = null,
52 blockLimit = null,
53 elements = [],
54 e = startNode,
55 elementName;
56
57 // Backward compact.
58 root = root || startNode.getDocument().getBody();
59
60 do {
61 if ( e.type == CKEDITOR.NODE_ELEMENT ) {
62 elements.push( e );
63
64 if ( !this.lastElement ) {
65 this.lastElement = e;
66
67 // If an object or non-editable element is fully selected at the end of the element path,
68 // it must not become the block limit.
69 if ( e.is( CKEDITOR.dtd.$object ) || e.getAttribute( 'contenteditable' ) == 'false' )
70 continue;
71 }
72
73 if ( e.equals( root ) )
74 break;
75
76 if ( !blockLimit ) {
77 elementName = e.getName();
78
79 // First editable element becomes a block limit, because it cannot be split.
80 if ( e.getAttribute( 'contenteditable' ) == 'true' )
81 blockLimit = e;
82 // "Else" because element cannot be both - block and block levelimit.
83 else if ( !block && pathBlockElements[ elementName ] )
84 block = e;
85
86 if ( pathBlockLimitElements[ elementName ] ) {
87 // End level DIV is considered as the block, if no block is available. (#525)
88 // But it must NOT be the root element (checked above).
89 if ( !block && elementName == 'div' && !checkHasBlock( e ) )
90 block = e;
91 else
92 blockLimit = e;
93 }
94 }
95 }
96 }
97 while ( ( e = e.getParent() ) );
98
99 // Block limit defaults to root.
100 if ( !blockLimit )
101 blockLimit = root;
102
103 /**
104 * First non-empty block element which:
105 *
106 * * is not a {@link CKEDITOR.dtd#$blockLimit},
107 * * or is a `div` which does not contain block elements and is not a `root`.
108 *
109 * This means a first, splittable block in elements path.
110 *
111 * @readonly
112 * @property {CKEDITOR.dom.element}
113 */
114 this.block = block;
115
116 /**
117 * See the {@link CKEDITOR.dtd#$blockLimit} description.
118 *
119 * @readonly
120 * @property {CKEDITOR.dom.element}
121 */
122 this.blockLimit = blockLimit;
123
124 /**
125 * The root of the elements path - `root` argument passed to class constructor or a `body` element.
126 *
127 * @readonly
128 * @property {CKEDITOR.dom.element}
129 */
130 this.root = root;
131
132 /**
133 * An array of elements (from `startNode` to `root`) in the path.
134 *
135 * @readonly
136 * @property {CKEDITOR.dom.element[]}
137 */
138 this.elements = elements;
139
140 /**
141 * The last element of the elements path - `startNode` or its parent.
142 *
143 * @readonly
144 * @property {CKEDITOR.dom.element} lastElement
145 */
146 };
147
148} )();
149
150CKEDITOR.dom.elementPath.prototype = {
151 /**
152 * Compares this element path with another one.
153 *
154 * @param {CKEDITOR.dom.elementPath} otherPath The elementPath object to be
155 * compared with this one.
156 * @returns {Boolean} `true` if the paths are equal, containing the same
157 * number of elements and the same elements in the same order.
158 */
159 compare: function( otherPath ) {
160 var thisElements = this.elements;
161 var otherElements = otherPath && otherPath.elements;
162
163 if ( !otherElements || thisElements.length != otherElements.length )
164 return false;
165
166 for ( var i = 0; i < thisElements.length; i++ ) {
167 if ( !thisElements[ i ].equals( otherElements[ i ] ) )
168 return false;
169 }
170
171 return true;
172 },
173
174 /**
175 * Search the path elements that meets the specified criteria.
176 *
177 * @param {String/Array/Function/Object/CKEDITOR.dom.element} query The criteria that can be
178 * either a tag name, list (array and object) of tag names, element or an node evaluator function.
179 * @param {Boolean} [excludeRoot] Not taking path root element into consideration.
180 * @param {Boolean} [fromTop] Search start from the topmost element instead of bottom.
181 * @returns {CKEDITOR.dom.element} The first matched dom element or `null`.
182 */
183 contains: function( query, excludeRoot, fromTop ) {
184 var evaluator;
185 if ( typeof query == 'string' )
186 evaluator = function( node ) {
187 return node.getName() == query;
188 };
189 if ( query instanceof CKEDITOR.dom.element )
190 evaluator = function( node ) {
191 return node.equals( query );
192 };
193 else if ( CKEDITOR.tools.isArray( query ) )
194 evaluator = function( node ) {
195 return CKEDITOR.tools.indexOf( query, node.getName() ) > -1;
196 };
197 else if ( typeof query == 'function' )
198 evaluator = query;
199 else if ( typeof query == 'object' )
200 evaluator = function( node ) {
201 return node.getName() in query;
202 };
203
204 var elements = this.elements,
205 length = elements.length;
206 excludeRoot && length--;
207
208 if ( fromTop ) {
209 elements = Array.prototype.slice.call( elements, 0 );
210 elements.reverse();
211 }
212
213 for ( var i = 0; i < length; i++ ) {
214 if ( evaluator( elements[ i ] ) )
215 return elements[ i ];
216 }
217
218 return null;
219 },
220
221 /**
222 * Check whether the elements path is the proper context for the specified
223 * tag name in the DTD.
224 *
225 * @param {String} tag The tag name.
226 * @returns {Boolean}
227 */
228 isContextFor: function( tag ) {
229 var holder;
230
231 // Check for block context.
232 if ( tag in CKEDITOR.dtd.$block ) {
233 // Indeterminate elements which are not subjected to be splitted or surrounded must be checked first.
234 var inter = this.contains( CKEDITOR.dtd.$intermediate );
235 holder = inter || ( this.root.equals( this.block ) && this.block ) || this.blockLimit;
236 return !!holder.getDtd()[ tag ];
237 }
238
239 return true;
240 },
241
242 /**
243 * Retrieve the text direction for this elements path.
244 *
245 * @returns {'ltr'/'rtl'}
246 */
247 direction: function() {
248 var directionNode = this.block || this.blockLimit || this.root;
249 return directionNode.getDirection( 1 );
250 }
251};
diff --git a/sources/core/dom/event.js b/sources/core/dom/event.js
new file mode 100644
index 0000000..8b1193a
--- /dev/null
+++ b/sources/core/dom/event.js
@@ -0,0 +1,208 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.event} class, which
8 * represents the a native DOM event object.
9 */
10
11/**
12 * Represents a native DOM event object.
13 *
14 * @class
15 * @constructor Creates an event class instance.
16 * @param {Object} domEvent A native DOM event object.
17 */
18CKEDITOR.dom.event = function( domEvent ) {
19 /**
20 * The native DOM event object represented by this class instance.
21 *
22 * @readonly
23 */
24 this.$ = domEvent;
25};
26
27CKEDITOR.dom.event.prototype = {
28 /**
29 * Gets the key code associated to the event.
30 *
31 * alert( event.getKey() ); // '65' is 'a' has been pressed
32 *
33 * @returns {Number} The key code.
34 */
35 getKey: function() {
36 return this.$.keyCode || this.$.which;
37 },
38
39 /**
40 * Gets a number represeting the combination of the keys pressed during the
41 * event. It is the sum with the current key code and the {@link CKEDITOR#CTRL},
42 * {@link CKEDITOR#SHIFT} and {@link CKEDITOR#ALT} constants.
43 *
44 * alert( event.getKeystroke() == 65 ); // 'a' key
45 * alert( event.getKeystroke() == CKEDITOR.CTRL + 65 ); // CTRL + 'a' key
46 * alert( event.getKeystroke() == CKEDITOR.CTRL + CKEDITOR.SHIFT + 65 ); // CTRL + SHIFT + 'a' key
47 *
48 * @returns {Number} The number representing the keys combination.
49 */
50 getKeystroke: function() {
51 var keystroke = this.getKey();
52
53 if ( this.$.ctrlKey || this.$.metaKey )
54 keystroke += CKEDITOR.CTRL;
55
56 if ( this.$.shiftKey )
57 keystroke += CKEDITOR.SHIFT;
58
59 if ( this.$.altKey )
60 keystroke += CKEDITOR.ALT;
61
62 return keystroke;
63 },
64
65 /**
66 * Prevents the original behavior of the event to happen. It can optionally
67 * stop propagating the event in the event chain.
68 *
69 * var element = CKEDITOR.document.getById( 'myElement' );
70 * element.on( 'click', function( ev ) {
71 * // The DOM event object is passed by the 'data' property.
72 * var domEvent = ev.data;
73 * // Prevent the click to chave any effect in the element.
74 * domEvent.preventDefault();
75 * } );
76 *
77 * @param {Boolean} [stopPropagation=false] Stop propagating this event in the
78 * event chain.
79 */
80 preventDefault: function( stopPropagation ) {
81 var $ = this.$;
82 if ( $.preventDefault )
83 $.preventDefault();
84 else
85 $.returnValue = false;
86
87 if ( stopPropagation )
88 this.stopPropagation();
89 },
90
91 /**
92 * Stops this event propagation in the event chain.
93 */
94 stopPropagation: function() {
95 var $ = this.$;
96 if ( $.stopPropagation )
97 $.stopPropagation();
98 else
99 $.cancelBubble = true;
100 },
101
102 /**
103 * Returns the DOM node where the event was targeted to.
104 *
105 * var element = CKEDITOR.document.getById( 'myElement' );
106 * element.on( 'click', function( ev ) {
107 * // The DOM event object is passed by the 'data' property.
108 * var domEvent = ev.data;
109 * // Add a CSS class to the event target.
110 * domEvent.getTarget().addClass( 'clicked' );
111 * } );
112 *
113 * @returns {CKEDITOR.dom.node} The target DOM node.
114 */
115 getTarget: function() {
116 var rawNode = this.$.target || this.$.srcElement;
117 return rawNode ? new CKEDITOR.dom.node( rawNode ) : null;
118 },
119
120 /**
121 * Returns an integer value that indicates the current processing phase of an event.
122 * For browsers that doesn't support event phase, {@link CKEDITOR#EVENT_PHASE_AT_TARGET} is always returned.
123 *
124 * @returns {Number} One of {@link CKEDITOR#EVENT_PHASE_CAPTURING},
125 * {@link CKEDITOR#EVENT_PHASE_AT_TARGET}, or {@link CKEDITOR#EVENT_PHASE_BUBBLING}.
126 */
127 getPhase: function() {
128 return this.$.eventPhase || 2;
129 },
130
131 /**
132 * Retrieves the coordinates of the mouse pointer relative to the top-left
133 * corner of the document, in mouse related event.
134 *
135 * element.on( 'mousemouse', function( ev ) {
136 * var pageOffset = ev.data.getPageOffset();
137 * alert( pageOffset.x ); // page offset X
138 * alert( pageOffset.y ); // page offset Y
139 * } );
140 *
141 * @returns {Object} The object contains the position.
142 * @returns {Number} return.x
143 * @returns {Number} return.y
144 */
145 getPageOffset: function() {
146 var doc = this.getTarget().getDocument().$;
147 var pageX = this.$.pageX || this.$.clientX + ( doc.documentElement.scrollLeft || doc.body.scrollLeft );
148 var pageY = this.$.pageY || this.$.clientY + ( doc.documentElement.scrollTop || doc.body.scrollTop );
149 return { x: pageX, y: pageY };
150 }
151};
152
153// For the followind constants, we need to go over the Unicode boundaries
154// (0x10FFFF) to avoid collision.
155
156/**
157 * CTRL key (0x110000).
158 *
159 * @readonly
160 * @property {Number} [=0x110000]
161 * @member CKEDITOR
162 */
163CKEDITOR.CTRL = 0x110000;
164
165/**
166 * SHIFT key (0x220000).
167 *
168 * @readonly
169 * @property {Number} [=0x220000]
170 * @member CKEDITOR
171 */
172CKEDITOR.SHIFT = 0x220000;
173
174/**
175 * ALT key (0x440000).
176 *
177 * @readonly
178 * @property {Number} [=0x440000]
179 * @member CKEDITOR
180 */
181CKEDITOR.ALT = 0x440000;
182
183/**
184 * Capturing phase.
185 *
186 * @readonly
187 * @property {Number} [=1]
188 * @member CKEDITOR
189 */
190CKEDITOR.EVENT_PHASE_CAPTURING = 1;
191
192/**
193 * Event at target.
194 *
195 * @readonly
196 * @property {Number} [=2]
197 * @member CKEDITOR
198 */
199CKEDITOR.EVENT_PHASE_AT_TARGET = 2;
200
201/**
202 * Bubbling phase.
203 *
204 * @readonly
205 * @property {Number} [=3]
206 * @member CKEDITOR
207 */
208CKEDITOR.EVENT_PHASE_BUBBLING = 3;
diff --git a/sources/core/dom/iterator.js b/sources/core/dom/iterator.js
new file mode 100644
index 0000000..50056ec
--- /dev/null
+++ b/sources/core/dom/iterator.js
@@ -0,0 +1,565 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @ignore
8 * File overview: DOM iterator which iterates over list items, lines and paragraphs.
9 */
10
11'use strict';
12
13( function() {
14 /**
15 * Represents the iterator class. It can be used to iterate
16 * over all elements (or even text nodes in case of {@link #enlargeBr} set to `false`)
17 * which establish "paragraph-like" spaces within the passed range.
18 *
19 * // <h1>[foo</h1><p>bar]</p>
20 * var iterator = range.createIterator();
21 * iterator.getNextParagraph(); // h1 element
22 * iterator.getNextParagraph(); // p element
23 *
24 * // <ul><li>[foo</li><li>bar]</li>
25 * // With enforceRealBlocks set to false the iterator will return two list item elements.
26 * // With enforceRealBlocks set to true the iterator will return two paragraphs and the DOM will be changed to:
27 * // <ul><li><p>foo</p></li><li><p>bar</p></li>
28 *
29 * @class CKEDITOR.dom.iterator
30 * @constructor Creates an iterator class instance.
31 * @param {CKEDITOR.dom.range} range
32 */
33 function iterator( range ) {
34 if ( arguments.length < 1 )
35 return;
36
37 /**
38 * @readonly
39 * @property {CKEDITOR.dom.range}
40 */
41 this.range = range;
42
43 /**
44 * @property {Boolean} [forceBrBreak=false]
45 */
46 this.forceBrBreak = 0;
47
48 // (#3730).
49 /**
50 * Whether to include `<br>` elements in the enlarged range. Should be
51 * set to `false` when using the iterator in the {@link CKEDITOR#ENTER_BR} mode.
52 *
53 * @property {Boolean} [enlargeBr=true]
54 */
55 this.enlargeBr = 1;
56
57 /**
58 * Whether the iterator should create a transformable block
59 * if the current one contains text and cannot be transformed.
60 * For example new blocks will be established in elements like
61 * `<li>` or `<td>`.
62 *
63 * @property {Boolean} [enforceRealBlocks=false]
64 */
65 this.enforceRealBlocks = 0;
66
67 this._ || ( this._ = {} );
68 }
69
70 /**
71 * Default iterator's filter. It is set only for nested iterators.
72 *
73 * @since 4.3
74 * @readonly
75 * @property {CKEDITOR.filter} filter
76 */
77
78 /**
79 * Iterator's active filter. It is set by the {@link #getNextParagraph} method
80 * when it enters a nested editable.
81 *
82 * @since 4.3
83 * @readonly
84 * @property {CKEDITOR.filter} activeFilter
85 */
86
87 var beginWhitespaceRegex = /^[\r\n\t ]+$/,
88 // Ignore bookmark nodes.(#3783)
89 bookmarkGuard = CKEDITOR.dom.walker.bookmark( false, true ),
90 whitespacesGuard = CKEDITOR.dom.walker.whitespaces( true ),
91 skipGuard = function( node ) {
92 return bookmarkGuard( node ) && whitespacesGuard( node );
93 },
94 listItemNames = { dd: 1, dt: 1, li: 1 };
95
96 iterator.prototype = {
97 /**
98 * Returns the next paragraph-like element or `null` if the end of a range is reached.
99 *
100 * @param {String} [blockTag='p'] Name of a block element which will be established by
101 * the iterator in block-less elements (see {@link #enforceRealBlocks}).
102 */
103 getNextParagraph: function( blockTag ) {
104 // The block element to be returned.
105 var block;
106
107 // The range object used to identify the paragraph contents.
108 var range;
109
110 // Indicats that the current element in the loop is the last one.
111 var isLast;
112
113 // Instructs to cleanup remaining BRs.
114 var removePreviousBr, removeLastBr;
115
116 blockTag = blockTag || 'p';
117
118 // We're iterating over nested editable.
119 if ( this._.nestedEditable ) {
120 // Get next block from nested iterator and returns it if was found.
121 block = this._.nestedEditable.iterator.getNextParagraph( blockTag );
122 if ( block ) {
123 // Inherit activeFilter from the nested iterator.
124 this.activeFilter = this._.nestedEditable.iterator.activeFilter;
125 return block;
126 }
127
128 // No block in nested iterator means that we reached the end of the nested editable.
129 // Reset the active filter to the default filter (or undefined if this iterator didn't have it).
130 this.activeFilter = this.filter;
131
132 // Try to find next nested editable or get back to parent (this) iterator.
133 if ( startNestedEditableIterator( this, blockTag, this._.nestedEditable.container, this._.nestedEditable.remaining ) ) {
134 // Inherit activeFilter from the nested iterator.
135 this.activeFilter = this._.nestedEditable.iterator.activeFilter;
136 return this._.nestedEditable.iterator.getNextParagraph( blockTag );
137 } else {
138 this._.nestedEditable = null;
139 }
140 }
141
142 // Block-less range should be checked first.
143 if ( !this.range.root.getDtd()[ blockTag ] )
144 return null;
145
146 // This is the first iteration. Let's initialize it.
147 if ( !this._.started )
148 range = startIterator.call( this );
149
150 var currentNode = this._.nextNode,
151 lastNode = this._.lastNode;
152
153 this._.nextNode = null;
154 while ( currentNode ) {
155 // closeRange indicates that a paragraph boundary has been found,
156 // so the range can be closed.
157 var closeRange = 0,
158 parentPre = currentNode.hasAscendant( 'pre' );
159
160 // includeNode indicates that the current node is good to be part
161 // of the range. By default, any non-element node is ok for it.
162 var includeNode = ( currentNode.type != CKEDITOR.NODE_ELEMENT ),
163 continueFromSibling = 0;
164
165 // If it is an element node, let's check if it can be part of the range.
166 if ( !includeNode ) {
167 var nodeName = currentNode.getName();
168
169 // Non-editable block was found - return it and move to processing
170 // its nested editables if they exist.
171 if ( CKEDITOR.dtd.$block[ nodeName ] && currentNode.getAttribute( 'contenteditable' ) == 'false' ) {
172 block = currentNode;
173
174 // Setup iterator for first of nested editables.
175 // If there's no editable, then algorithm will move to next element after current block.
176 startNestedEditableIterator( this, blockTag, block );
177
178 // Gets us straight to the end of getParagraph() because block variable is set.
179 break;
180 } else if ( currentNode.isBlockBoundary( this.forceBrBreak && !parentPre && { br: 1 } ) ) {
181 // <br> boundaries must be part of the range. It will
182 // happen only if ForceBrBreak.
183 if ( nodeName == 'br' )
184 includeNode = 1;
185 else if ( !range && !currentNode.getChildCount() && nodeName != 'hr' ) {
186 // If we have found an empty block, and haven't started
187 // the range yet, it means we must return this block.
188 block = currentNode;
189 isLast = currentNode.equals( lastNode );
190 break;
191 }
192
193 // The range must finish right before the boundary,
194 // including possibly skipped empty spaces. (#1603)
195 if ( range ) {
196 range.setEndAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
197
198 // The found boundary must be set as the next one at this
199 // point. (#1717)
200 if ( nodeName != 'br' ) {
201 this._.nextNode = currentNode;
202 }
203 }
204
205 closeRange = 1;
206 } else {
207 // If we have child nodes, let's check them.
208 if ( currentNode.getFirst() ) {
209 // If we don't have a range yet, let's start it.
210 if ( !range ) {
211 range = this.range.clone();
212 range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
213 }
214
215 currentNode = currentNode.getFirst();
216 continue;
217 }
218 includeNode = 1;
219 }
220 } else if ( currentNode.type == CKEDITOR.NODE_TEXT ) {
221 // Ignore normal whitespaces (i.e. not including &nbsp; or
222 // other unicode whitespaces) before/after a block node.
223 if ( beginWhitespaceRegex.test( currentNode.getText() ) )
224 includeNode = 0;
225 }
226
227 // The current node is good to be part of the range and we are
228 // starting a new range, initialize it first.
229 if ( includeNode && !range ) {
230 range = this.range.clone();
231 range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
232 }
233
234 // The last node has been found.
235 isLast = ( ( !closeRange || includeNode ) && currentNode.equals( lastNode ) );
236
237 // If we are in an element boundary, let's check if it is time
238 // to close the range, otherwise we include the parent within it.
239 if ( range && !closeRange ) {
240 while ( !currentNode.getNext( skipGuard ) && !isLast ) {
241 var parentNode = currentNode.getParent();
242
243 if ( parentNode.isBlockBoundary( this.forceBrBreak && !parentPre && { br: 1 } ) ) {
244 closeRange = 1;
245 includeNode = 0;
246 isLast = isLast || ( parentNode.equals( lastNode ) );
247 // Make sure range includes bookmarks at the end of the block. (#7359)
248 range.setEndAt( parentNode, CKEDITOR.POSITION_BEFORE_END );
249 break;
250 }
251
252 currentNode = parentNode;
253 includeNode = 1;
254 isLast = ( currentNode.equals( lastNode ) );
255 continueFromSibling = 1;
256 }
257 }
258
259 // Now finally include the node.
260 if ( includeNode )
261 range.setEndAt( currentNode, CKEDITOR.POSITION_AFTER_END );
262
263 currentNode = this._getNextSourceNode( currentNode, continueFromSibling, lastNode );
264 isLast = !currentNode;
265
266 // We have found a block boundary. Let's close the range and move out of the
267 // loop.
268 if ( isLast || ( closeRange && range ) )
269 break;
270 }
271
272 // Now, based on the processed range, look for (or create) the block to be returned.
273 if ( !block ) {
274 // If no range has been found, this is the end.
275 if ( !range ) {
276 this._.docEndMarker && this._.docEndMarker.remove();
277 this._.nextNode = null;
278 return null;
279 }
280
281 var startPath = new CKEDITOR.dom.elementPath( range.startContainer, range.root );
282 var startBlockLimit = startPath.blockLimit,
283 checkLimits = { div: 1, th: 1, td: 1 };
284 block = startPath.block;
285
286 if ( !block && startBlockLimit && !this.enforceRealBlocks && checkLimits[ startBlockLimit.getName() ] &&
287 range.checkStartOfBlock() && range.checkEndOfBlock() && !startBlockLimit.equals( range.root ) ) {
288 block = startBlockLimit;
289 } else if ( !block || ( this.enforceRealBlocks && block.is( listItemNames ) ) ) {
290 // Create the fixed block.
291 block = this.range.document.createElement( blockTag );
292
293 // Move the contents of the temporary range to the fixed block.
294 range.extractContents().appendTo( block );
295 block.trim();
296
297 // Insert the fixed block into the DOM.
298 range.insertNode( block );
299
300 removePreviousBr = removeLastBr = true;
301 } else if ( block.getName() != 'li' ) {
302 // If the range doesn't includes the entire contents of the
303 // block, we must split it, isolating the range in a dedicated
304 // block.
305 if ( !range.checkStartOfBlock() || !range.checkEndOfBlock() ) {
306 // The resulting block will be a clone of the current one.
307 block = block.clone( false );
308
309 // Extract the range contents, moving it to the new block.
310 range.extractContents().appendTo( block );
311 block.trim();
312
313 // Split the block. At this point, the range will be in the
314 // right position for our intents.
315 var splitInfo = range.splitBlock();
316
317 removePreviousBr = !splitInfo.wasStartOfBlock;
318 removeLastBr = !splitInfo.wasEndOfBlock;
319
320 // Insert the new block into the DOM.
321 range.insertNode( block );
322 }
323 } else if ( !isLast ) {
324 // LIs are returned as is, with all their children (due to the
325 // nested lists). But, the next node is the node right after
326 // the current range, which could be an <li> child (nested
327 // lists) or the next sibling <li>.
328
329 this._.nextNode = ( block.equals( lastNode ) ? null : this._getNextSourceNode( range.getBoundaryNodes().endNode, 1, lastNode ) );
330 }
331 }
332
333 if ( removePreviousBr ) {
334 var previousSibling = block.getPrevious();
335 if ( previousSibling && previousSibling.type == CKEDITOR.NODE_ELEMENT ) {
336 if ( previousSibling.getName() == 'br' )
337 previousSibling.remove();
338 else if ( previousSibling.getLast() && previousSibling.getLast().$.nodeName.toLowerCase() == 'br' )
339 previousSibling.getLast().remove();
340 }
341 }
342
343 if ( removeLastBr ) {
344 var lastChild = block.getLast();
345 if ( lastChild && lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.getName() == 'br' ) {
346 // Remove br filler on browser which do not need it.
347 if ( !CKEDITOR.env.needsBrFiller || lastChild.getPrevious( bookmarkGuard ) || lastChild.getNext( bookmarkGuard ) )
348 lastChild.remove();
349 }
350 }
351
352 // Get a reference for the next element. This is important because the
353 // above block can be removed or changed, so we can rely on it for the
354 // next interation.
355 if ( !this._.nextNode ) {
356 this._.nextNode = ( isLast || block.equals( lastNode ) || !lastNode ) ? null : this._getNextSourceNode( block, 1, lastNode );
357 }
358
359 return block;
360 },
361
362 /**
363 * Gets the next element to check or `null` when the `lastNode` or the
364 * {@link #range}'s {@link CKEDITOR.dom.range#root root} is reached. Bookmarks are skipped.
365 *
366 * @since 4.4.6
367 * @private
368 * @param {CKEDITOR.dom.node} node
369 * @param {Boolean} startFromSibling
370 * @param {CKEDITOR.dom.node} lastNode
371 * @returns {CKEDITOR.dom.node}
372 */
373 _getNextSourceNode: function( node, startFromSibling, lastNode ) {
374 var rootNode = this.range.root,
375 next;
376
377 // Here we are checking in guard function whether current element
378 // reach lastNode(default behaviour) and root node to prevent against
379 // getting out of editor instance root DOM object.
380 // #12484
381 function guardFunction( node ) {
382 return !( node.equals( lastNode ) || node.equals( rootNode ) );
383 }
384
385 next = node.getNextSourceNode( startFromSibling, null, guardFunction );
386 while ( !bookmarkGuard( next ) ) {
387 next = next.getNextSourceNode( startFromSibling, null, guardFunction );
388 }
389 return next;
390 }
391 };
392
393 // @context CKEDITOR.dom.iterator
394 // @returns Collapsed range which will be reused when during furter processing.
395 function startIterator() {
396 var range = this.range.clone(),
397 // Indicate at least one of the range boundaries is inside a preformat block.
398 touchPre,
399
400 // (#12178)
401 // Remember if following situation takes place:
402 // * startAtInnerBoundary: <p>foo[</p>...
403 // * endAtInnerBoundary: ...<p>]bar</p>
404 // Because information about line break will be lost when shrinking range.
405 // Note that we test only if path block exist, because we must properly shrink
406 // range containing table and/or table cells.
407 // Note: When range is collapsed there's no way it can be shrinked.
408 // By checking if range is collapsed we also prevent #12308.
409 startPath = range.startPath(),
410 endPath = range.endPath(),
411 startAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, startPath.block ),
412 endAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, endPath.block, 1 );
413
414 // Shrink the range to exclude harmful "noises" (#4087, #4450, #5435).
415 range.shrink( CKEDITOR.SHRINK_ELEMENT, true );
416
417 if ( startAtInnerBoundary )
418 range.setStartAt( startPath.block, CKEDITOR.POSITION_BEFORE_END );
419 if ( endAtInnerBoundary )
420 range.setEndAt( endPath.block, CKEDITOR.POSITION_AFTER_START );
421
422 touchPre = range.endContainer.hasAscendant( 'pre', true ) || range.startContainer.hasAscendant( 'pre', true );
423
424 range.enlarge( this.forceBrBreak && !touchPre || !this.enlargeBr ? CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR.ENLARGE_BLOCK_CONTENTS );
425
426 if ( !range.collapsed ) {
427 var walker = new CKEDITOR.dom.walker( range.clone() ),
428 ignoreBookmarkTextEvaluator = CKEDITOR.dom.walker.bookmark( true, true );
429 // Avoid anchor inside bookmark inner text.
430 walker.evaluator = ignoreBookmarkTextEvaluator;
431 this._.nextNode = walker.next();
432 // TODO: It's better to have walker.reset() used here.
433 walker = new CKEDITOR.dom.walker( range.clone() );
434 walker.evaluator = ignoreBookmarkTextEvaluator;
435 var lastNode = walker.previous();
436 this._.lastNode = lastNode.getNextSourceNode( true, null, range.root );
437
438 // We may have an empty text node at the end of block due to [3770].
439 // If that node is the lastNode, it would cause our logic to leak to the
440 // next block.(#3887)
441 if ( this._.lastNode && this._.lastNode.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.trim( this._.lastNode.getText() ) && this._.lastNode.getParent().isBlockBoundary() ) {
442 var testRange = this.range.clone();
443 testRange.moveToPosition( this._.lastNode, CKEDITOR.POSITION_AFTER_END );
444 if ( testRange.checkEndOfBlock() ) {
445 var path = new CKEDITOR.dom.elementPath( testRange.endContainer, testRange.root ),
446 lastBlock = path.block || path.blockLimit;
447 this._.lastNode = lastBlock.getNextSourceNode( true );
448 }
449 }
450
451 // The end of document or range.root was reached, so we need a marker node inside.
452 if ( !this._.lastNode || !range.root.contains( this._.lastNode ) ) {
453 this._.lastNode = this._.docEndMarker = range.document.createText( '' );
454 this._.lastNode.insertAfter( lastNode );
455 }
456
457 // Let's reuse this variable.
458 range = null;
459 }
460
461 this._.started = 1;
462
463 return range;
464 }
465
466 // Does a nested editables lookup inside editablesContainer.
467 // If remainingEditables is set will lookup inside this array.
468 // @param {CKEDITOR.dom.element} editablesContainer
469 // @param {CKEDITOR.dom.element[]} [remainingEditables]
470 function getNestedEditableIn( editablesContainer, remainingEditables ) {
471 if ( remainingEditables == null )
472 remainingEditables = findNestedEditables( editablesContainer );
473
474 var editable;
475
476 while ( ( editable = remainingEditables.shift() ) ) {
477 if ( isIterableEditable( editable ) )
478 return { element: editable, remaining: remainingEditables };
479 }
480
481 return null;
482 }
483
484 // Checkes whether we can iterate over this editable.
485 function isIterableEditable( editable ) {
486 // Reject blockless editables.
487 return editable.getDtd().p;
488 }
489
490 // Finds nested editables within container. Does not return
491 // editables nested in another editable (twice).
492 function findNestedEditables( container ) {
493 var editables = [];
494
495 container.forEach( function( element ) {
496 if ( element.getAttribute( 'contenteditable' ) == 'true' ) {
497 editables.push( element );
498 return false; // Skip children.
499 }
500 }, CKEDITOR.NODE_ELEMENT, true );
501
502 return editables;
503 }
504
505 // Looks for a first nested editable after previousEditable (if passed) and creates
506 // nested iterator for it.
507 function startNestedEditableIterator( parentIterator, blockTag, editablesContainer, remainingEditables ) {
508 var editable = getNestedEditableIn( editablesContainer, remainingEditables );
509
510 if ( !editable )
511 return 0;
512
513 var filter = CKEDITOR.filter.instances[ editable.element.data( 'cke-filter' ) ];
514
515 // If current editable has a filter and this filter does not allow for block tag,
516 // search for next nested editable in remaining ones.
517 if ( filter && !filter.check( blockTag ) )
518 return startNestedEditableIterator( parentIterator, blockTag, editablesContainer, editable.remaining );
519
520 var range = new CKEDITOR.dom.range( editable.element );
521 range.selectNodeContents( editable.element );
522
523 var iterator = range.createIterator();
524 // This setting actually does not change anything in this case,
525 // because entire range contents is selected, so there're no <br>s to be included.
526 // But it seems right to copy it too.
527 iterator.enlargeBr = parentIterator.enlargeBr;
528 // Inherit configuration from parent iterator.
529 iterator.enforceRealBlocks = parentIterator.enforceRealBlocks;
530 // Set the activeFilter (which can be overriden when this iteator will start nested iterator)
531 // and the default filter, which will make it possible to reset to
532 // current iterator's activeFilter after leaving nested editable.
533 iterator.activeFilter = iterator.filter = filter;
534
535 parentIterator._.nestedEditable = {
536 element: editable.element,
537 container: editablesContainer,
538 remaining: editable.remaining,
539 iterator: iterator
540 };
541
542 return 1;
543 }
544
545 // Checks whether range starts or ends at inner block boundary.
546 // See usage comments to learn more.
547 function rangeAtInnerBlockBoundary( range, block, checkEnd ) {
548 if ( !block )
549 return false;
550
551 var testRange = range.clone();
552 testRange.collapse( !checkEnd );
553 return testRange.checkBoundaryOfElement( block, checkEnd ? CKEDITOR.START : CKEDITOR.END );
554 }
555
556 /**
557 * Creates a {@link CKEDITOR.dom.iterator} instance for this range.
558 *
559 * @member CKEDITOR.dom.range
560 * @returns {CKEDITOR.dom.iterator}
561 */
562 CKEDITOR.dom.range.prototype.createIterator = function() {
563 return new iterator( this );
564 };
565} )();
diff --git a/sources/core/dom/node.js b/sources/core/dom/node.js
new file mode 100644
index 0000000..51bba18
--- /dev/null
+++ b/sources/core/dom/node.js
@@ -0,0 +1,902 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.node} class which is the base
8 * class for classes that represent DOM nodes.
9 */
10
11/**
12 * Base class for classes representing DOM nodes. This constructor may return
13 * an instance of a class that inherits from this class, like
14 * {@link CKEDITOR.dom.element} or {@link CKEDITOR.dom.text}.
15 *
16 * @class
17 * @extends CKEDITOR.dom.domObject
18 * @constructor Creates a node class instance.
19 * @param {Object} domNode A native DOM node.
20 * @see CKEDITOR.dom.element
21 * @see CKEDITOR.dom.text
22 */
23CKEDITOR.dom.node = function( domNode ) {
24 if ( domNode ) {
25 var type =
26 domNode.nodeType == CKEDITOR.NODE_DOCUMENT ? 'document' :
27 domNode.nodeType == CKEDITOR.NODE_ELEMENT ? 'element' :
28 domNode.nodeType == CKEDITOR.NODE_TEXT ? 'text' :
29 domNode.nodeType == CKEDITOR.NODE_COMMENT ? 'comment' :
30 domNode.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ? 'documentFragment' :
31 'domObject'; // Call the base constructor otherwise.
32
33 return new CKEDITOR.dom[ type ]( domNode );
34 }
35
36 return this;
37};
38
39CKEDITOR.dom.node.prototype = new CKEDITOR.dom.domObject();
40
41/**
42 * Element node type.
43 *
44 * @readonly
45 * @property {Number} [=1]
46 * @member CKEDITOR
47 */
48CKEDITOR.NODE_ELEMENT = 1;
49
50/**
51 * Document node type.
52 *
53 * @readonly
54 * @property {Number} [=9]
55 * @member CKEDITOR
56 */
57CKEDITOR.NODE_DOCUMENT = 9;
58
59/**
60 * Text node type.
61 *
62 * @readonly
63 * @property {Number} [=3]
64 * @member CKEDITOR
65 */
66CKEDITOR.NODE_TEXT = 3;
67
68/**
69 * Comment node type.
70 *
71 * @readonly
72 * @property {Number} [=8]
73 * @member CKEDITOR
74 */
75CKEDITOR.NODE_COMMENT = 8;
76
77/**
78 * Document fragment node type.
79 *
80 * @readonly
81 * @property {Number} [=11]
82 * @member CKEDITOR
83 */
84CKEDITOR.NODE_DOCUMENT_FRAGMENT = 11;
85
86/**
87 * Indicates that positions of both nodes are identical (this is the same node). See {@link CKEDITOR.dom.node#getPosition}.
88 *
89 * @readonly
90 * @property {Number} [=0]
91 * @member CKEDITOR
92 */
93CKEDITOR.POSITION_IDENTICAL = 0;
94
95/**
96 * Indicates that nodes are in different (detached) trees. See {@link CKEDITOR.dom.node#getPosition}.
97 *
98 * @readonly
99 * @property {Number} [=1]
100 * @member CKEDITOR
101 */
102CKEDITOR.POSITION_DISCONNECTED = 1;
103
104/**
105 * Indicates that the context node follows the other node. See {@link CKEDITOR.dom.node#getPosition}.
106 *
107 * @readonly
108 * @property {Number} [=2]
109 * @member CKEDITOR
110 */
111CKEDITOR.POSITION_FOLLOWING = 2;
112
113/**
114 * Indicates that the context node precedes the other node. See {@link CKEDITOR.dom.node#getPosition}.
115 *
116 * @readonly
117 * @property {Number} [=4]
118 * @member CKEDITOR
119 */
120CKEDITOR.POSITION_PRECEDING = 4;
121
122/**
123 * Indicates that the context node is a descendant of the other node. See {@link CKEDITOR.dom.node#getPosition}.
124 *
125 * @readonly
126 * @property {Number} [=8]
127 * @member CKEDITOR
128 */
129CKEDITOR.POSITION_IS_CONTAINED = 8;
130
131/**
132 * Indicates that the context node contains the other node. See {@link CKEDITOR.dom.node#getPosition}.
133 *
134 * @readonly
135 * @property {Number} [=16]
136 * @member CKEDITOR
137 */
138CKEDITOR.POSITION_CONTAINS = 16;
139
140CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, {
141 /**
142 * Makes this node a child of another element.
143 *
144 * var p = new CKEDITOR.dom.element( 'p' );
145 * var strong = new CKEDITOR.dom.element( 'strong' );
146 * strong.appendTo( p );
147 *
148 * // Result: '<p><strong></strong></p>'.
149 *
150 * @param {CKEDITOR.dom.element} element The target element to which this node will be appended.
151 * @returns {CKEDITOR.dom.element} The target element.
152 */
153 appendTo: function( element, toStart ) {
154 element.append( this, toStart );
155 return element;
156 },
157
158 /**
159 * Clones this node.
160 *
161 * **Note**: Values set by {#setCustomData} will not be available in the clone.
162 *
163 * @param {Boolean} [includeChildren=false] If `true` then all node's
164 * children will be cloned recursively.
165 * @param {Boolean} [cloneId=false] Whether ID attributes should be cloned, too.
166 * @returns {CKEDITOR.dom.node} Clone of this node.
167 */
168 clone: function( includeChildren, cloneId ) {
169 var $clone = this.$.cloneNode( includeChildren );
170
171 // The "id" attribute should never be cloned to avoid duplication.
172 removeIds( $clone );
173
174 var node = new CKEDITOR.dom.node( $clone );
175
176 // On IE8 we need to fixed HTML5 node name, see details below.
177 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 &&
178 ( this.type == CKEDITOR.NODE_ELEMENT || this.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) {
179 renameNodes( node );
180 }
181
182 return node;
183
184 function removeIds( node ) {
185 // Reset data-cke-expando only when has been cloned (IE and only for some types of objects).
186 if ( node[ 'data-cke-expando' ] )
187 node[ 'data-cke-expando' ] = false;
188
189 if ( node.nodeType != CKEDITOR.NODE_ELEMENT && node.nodeType != CKEDITOR.NODE_DOCUMENT_FRAGMENT )
190 return;
191
192 if ( !cloneId && node.nodeType == CKEDITOR.NODE_ELEMENT )
193 node.removeAttribute( 'id', false );
194
195 if ( includeChildren ) {
196 var childs = node.childNodes;
197 for ( var i = 0; i < childs.length; i++ )
198 removeIds( childs[ i ] );
199 }
200 }
201
202 // IE8 rename HTML5 nodes by adding `:` at the begging of the tag name when the node is cloned,
203 // so `<figure>` will be `<:figure>` after 'cloneNode'. We need to fix it (#13101).
204 function renameNodes( node ) {
205 if ( node.type != CKEDITOR.NODE_ELEMENT && node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT )
206 return;
207
208 if ( node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) {
209 var name = node.getName();
210 if ( name[ 0 ] == ':' ) {
211 node.renameNode( name.substring( 1 ) );
212 }
213 }
214
215 if ( includeChildren ) {
216 for ( var i = 0; i < node.getChildCount(); i++ )
217 renameNodes( node.getChild( i ) );
218 }
219 }
220 },
221
222 /**
223 * Checks if the node is preceded by any sibling.
224 *
225 * @returns {Boolean}
226 */
227 hasPrevious: function() {
228 return !!this.$.previousSibling;
229 },
230
231 /**
232 * Checks if the node is succeeded by any sibling.
233 *
234 * @returns {Boolean}
235 */
236 hasNext: function() {
237 return !!this.$.nextSibling;
238 },
239
240 /**
241 * Inserts this element after a node.
242 *
243 * var em = new CKEDITOR.dom.element( 'em' );
244 * var strong = new CKEDITOR.dom.element( 'strong' );
245 * strong.insertAfter( em );
246 *
247 * // Result: '<em></em><strong></strong>'
248 *
249 * @param {CKEDITOR.dom.node} node The node that will precede this element.
250 * @returns {CKEDITOR.dom.node} The node preceding this one after insertion.
251 */
252 insertAfter: function( node ) {
253 node.$.parentNode.insertBefore( this.$, node.$.nextSibling );
254 return node;
255 },
256
257 /**
258 * Inserts this element before a node.
259 *
260 * var em = new CKEDITOR.dom.element( 'em' );
261 * var strong = new CKEDITOR.dom.element( 'strong' );
262 * strong.insertBefore( em );
263 *
264 * // result: '<strong></strong><em></em>'
265 *
266 * @param {CKEDITOR.dom.node} node The node that will succeed this element.
267 * @returns {CKEDITOR.dom.node} The node being inserted.
268 */
269 insertBefore: function( node ) {
270 node.$.parentNode.insertBefore( this.$, node.$ );
271 return node;
272 },
273
274 /**
275 * Inserts a node before this node.
276 *
277 * var em = new CKEDITOR.dom.element( 'em' );
278 * var strong = new CKEDITOR.dom.element( 'strong' );
279 * strong.insertBeforeMe( em );
280 *
281 * // result: '<em></em><strong></strong>'
282 *
283 * @param {CKEDITOR.dom.node} node The node that will preceed this element.
284 * @returns {CKEDITOR.dom.node} The node being inserted.
285 */
286 insertBeforeMe: function( node ) {
287 this.$.parentNode.insertBefore( node.$, this.$ );
288 return node;
289 },
290
291 /**
292 * Retrieves a uniquely identifiable tree address for this node.
293 * The tree address returned is an array of integers, with each integer
294 * indicating a child index of a DOM node, starting from
295 * `document.documentElement`.
296 *
297 * For example, assuming `<body>` is the second child
298 * of `<html>` (`<head>` being the first),
299 * and we would like to address the third child under the
300 * fourth child of `<body>`, the tree address returned would be:
301 * `[1, 3, 2]`.
302 *
303 * The tree address cannot be used for finding back the DOM tree node once
304 * the DOM tree structure has been modified.
305 *
306 * @param {Boolean} [normalized=false] See {@link #getIndex}.
307 * @returns {Array} The address.
308 */
309 getAddress: function( normalized ) {
310 var address = [];
311 var $documentElement = this.getDocument().$.documentElement;
312 var node = this.$;
313
314 while ( node && node != $documentElement ) {
315 var parentNode = node.parentNode;
316
317 if ( parentNode ) {
318 // Get the node index. For performance, call getIndex
319 // directly, instead of creating a new node object.
320 address.unshift( this.getIndex.call( { $: node }, normalized ) );
321 }
322
323 node = parentNode;
324 }
325
326 return address;
327 },
328
329 /**
330 * Gets the document containing this element.
331 *
332 * var element = CKEDITOR.document.getById( 'example' );
333 * alert( element.getDocument().equals( CKEDITOR.document ) ); // true
334 *
335 * @returns {CKEDITOR.dom.document} The document.
336 */
337 getDocument: function() {
338 return new CKEDITOR.dom.document( this.$.ownerDocument || this.$.parentNode.ownerDocument );
339 },
340
341 /**
342 * Gets the index of a node in an array of its `parent.childNodes`.
343 * Returns `-1` if a node does not have a parent or when the `normalized` argument is set to `true`
344 * and the text node is empty and will be removed during the normalization.
345 *
346 * Let us assume having the following `childNodes` array:
347 *
348 * [ emptyText, element1, text, text, element2, emptyText2 ]
349 *
350 * emptyText.getIndex() // 0
351 * emptyText.getIndex( true ) // -1
352 * element1.getIndex(); // 1
353 * element1.getIndex( true ); // 0
354 * element2.getIndex(); // 4
355 * element2.getIndex( true ); // 2
356 * emptyText2.getIndex(); // 5
357 * emptyText2.getIndex( true ); // -1
358 *
359 * @param {Boolean} normalized When `true`, adjacent text nodes are merged and empty text nodes are removed.
360 * @returns {Number} Index of a node or `-1` if a node does not have a parent or is removed during the normalization.
361 */
362 getIndex: function( normalized ) {
363 // Attention: getAddress depends on this.$
364 // getIndex is called on a plain object: { $ : node }
365
366 var current = this.$,
367 index = -1,
368 isNormalizing;
369
370 if ( !this.$.parentNode )
371 return -1;
372
373 // The idea is - all empty text nodes will be virtually merged into their adjacent text nodes.
374 // If an empty text node does not have an adjacent non-empty text node we can return -1 straight away,
375 // because it and all its sibling text nodes will be merged into an empty text node and then totally ignored.
376 if ( normalized && current.nodeType == CKEDITOR.NODE_TEXT && isEmpty( current ) ) {
377 var adjacent = getAdjacentNonEmptyTextNode( current ) || getAdjacentNonEmptyTextNode( current, true );
378
379 if ( !adjacent )
380 return -1;
381 }
382
383 do {
384 // Bypass blank node and adjacent text nodes.
385 if ( normalized && current != this.$ && current.nodeType == CKEDITOR.NODE_TEXT && ( isNormalizing || isEmpty( current ) ) )
386 continue;
387
388 index++;
389 isNormalizing = current.nodeType == CKEDITOR.NODE_TEXT;
390 }
391 while ( ( current = current.previousSibling ) );
392
393 return index;
394
395 function getAdjacentNonEmptyTextNode( node, lookForward ) {
396 var sibling = lookForward ? node.nextSibling : node.previousSibling;
397
398 if ( !sibling || sibling.nodeType != CKEDITOR.NODE_TEXT ) {
399 return null;
400 }
401
402 // If found a non-empty text node, then return it.
403 // If not, then continue search.
404 return isEmpty( sibling ) ? getAdjacentNonEmptyTextNode( sibling, lookForward ) : sibling;
405 }
406
407 // Checks whether a text node is empty or is FCSeq string (which will be totally removed when normalizing).
408 function isEmpty( textNode ) {
409 return !textNode.nodeValue || textNode.nodeValue == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE;
410 }
411 },
412
413 /**
414 * @todo
415 */
416 getNextSourceNode: function( startFromSibling, nodeType, guard ) {
417 // If "guard" is a node, transform it in a function.
418 if ( guard && !guard.call ) {
419 var guardNode = guard;
420 guard = function( node ) {
421 return !node.equals( guardNode );
422 };
423 }
424
425 var node = ( !startFromSibling && this.getFirst && this.getFirst() ),
426 parent;
427
428 // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
429 // send the 'moving out' signal even we don't actually dive into.
430 if ( !node ) {
431 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
432 return null;
433 node = this.getNext();
434 }
435
436 while ( !node && ( parent = ( parent || this ).getParent() ) ) {
437 // The guard check sends the "true" paramenter to indicate that
438 // we are moving "out" of the element.
439 if ( guard && guard( parent, true ) === false )
440 return null;
441
442 node = parent.getNext();
443 }
444
445 if ( !node )
446 return null;
447
448 if ( guard && guard( node ) === false )
449 return null;
450
451 if ( nodeType && nodeType != node.type )
452 return node.getNextSourceNode( false, nodeType, guard );
453
454 return node;
455 },
456
457 /**
458 * @todo
459 */
460 getPreviousSourceNode: function( startFromSibling, nodeType, guard ) {
461 if ( guard && !guard.call ) {
462 var guardNode = guard;
463 guard = function( node ) {
464 return !node.equals( guardNode );
465 };
466 }
467
468 var node = ( !startFromSibling && this.getLast && this.getLast() ),
469 parent;
470
471 // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
472 // send the 'moving out' signal even we don't actually dive into.
473 if ( !node ) {
474 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
475 return null;
476 node = this.getPrevious();
477 }
478
479 while ( !node && ( parent = ( parent || this ).getParent() ) ) {
480 // The guard check sends the "true" paramenter to indicate that
481 // we are moving "out" of the element.
482 if ( guard && guard( parent, true ) === false )
483 return null;
484
485 node = parent.getPrevious();
486 }
487
488 if ( !node )
489 return null;
490
491 if ( guard && guard( node ) === false )
492 return null;
493
494 if ( nodeType && node.type != nodeType )
495 return node.getPreviousSourceNode( false, nodeType, guard );
496
497 return node;
498 },
499
500 /**
501 * Gets the node that preceeds this element in its parent's child list.
502 *
503 * var element = CKEDITOR.dom.element.createFromHtml( '<div><i>prev</i><b>Example</b></div>' );
504 * var first = element.getLast().getPrev();
505 * alert( first.getName() ); // 'i'
506 *
507 * @param {Function} [evaluator] Filtering the result node.
508 * @returns {CKEDITOR.dom.node} The previous node or null if not available.
509 */
510 getPrevious: function( evaluator ) {
511 var previous = this.$,
512 retval;
513 do {
514 previous = previous.previousSibling;
515
516 // Avoid returning the doc type node.
517 // http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927
518 retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous );
519 }
520 while ( retval && evaluator && !evaluator( retval ) );
521 return retval;
522 },
523
524 /**
525 * Gets the node that follows this element in its parent's child list.
526 *
527 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b><i>next</i></div>' );
528 * var last = element.getFirst().getNext();
529 * alert( last.getName() ); // 'i'
530 *
531 * @param {Function} [evaluator] Filtering the result node.
532 * @returns {CKEDITOR.dom.node} The next node or null if not available.
533 */
534 getNext: function( evaluator ) {
535 var next = this.$,
536 retval;
537 do {
538 next = next.nextSibling;
539 retval = next && new CKEDITOR.dom.node( next );
540 }
541 while ( retval && evaluator && !evaluator( retval ) );
542 return retval;
543 },
544
545 /**
546 * Gets the parent element for this node.
547 *
548 * var node = editor.document.getBody().getFirst();
549 * var parent = node.getParent();
550 * alert( parent.getName() ); // 'body'
551 *
552 * @param {Boolean} [allowFragmentParent=false] Consider also parent node that is of
553 * fragment type {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
554 * @returns {CKEDITOR.dom.element} The parent element.
555 */
556 getParent: function( allowFragmentParent ) {
557 var parent = this.$.parentNode;
558 return ( parent && ( parent.nodeType == CKEDITOR.NODE_ELEMENT || allowFragmentParent && parent.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) ? new CKEDITOR.dom.node( parent ) : null;
559 },
560
561 /**
562 * Returns an array containing node parents and the node itself. By default nodes are in _descending_ order.
563 *
564 * // Assuming that body has paragraph as the first child.
565 * var node = editor.document.getBody().getFirst();
566 * var parents = node.getParents();
567 * alert( parents[ 0 ].getName() + ',' + parents[ 2 ].getName() ); // 'html,p'
568 *
569 * @param {Boolean} [closerFirst=false] Determines the order of returned nodes.
570 * @returns {Array} Returns an array of {@link CKEDITOR.dom.node}.
571 */
572 getParents: function( closerFirst ) {
573 var node = this;
574 var parents = [];
575
576 do {
577 parents[ closerFirst ? 'push' : 'unshift' ]( node );
578 }
579 while ( ( node = node.getParent() ) );
580
581 return parents;
582 },
583
584 /**
585 * @todo
586 */
587 getCommonAncestor: function( node ) {
588 if ( node.equals( this ) )
589 return this;
590
591 if ( node.contains && node.contains( this ) )
592 return node;
593
594 var start = this.contains ? this : this.getParent();
595
596 do {
597 if ( start.contains( node ) ) return start;
598 }
599 while ( ( start = start.getParent() ) );
600
601 return null;
602 },
603
604 /**
605 * Determines the position relation between this node and the given {@link CKEDITOR.dom.node} in the document.
606 * This node can be preceding ({@link CKEDITOR#POSITION_PRECEDING}) or following ({@link CKEDITOR#POSITION_FOLLOWING})
607 * the given node. This node can also contain ({@link CKEDITOR#POSITION_CONTAINS}) or be contained by
608 * ({@link CKEDITOR#POSITION_IS_CONTAINED}) the given node. The function returns a bitmask of constants
609 * listed above or {@link CKEDITOR#POSITION_IDENTICAL} if the given node is the same as this node.
610 *
611 * @param {CKEDITOR.dom.node} otherNode A node to check relation with.
612 * @returns {Number} Position relation between this node and given node.
613 */
614 getPosition: function( otherNode ) {
615 var $ = this.$;
616 var $other = otherNode.$;
617
618 if ( $.compareDocumentPosition )
619 return $.compareDocumentPosition( $other );
620
621 // IE and Safari have no support for compareDocumentPosition.
622
623 if ( $ == $other )
624 return CKEDITOR.POSITION_IDENTICAL;
625
626 // Only element nodes support contains and sourceIndex.
627 if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) {
628 if ( $.contains ) {
629 if ( $.contains( $other ) )
630 return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING;
631
632 if ( $other.contains( $ ) )
633 return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
634 }
635
636 if ( 'sourceIndex' in $ )
637 return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
638
639 }
640
641 // For nodes that don't support compareDocumentPosition, contains
642 // or sourceIndex, their "address" is compared.
643
644 var addressOfThis = this.getAddress(),
645 addressOfOther = otherNode.getAddress(),
646 minLevel = Math.min( addressOfThis.length, addressOfOther.length );
647
648 // Determinate preceding/following relationship.
649 for ( var i = 0; i < minLevel; i++ ) {
650 if ( addressOfThis[ i ] != addressOfOther[ i ] ) {
651 return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
652 }
653 }
654
655 // Determinate contains/contained relationship.
656 return ( addressOfThis.length < addressOfOther.length ) ? CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
657 },
658
659 /**
660 * Gets the closest ancestor node of this node, specified by its name or using an evaluator function.
661 *
662 * // Suppose we have the following HTML structure:
663 * // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div>
664 * // If node == <b>
665 * ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner">
666 * ascendant = node.getAscendant( 'b' ); // ascendant == null
667 * ascendant = node.getAscendant( 'b', true ); // ascendant == <b>
668 * ascendant = node.getAscendant( { div:1, p:1 } ); // Searches for the first 'div' or 'p': ascendant == <div id="inner">
669 *
670 * // Using custom evaluator:
671 * ascendant = node.getAscendant( function( el ) {
672 * return el.getId() == 'inner';
673 * } );
674 * // ascendant == <div id="inner">
675 *
676 * @since 3.6.1
677 * @param {String/Function/Object} query The name of the ancestor node to search or
678 * an object with the node names to search for or an evaluator function.
679 * @param {Boolean} [includeSelf] Whether to include the current
680 * node in the search.
681 * @returns {CKEDITOR.dom.node} The located ancestor node or `null` if not found.
682 */
683 getAscendant: function( query, includeSelf ) {
684 var $ = this.$,
685 evaluator,
686 isCustomEvaluator;
687
688 if ( !includeSelf ) {
689 $ = $.parentNode;
690 }
691
692 // Custom checker provided in an argument.
693 if ( typeof query == 'function' ) {
694 isCustomEvaluator = true;
695 evaluator = query;
696 } else {
697 // Predefined tag name checker.
698 isCustomEvaluator = false;
699 evaluator = function( $ ) {
700 var name = ( typeof $.nodeName == 'string' ? $.nodeName.toLowerCase() : '' );
701
702 return ( typeof query == 'string' ? name == query : name in query );
703 };
704 }
705
706 while ( $ ) {
707 // For user provided checker we use CKEDITOR.dom.node.
708 if ( evaluator( isCustomEvaluator ? new CKEDITOR.dom.node( $ ) : $ ) ) {
709 return new CKEDITOR.dom.node( $ );
710 }
711
712 try {
713 $ = $.parentNode;
714 } catch ( e ) {
715 $ = null;
716 }
717 }
718
719 return null;
720 },
721
722 /**
723 * @todo
724 */
725 hasAscendant: function( name, includeSelf ) {
726 var $ = this.$;
727
728 if ( !includeSelf )
729 $ = $.parentNode;
730
731 while ( $ ) {
732 if ( $.nodeName && $.nodeName.toLowerCase() == name )
733 return true;
734
735 $ = $.parentNode;
736 }
737 return false;
738 },
739
740 /**
741 * @todo
742 */
743 move: function( target, toStart ) {
744 target.append( this.remove(), toStart );
745 },
746
747 /**
748 * Removes this node from the document DOM.
749 *
750 * var element = CKEDITOR.document.getById( 'MyElement' );
751 * element.remove();
752 *
753 * @param {Boolean} [preserveChildren=false] Indicates that the children
754 * elements must remain in the document, removing only the outer tags.
755 */
756 remove: function( preserveChildren ) {
757 var $ = this.$;
758 var parent = $.parentNode;
759
760 if ( parent ) {
761 if ( preserveChildren ) {
762 // Move all children before the node.
763 for ( var child;
764 ( child = $.firstChild ); ) {
765 parent.insertBefore( $.removeChild( child ), $ );
766 }
767 }
768
769 parent.removeChild( $ );
770 }
771
772 return this;
773 },
774
775 /**
776 * @todo
777 */
778 replace: function( nodeToReplace ) {
779 this.insertBefore( nodeToReplace );
780 nodeToReplace.remove();
781 },
782
783 /**
784 * @todo
785 */
786 trim: function() {
787 this.ltrim();
788 this.rtrim();
789 },
790
791 /**
792 * @todo
793 */
794 ltrim: function() {
795 var child;
796 while ( this.getFirst && ( child = this.getFirst() ) ) {
797 if ( child.type == CKEDITOR.NODE_TEXT ) {
798 var trimmed = CKEDITOR.tools.ltrim( child.getText() ),
799 originalLength = child.getLength();
800
801 if ( !trimmed ) {
802 child.remove();
803 continue;
804 } else if ( trimmed.length < originalLength ) {
805 child.split( originalLength - trimmed.length );
806
807 // IE BUG: child.remove() may raise JavaScript errors here. (#81)
808 this.$.removeChild( this.$.firstChild );
809 }
810 }
811 break;
812 }
813 },
814
815 /**
816 * @todo
817 */
818 rtrim: function() {
819 var child;
820 while ( this.getLast && ( child = this.getLast() ) ) {
821 if ( child.type == CKEDITOR.NODE_TEXT ) {
822 var trimmed = CKEDITOR.tools.rtrim( child.getText() ),
823 originalLength = child.getLength();
824
825 if ( !trimmed ) {
826 child.remove();
827 continue;
828 } else if ( trimmed.length < originalLength ) {
829 child.split( trimmed.length );
830
831 // IE BUG: child.getNext().remove() may raise JavaScript errors here.
832 // (#81)
833 this.$.lastChild.parentNode.removeChild( this.$.lastChild );
834 }
835 }
836 break;
837 }
838
839 if ( CKEDITOR.env.needsBrFiller ) {
840 child = this.$.lastChild;
841
842 if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) {
843 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324).
844 child.parentNode.removeChild( child );
845 }
846 }
847 },
848
849 /**
850 * Checks if this node is read-only (should not be changed).
851 *
852 * // For the following HTML:
853 * // <b>foo</b><div contenteditable="false"><i>bar</i></div>
854 *
855 * elB.isReadOnly(); // -> false
856 * foo.isReadOnly(); // -> false
857 * elDiv.isReadOnly(); // -> true
858 * elI.isReadOnly(); // -> true
859 *
860 * This method works in two modes depending on browser support for the `element.isContentEditable` property and
861 * the value of the `checkOnlyAttributes` parameter. The `element.isContentEditable` check is faster, but it is known
862 * to malfunction in hidden or detached nodes. Additionally, when processing some detached DOM tree you may want to imitate
863 * that this happens inside an editable container (like it would happen inside the {@link CKEDITOR.editable}). To do so,
864 * you can temporarily attach this tree to an element with the `data-cke-editable` attribute and use the
865 * `checkOnlyAttributes` mode.
866 *
867 * @since 3.5
868 * @param {Boolean} [checkOnlyAttributes=false] If `true`, only attributes will be checked, native methods will not
869 * be used. This parameter needs to be `true` to check hidden or detached elements. Introduced in 4.5.
870 * @returns {Boolean}
871 */
872 isReadOnly: function( checkOnlyAttributes ) {
873 var element = this;
874 if ( this.type != CKEDITOR.NODE_ELEMENT )
875 element = this.getParent();
876
877 // Prevent Edge crash (#13609, #13919).
878 if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) {
879 checkOnlyAttributes = true;
880 }
881
882 if ( !checkOnlyAttributes && element && typeof element.$.isContentEditable != 'undefined' ) {
883 return !( element.$.isContentEditable || element.data( 'cke-editable' ) );
884 }
885 else {
886 // Degrade for old browsers which don't support "isContentEditable", e.g. FF3
887
888 while ( element ) {
889 if ( element.data( 'cke-editable' ) ) {
890 return false;
891 } else if ( element.hasAttribute( 'contenteditable' ) ) {
892 return element.getAttribute( 'contenteditable' ) == 'false';
893 }
894
895 element = element.getParent();
896 }
897
898 // Reached the root of DOM tree, no editable found.
899 return true;
900 }
901 }
902} );
diff --git a/sources/core/dom/nodelist.js b/sources/core/dom/nodelist.js
new file mode 100644
index 0000000..165a415
--- /dev/null
+++ b/sources/core/dom/nodelist.js
@@ -0,0 +1,43 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * Represents a list of {@link CKEDITOR.dom.node} objects.
8 * It's a wrapper for native nodes list.
9 *
10 * var nodeList = CKEDITOR.document.getBody().getChildren();
11 * alert( nodeList.count() ); // number [0;N]
12 *
13 * @class
14 * @constructor Creates a document class instance.
15 * @param {Object} nativeList
16 */
17CKEDITOR.dom.nodeList = function( nativeList ) {
18 this.$ = nativeList;
19};
20
21CKEDITOR.dom.nodeList.prototype = {
22 /**
23 * Get count of nodes in this list.
24 *
25 * @returns {Number}
26 */
27 count: function() {
28 return this.$.length;
29 },
30
31 /**
32 * Get node from the list.
33 *
34 * @returns {CKEDITOR.dom.node}
35 */
36 getItem: function( index ) {
37 if ( index < 0 || index >= this.$.length )
38 return null;
39
40 var $node = this.$[ index ];
41 return $node ? new CKEDITOR.dom.node( $node ) : null;
42 }
43};
diff --git a/sources/core/dom/range.js b/sources/core/dom/range.js
new file mode 100644
index 0000000..6407074
--- /dev/null
+++ b/sources/core/dom/range.js
@@ -0,0 +1,2978 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * Represents a delimited piece of content in a DOM Document.
8 * It is contiguous in the sense that it can be characterized as selecting all
9 * of the content between a pair of boundary-points.
10 *
11 * This class shares much of the W3C
12 * [Document Object Model Range](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html)
13 * ideas and features, adding several range manipulation tools to it, but it's
14 * not intended to be compatible with it.
15 *
16 * // Create a range for the entire contents of the editor document body.
17 * var range = new CKEDITOR.dom.range( editor.document );
18 * range.selectNodeContents( editor.document.getBody() );
19 * // Delete the contents.
20 * range.deleteContents();
21 *
22 * Usually you will want to work on a ranges rooted in the editor's {@link CKEDITOR.editable editable}
23 * element. Such ranges can be created with a shorthand method &ndash; {@link CKEDITOR.editor#createRange editor.createRange}.
24 *
25 * var range = editor.createRange();
26 * range.root.equals( editor.editable() ); // -> true
27 *
28 * Note that the {@link #root} of a range is an important property, which limits many
29 * algorithms implemented in range's methods. Therefore it is crucial, especially
30 * when using ranges inside inline editors, to specify correct root, so using
31 * the {@link CKEDITOR.editor#createRange} method is highly recommended.
32 *
33 * ### Selection
34 *
35 * Range is only a logical representation of a piece of content in a DOM. It should not
36 * be confused with a {@link CKEDITOR.dom.selection selection} which represents "physically
37 * marked" content. It is possible to create unlimited number of various ranges, when
38 * only one real selection may exist at a time in a document. Ranges are used to read position
39 * of selection in the DOM and to move selection to new positions.
40 *
41 * The editor selection may be retrieved using the {@link CKEDITOR.editor#getSelection} method:
42 *
43 * var sel = editor.getSelection(),
44 * ranges = sel.getRanges(); // CKEDITOR.dom.rangeList instance.
45 *
46 * var range = ranges[ 0 ];
47 * range.root; // -> editor's editable element.
48 *
49 * A range can also be selected:
50 *
51 * var range = editor.createRange();
52 * range.selectNodeContents( editor.editable() );
53 * sel.selectRanges( [ range ] );
54 *
55 * @class
56 * @constructor Creates a {@link CKEDITOR.dom.range} instance that can be used inside a specific DOM Document.
57 * @param {CKEDITOR.dom.document/CKEDITOR.dom.element} root The document or element
58 * within which the range will be scoped.
59 * @todo global "TODO" - precise algorithms descriptions needed for the most complex methods like #enlarge.
60 */
61CKEDITOR.dom.range = function( root ) {
62 /**
63 * Node within which the range begins.
64 *
65 * var range = new CKEDITOR.dom.range( editor.document );
66 * range.selectNodeContents( editor.document.getBody() );
67 * alert( range.startContainer.getName() ); // 'body'
68 *
69 * @readonly
70 * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
71 */
72 this.startContainer = null;
73
74 /**
75 * Offset within the starting node of the range.
76 *
77 * var range = new CKEDITOR.dom.range( editor.document );
78 * range.selectNodeContents( editor.document.getBody() );
79 * alert( range.startOffset ); // 0
80 *
81 * @readonly
82 * @property {Number}
83 */
84 this.startOffset = null;
85
86 /**
87 * Node within which the range ends.
88 *
89 * var range = new CKEDITOR.dom.range( editor.document );
90 * range.selectNodeContents( editor.document.getBody() );
91 * alert( range.endContainer.getName() ); // 'body'
92 *
93 * @readonly
94 * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
95 */
96 this.endContainer = null;
97
98 /**
99 * Offset within the ending node of the range.
100 *
101 * var range = new CKEDITOR.dom.range( editor.document );
102 * range.selectNodeContents( editor.document.getBody() );
103 * alert( range.endOffset ); // == editor.document.getBody().getChildCount()
104 *
105 * @readonly
106 * @property {Number}
107 */
108 this.endOffset = null;
109
110 /**
111 * Indicates that this is a collapsed range. A collapsed range has its
112 * start and end boundaries at the very same point so nothing is contained
113 * in it.
114 *
115 * var range = new CKEDITOR.dom.range( editor.document );
116 * range.selectNodeContents( editor.document.getBody() );
117 * alert( range.collapsed ); // false
118 * range.collapse();
119 * alert( range.collapsed ); // true
120 *
121 * @readonly
122 */
123 this.collapsed = true;
124
125 var isDocRoot = root instanceof CKEDITOR.dom.document;
126 /**
127 * The document within which the range can be used.
128 *
129 * // Selects the body contents of the range document.
130 * range.selectNodeContents( range.document.getBody() );
131 *
132 * @readonly
133 * @property {CKEDITOR.dom.document}
134 */
135 this.document = isDocRoot ? root : root.getDocument();
136
137 /**
138 * The ancestor DOM element within which the range manipulation are limited.
139 *
140 * @readonly
141 * @property {CKEDITOR.dom.element}
142 */
143 this.root = isDocRoot ? root.getBody() : root;
144};
145
146( function() {
147 // Updates the "collapsed" property for the given range object.
148 function updateCollapsed( range ) {
149 range.collapsed = ( range.startContainer && range.endContainer && range.startContainer.equals( range.endContainer ) && range.startOffset == range.endOffset );
150 }
151
152 // This is a shared function used to delete, extract and clone the range content.
153 //
154 // The outline of the algorithm:
155 //
156 // 1. Normalization. We handle special cases, split text nodes if we can, find boundary nodes (startNode and endNode).
157 // 2. Gathering data.
158 // * We start by creating two arrays of boundary nodes parents. You can imagine these arrays as lines limiting
159 // the tree from the left and right and thus marking the part which is selected by the range. The both lines
160 // start in the same node which is the range.root and end in startNode and endNode.
161 // * Then we find min level and max levels. Level represents all nodes which are equally far from the range.root.
162 // Min level is the level at which the left and right boundaries diverged (the first diverged level). And max levels
163 // are how deep the start and end nodes are nested.
164 // 3. Cloning/extraction.
165 // * We start iterating over start node parents (left branch) from min level and clone the parent (usually shallow clone,
166 // because we know that it's not fully selected) and its right siblings (deep clone, because they are fully selected).
167 // We iterate over siblings up to meeting end node parent or end of the siblings chain.
168 // * We clone level after level down to the startNode.
169 // * Then we do the same with end node parents (right branch), because it may contains notes we omit during the previous
170 // step, for example if the right branch is deeper then left branch. Things are more complicated here because we have to
171 // watch out for nodes that were already cloned.
172 // * ***Note:** Setting `cloneId` option to `false` for **extraction** works for partially selected elements only.
173 // See range.extractContents to know more.
174 // 4. Clean up.
175 // * There are two things we need to do - updating the range position and perform the action of the "mergeThen"
176 // param (see range.deleteContents or range.extractContents).
177 // See comments in mergeAndUpdate because this is lots of fun too.
178 function execContentsAction( range, action, docFrag, mergeThen, cloneId ) {
179 'use strict';
180
181 range.optimizeBookmark();
182
183 var isDelete = action === 0;
184 var isExtract = action == 1;
185 var isClone = action == 2;
186 var doClone = isClone || isExtract;
187
188 var startNode = range.startContainer;
189 var endNode = range.endContainer;
190
191 var startOffset = range.startOffset;
192 var endOffset = range.endOffset;
193
194 var cloneStartNode;
195 var cloneEndNode;
196
197 var doNotRemoveStartNode;
198 var doNotRemoveEndNode;
199
200 var cloneStartText;
201 var cloneEndText;
202
203 // Handle here an edge case where we clone a range which is located in one text node.
204 // This allows us to not think about startNode == endNode case later on.
205 // We do that only when cloning, because in other cases we can safely split this text node
206 // and hence we can easily handle this case as many others.
207 if ( isClone && endNode.type == CKEDITOR.NODE_TEXT && startNode.equals( endNode ) ) {
208 startNode = range.document.createText( startNode.substring( startOffset, endOffset ) );
209 docFrag.append( startNode );
210 return;
211 }
212
213 // For text containers, we must simply split the node and point to the
214 // second part. The removal will be handled by the rest of the code.
215 if ( endNode.type == CKEDITOR.NODE_TEXT ) {
216 // If Extract or Delete we can split the text node,
217 // but if Clone (2), then we cannot modify the DOM (#11586) so we mark the text node for cloning.
218 if ( !isClone ) {
219 endNode = endNode.split( endOffset );
220 } else {
221 cloneEndText = true;
222 }
223 } else {
224 // If there's no node after the range boundary we set endNode to the previous node
225 // and mark it to be cloned.
226 if ( endNode.getChildCount() > 0 ) {
227 // If the offset points after the last node.
228 if ( endOffset >= endNode.getChildCount() ) {
229 endNode = endNode.getChild( endOffset - 1 );
230 cloneEndNode = true;
231 } else {
232 endNode = endNode.getChild( endOffset );
233 }
234 }
235 // The end container is empty (<h1>]</h1>), but we want to clone it, although not remove.
236 else {
237 cloneEndNode = true;
238 doNotRemoveEndNode = true;
239 }
240 }
241
242 // For text containers, we must simply split the node. The removal will
243 // be handled by the rest of the code .
244 if ( startNode.type == CKEDITOR.NODE_TEXT ) {
245 // If Extract or Delete we can split the text node,
246 // but if Clone (2), then we cannot modify the DOM (#11586) so we mark
247 // the text node for cloning.
248 if ( !isClone ) {
249 startNode.split( startOffset );
250 } else {
251 cloneStartText = true;
252 }
253 } else {
254 // If there's no node before the range boundary we set startNode to the next node
255 // and mark it to be cloned.
256 if ( startNode.getChildCount() > 0 ) {
257 if ( startOffset === 0 ) {
258 startNode = startNode.getChild( startOffset );
259 cloneStartNode = true;
260 } else {
261 startNode = startNode.getChild( startOffset - 1 );
262 }
263 }
264 // The start container is empty (<h1>[</h1>), but we want to clone it, although not remove.
265 else {
266 cloneStartNode = true;
267 doNotRemoveStartNode = true;
268 }
269 }
270
271 // Get the parent nodes tree for the start and end boundaries.
272 var startParents = startNode.getParents(),
273 endParents = endNode.getParents(),
274 // Level at which start and end boundaries diverged.
275 minLevel = findMinLevel(),
276 maxLevelLeft = startParents.length - 1,
277 maxLevelRight = endParents.length - 1,
278 // Keeps the frag/element which is parent of the level that we are currently cloning.
279 levelParent = docFrag,
280 nextLevelParent,
281 leftNode,
282 rightNode,
283 nextSibling,
284 // Keeps track of the last connected level (on which left and right branches are connected)
285 // Usually this is minLevel, but not always.
286 lastConnectedLevel = -1;
287
288 // THE LEFT BRANCH.
289 for ( var level = minLevel; level <= maxLevelLeft; level++ ) {
290 leftNode = startParents[ level ];
291 nextSibling = leftNode.getNext();
292
293 // 1.
294 // The first step is to handle partial selection of the left branch.
295
296 // Max depth of the left branch. It means that ( leftSibling == endNode ).
297 // We also check if the leftNode isn't only partially selected, because in this case
298 // we want to make a shallow clone of it (the else part).
299 if ( level == maxLevelLeft && !( leftNode.equals( endParents[ level ] ) && maxLevelLeft < maxLevelRight ) ) {
300 if ( cloneStartNode ) {
301 consume( leftNode, levelParent, false, doNotRemoveStartNode );
302 } else if ( cloneStartText ) {
303 levelParent.append( range.document.createText( leftNode.substring( startOffset ) ) );
304 }
305 } else if ( doClone ) {
306 nextLevelParent = levelParent.append( leftNode.clone( 0, cloneId ) );
307 }
308
309 // 2.
310 // The second step is to handle full selection of the content between the left branch and the right branch.
311
312 while ( nextSibling ) {
313 // We can't clone entire endParent just like we can't clone entire startParent -
314 // - they are not fully selected with the range. Partial endParent selection
315 // will be cloned in the next loop.
316 if ( nextSibling.equals( endParents[ level ] ) ) {
317 lastConnectedLevel = level;
318 break;
319 }
320
321 nextSibling = consume( nextSibling, levelParent );
322 }
323
324 levelParent = nextLevelParent;
325 }
326
327 // Reset levelParent, because we reset the level.
328 levelParent = docFrag;
329
330 // THE RIGHT BRANCH.
331 for ( level = minLevel; level <= maxLevelRight; level++ ) {
332 rightNode = endParents[ level ];
333 nextSibling = rightNode.getPrevious();
334
335 // Do not process this node if it is shared with the left branch
336 // because it was already processed.
337 //
338 // Note: Don't worry about text nodes selection - if the entire range was placed in a single text node
339 // it was handled as a special case at the beginning. In other cases when startNode == endNode
340 // or when on this level leftNode == rightNode (so rightNode.equals( startParents[ level ] ))
341 // this node was handled by the previous loop.
342 if ( !rightNode.equals( startParents[ level ] ) ) {
343 // 1.
344 // The first step is to handle partial selection of the right branch.
345
346 // Max depth of the right branch. It means that ( rightNode == endNode ).
347 // We also check if the rightNode isn't only partially selected, because in this case
348 // we want to make a shallow clone of it (the else part).
349 if ( level == maxLevelRight && !( rightNode.equals( startParents[ level ] ) && maxLevelRight < maxLevelLeft ) ) {
350 if ( cloneEndNode ) {
351 consume( rightNode, levelParent, false, doNotRemoveEndNode );
352 } else if ( cloneEndText ) {
353 levelParent.append( range.document.createText( rightNode.substring( 0, endOffset ) ) );
354 }
355 } else if ( doClone ) {
356 nextLevelParent = levelParent.append( rightNode.clone( 0, cloneId ) );
357 }
358
359 // 2.
360 // The second step is to handle all left (selected) siblings of the rightNode which
361 // have not yet been handled. If the level branches were connected, the previous loop
362 // already copied all siblings (except the current rightNode).
363 if ( level > lastConnectedLevel ) {
364 while ( nextSibling ) {
365 nextSibling = consume( nextSibling, levelParent, true );
366 }
367 }
368
369 levelParent = nextLevelParent;
370 } else if ( doClone ) {
371 // If this is "shared" node and we are in cloning mode we have to update levelParent to
372 // reflect that we visited the node (even though we didn't process it).
373 // If we don't do that, in next iterations nodes will be appended to wrong parent.
374 //
375 // We can just take first child because the algorithm guarantees
376 // that this will be the only child on this level. (#13568)
377 levelParent = levelParent.getChild( 0 );
378 }
379 }
380
381 // Delete or Extract.
382 // We need to update the range and if mergeThen was passed do it.
383 if ( !isClone ) {
384 mergeAndUpdate();
385 }
386
387 // Depending on an action:
388 // * clones node and adds to new parent,
389 // * removes node,
390 // * moves node to the new parent.
391 function consume( node, newParent, toStart, forceClone ) {
392 var nextSibling = toStart ? node.getPrevious() : node.getNext();
393
394 // We do not clone if we are only deleting, so do nothing.
395 if ( forceClone && isDelete ) {
396 return nextSibling;
397 }
398
399 // If cloning, just clone it.
400 if ( isClone || forceClone ) {
401 newParent.append( node.clone( true, cloneId ), toStart );
402 } else {
403 // Both Delete and Extract will remove the node.
404 node.remove();
405
406 // When Extracting, move the removed node to the docFrag.
407 if ( isExtract ) {
408 newParent.append( node );
409 }
410 }
411
412 return nextSibling;
413 }
414
415 // Finds a level number on which both branches starts diverging.
416 // If such level does not exist, return the last on which both branches have nodes.
417 function findMinLevel() {
418 // Compare them, to find the top most siblings.
419 var i, topStart, topEnd,
420 maxLevel = Math.min( startParents.length, endParents.length );
421
422 for ( i = 0; i < maxLevel; i++ ) {
423 topStart = startParents[ i ];
424 topEnd = endParents[ i ];
425
426 // The compared nodes will match until we find the top most siblings (different nodes that have the same parent).
427 // "i" will hold the index in the parents array for the top most element.
428 if ( !topStart.equals( topEnd ) ) {
429 return i;
430 }
431 }
432
433 // When startNode == endNode.
434 return i - 1;
435 }
436
437 // Executed only when deleting or extracting to update range position
438 // and perform the merge operation.
439 function mergeAndUpdate() {
440 var commonLevel = minLevel - 1,
441 boundariesInEmptyNode = doNotRemoveStartNode && doNotRemoveEndNode && !startNode.equals( endNode );
442
443 // If a node has been partially selected, collapse the range between
444 // startParents[ minLevel + 1 ] and endParents[ minLevel + 1 ] (the first diverged elements).
445 // Otherwise, simply collapse it to the start. (W3C specs).
446 //
447 // All clear, right?
448 //
449 // It took me few hours to truly understand a previous version of this condition.
450 // Mine seems to be more straightforward (even if it doesn't look so) and I could leave you here
451 // without additional comments, but I'm not that mean so here goes the explanation.
452 //
453 // We want to know if both ends of the range are anchored in the same element. Really. It's this simple.
454 // But why? Because we need to differentiate situations like:
455 //
456 // <p>foo[<b>x</b>bar]y</p> (commonLevel = p, maxLL = "foo", maxLR = "y")
457 // from:
458 // <p>foo<b>x[</b>bar]y</p> (commonLevel = p, maxLL = "x", maxLR = "y")
459 //
460 // In the first case we can collapse the range to the left, because simply everything between range's
461 // boundaries was removed.
462 // In the second case we must place the range after </b>, because <b> was only **partially selected**.
463 //
464 // * <b> is our startParents[ commonLevel + 1 ]
465 // * "y" is our endParents[ commonLevel + 1 ].
466 //
467 // By now "bar" is removed from the DOM so <b> is a direct sibling of "y":
468 // <p>foo<b>x</b>y</p>
469 //
470 // Therefore it's enough to place the range between <b> and "y".
471 //
472 // Now, what does the comparison mean? Why not just taking startNode and endNode and checking
473 // their parents? Because the tree is already changed and they may be gone. Plus, thanks to
474 // cloneStartNode and cloneEndNode, that would be reaaaaly tricky.
475 //
476 // So we play with levels which can give us the same information:
477 // * commonLevel - the level of common ancestor,
478 // * maxLevel - 1 - the level of range boundary parent (range boundary is here like a bookmark span).
479 // * commonLevel < maxLevel - 1 - whether the range boundary is not a child of common ancestor.
480 //
481 // There's also an edge case in which both range boundaries were placed in empty nodes like:
482 // <p>[</p><p>]</p>
483 // Those boundaries were not removed, but in this case start and end nodes are child of the common ancestor.
484 // We handle this edge case separately.
485 if ( commonLevel < ( maxLevelLeft - 1 ) || commonLevel < ( maxLevelRight - 1 ) || boundariesInEmptyNode ) {
486 if ( boundariesInEmptyNode ) {
487 range.moveToPosition( endNode, CKEDITOR.POSITION_BEFORE_START );
488 } else if ( ( maxLevelRight == commonLevel + 1 ) && cloneEndNode ) {
489 // The maxLevelRight + 1 element could be already removed so we use the fact that
490 // we know that it was the last element in its parent.
491 range.moveToPosition( endParents[ commonLevel ], CKEDITOR.POSITION_BEFORE_END );
492 } else {
493 range.moveToPosition( endParents[ commonLevel + 1 ], CKEDITOR.POSITION_BEFORE_START );
494 }
495
496 // Merge split parents.
497 if ( mergeThen ) {
498 // Find the first diverged node in the left branch.
499 var topLeft = startParents[ commonLevel + 1 ];
500
501 // TopLeft may simply not exist if commonLevel == maxLevel or may be a text node.
502 if ( topLeft && topLeft.type == CKEDITOR.NODE_ELEMENT ) {
503 var span = CKEDITOR.dom.element.createFromHtml( '<span ' +
504 'data-cke-bookmark="1" style="display:none">&nbsp;</span>', range.document );
505 span.insertAfter( topLeft );
506 topLeft.mergeSiblings( false );
507 range.moveToBookmark( { startNode: span } );
508 }
509 }
510 } else {
511 // Collapse it to the start.
512 range.collapse( true );
513 }
514 }
515 }
516
517 var inlineChildReqElements = {
518 abbr: 1, acronym: 1, b: 1, bdo: 1, big: 1, cite: 1, code: 1, del: 1,
519 dfn: 1, em: 1, font: 1, i: 1, ins: 1, label: 1, kbd: 1, q: 1, samp: 1, small: 1, span: 1, strike: 1,
520 strong: 1, sub: 1, sup: 1, tt: 1, u: 1, 'var': 1
521 };
522
523 // Creates the appropriate node evaluator for the dom walker used inside
524 // check(Start|End)OfBlock.
525 function getCheckStartEndBlockEvalFunction() {
526 var skipBogus = false,
527 whitespaces = CKEDITOR.dom.walker.whitespaces(),
528 bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),
529 isBogus = CKEDITOR.dom.walker.bogus();
530
531 return function( node ) {
532 // First skip empty nodes
533 if ( bookmarkEvaluator( node ) || whitespaces( node ) )
534 return true;
535
536 // Skip the bogus node at the end of block.
537 if ( isBogus( node ) && !skipBogus ) {
538 skipBogus = true;
539 return true;
540 }
541
542 // If there's any visible text, then we're not at the start.
543 if ( node.type == CKEDITOR.NODE_TEXT &&
544 ( node.hasAscendant( 'pre' ) ||
545 CKEDITOR.tools.trim( node.getText() ).length ) ) {
546 return false;
547 }
548
549 // If there are non-empty inline elements (e.g. <img />), then we're not
550 // at the start.
551 if ( node.type == CKEDITOR.NODE_ELEMENT && !node.is( inlineChildReqElements ) )
552 return false;
553
554 return true;
555 };
556 }
557
558 var isBogus = CKEDITOR.dom.walker.bogus(),
559 nbspRegExp = /^[\t\r\n ]*(?:&nbsp;|\xa0)$/,
560 editableEval = CKEDITOR.dom.walker.editable(),
561 notIgnoredEval = CKEDITOR.dom.walker.ignored( true );
562
563 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
564 // text node and non-empty elements unless it's being bookmark text.
565 function elementBoundaryEval( checkStart ) {
566 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
567 bookmark = CKEDITOR.dom.walker.bookmark( 1 );
568
569 return function( node ) {
570 // First skip empty nodes.
571 if ( bookmark( node ) || whitespaces( node ) )
572 return true;
573
574 // Tolerant bogus br when checking at the end of block.
575 // Reject any text node unless it's being bookmark
576 // OR it's spaces.
577 // Reject any element unless it's being invisible empty. (#3883)
578 return !checkStart && isBogus( node ) ||
579 node.type == CKEDITOR.NODE_ELEMENT &&
580 node.is( CKEDITOR.dtd.$removeEmpty );
581 };
582 }
583
584 function getNextEditableNode( isPrevious ) {
585 return function() {
586 var first;
587
588 return this[ isPrevious ? 'getPreviousNode' : 'getNextNode' ]( function( node ) {
589 // Cache first not ignorable node.
590 if ( !first && notIgnoredEval( node ) )
591 first = node;
592
593 // Return true if found editable node, but not a bogus next to start of our lookup (first != bogus).
594 return editableEval( node ) && !( isBogus( node ) && node.equals( first ) );
595 } );
596 };
597 }
598
599 CKEDITOR.dom.range.prototype = {
600 /**
601 * Clones this range.
602 *
603 * @returns {CKEDITOR.dom.range}
604 */
605 clone: function() {
606 var clone = new CKEDITOR.dom.range( this.root );
607
608 clone._setStartContainer( this.startContainer );
609 clone.startOffset = this.startOffset;
610 clone._setEndContainer( this.endContainer );
611 clone.endOffset = this.endOffset;
612 clone.collapsed = this.collapsed;
613
614 return clone;
615 },
616
617 /**
618 * Makes the range collapsed by moving its start point (or end point if `toStart==true`)
619 * to the second end.
620 *
621 * @param {Boolean} toStart Collapse range "to start".
622 */
623 collapse: function( toStart ) {
624 if ( toStart ) {
625 this._setEndContainer( this.startContainer );
626 this.endOffset = this.startOffset;
627 } else {
628 this._setStartContainer( this.endContainer );
629 this.startOffset = this.endOffset;
630 }
631
632 this.collapsed = true;
633 },
634
635 /**
636 * Clones content nodes of the range and adds them to a document fragment, which is returned.
637 *
638 * @param {Boolean} [cloneId=true] Whether to preserve ID attributes in the clone.
639 * @returns {CKEDITOR.dom.documentFragment} Document fragment containing a clone of range's content.
640 */
641 cloneContents: function( cloneId ) {
642 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
643
644 cloneId = typeof cloneId == 'undefined' ? true : cloneId;
645
646 if ( !this.collapsed )
647 execContentsAction( this, 2, docFrag, false, cloneId );
648
649 return docFrag;
650 },
651
652 /**
653 * Deletes the content nodes of the range permanently from the DOM tree.
654 *
655 * @param {Boolean} [mergeThen] Merge any split elements result in DOM true due to partial selection.
656 */
657 deleteContents: function( mergeThen ) {
658 if ( this.collapsed )
659 return;
660
661 execContentsAction( this, 0, null, mergeThen );
662 },
663
664 /**
665 * The content nodes of the range are cloned and added to a document fragment,
666 * meanwhile they are removed permanently from the DOM tree.
667 *
668 * **Note:** Setting the `cloneId` parameter to `false` works for **partially** selected elements only.
669 * If an element with an ID attribute is **fully enclosed** in a range, it will keep the ID attribute
670 * regardless of the `cloneId` parameter value, because it is not cloned &mdash; it is moved to the returned
671 * document fragment.
672 *
673 * @param {Boolean} [mergeThen] Merge any split elements result in DOM true due to partial selection.
674 * @param {Boolean} [cloneId=true] Whether to preserve ID attributes in the extracted content.
675 * @returns {CKEDITOR.dom.documentFragment} Document fragment containing extracted content.
676 */
677 extractContents: function( mergeThen, cloneId ) {
678 var docFrag = new CKEDITOR.dom.documentFragment( this.document );
679
680 cloneId = typeof cloneId == 'undefined' ? true : cloneId;
681
682 if ( !this.collapsed )
683 execContentsAction( this, 1, docFrag, mergeThen, cloneId );
684
685 return docFrag;
686 },
687
688 /**
689 * Creates a bookmark object, which can be later used to restore the
690 * range by using the {@link #moveToBookmark} function.
691 *
692 * This is an "intrusive" way to create a bookmark. It includes `<span>` tags
693 * in the range boundaries. The advantage of it is that it is possible to
694 * handle DOM mutations when moving back to the bookmark.
695 *
696 * **Note:** The inclusion of nodes in the DOM is a design choice and
697 * should not be changed as there are other points in the code that may be
698 * using those nodes to perform operations.
699 *
700 * @param {Boolean} [serializable] Indicates that the bookmark nodes
701 * must contain IDs, which can be used to restore the range even
702 * when these nodes suffer mutations (like cloning or `innerHTML` change).
703 * @returns {Object} And object representing a bookmark.
704 * @returns {CKEDITOR.dom.node/String} return.startNode Node or element ID.
705 * @returns {CKEDITOR.dom.node/String} return.endNode Node or element ID.
706 * @returns {Boolean} return.serializable
707 * @returns {Boolean} return.collapsed
708 */
709 createBookmark: function( serializable ) {
710 var startNode, endNode;
711 var baseId;
712 var clone;
713 var collapsed = this.collapsed;
714
715 startNode = this.document.createElement( 'span' );
716 startNode.data( 'cke-bookmark', 1 );
717 startNode.setStyle( 'display', 'none' );
718
719 // For IE, it must have something inside, otherwise it may be
720 // removed during DOM operations.
721 startNode.setHtml( '&nbsp;' );
722
723 if ( serializable ) {
724 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
725 startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );
726 }
727
728 // If collapsed, the endNode will not be created.
729 if ( !collapsed ) {
730 endNode = startNode.clone();
731 endNode.setHtml( '&nbsp;' );
732
733 if ( serializable )
734 endNode.setAttribute( 'id', baseId + 'E' );
735
736 clone = this.clone();
737 clone.collapse();
738 clone.insertNode( endNode );
739 }
740
741 clone = this.clone();
742 clone.collapse( true );
743 clone.insertNode( startNode );
744
745 // Update the range position.
746 if ( endNode ) {
747 this.setStartAfter( startNode );
748 this.setEndBefore( endNode );
749 } else {
750 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
751 }
752
753 return {
754 startNode: serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,
755 endNode: serializable ? baseId + 'E' : endNode,
756 serializable: serializable,
757 collapsed: collapsed
758 };
759 },
760
761 /**
762 * Creates a "non intrusive" and "mutation sensible" bookmark. This
763 * kind of bookmark should be used only when the DOM is supposed to
764 * remain stable after its creation.
765 *
766 * @param {Boolean} [normalized] Indicates that the bookmark must
767 * be normalized. When normalized, the successive text nodes are
768 * considered a single node. To successfully load a normalized
769 * bookmark, the DOM tree must also be normalized before calling
770 * {@link #moveToBookmark}.
771 * @returns {Object} An object representing the bookmark.
772 * @returns {Array} return.start Start container's address (see {@link CKEDITOR.dom.node#getAddress}).
773 * @returns {Array} return.end Start container's address.
774 * @returns {Number} return.startOffset
775 * @returns {Number} return.endOffset
776 * @returns {Boolean} return.collapsed
777 * @returns {Boolean} return.normalized
778 * @returns {Boolean} return.is2 This is "bookmark2".
779 */
780 createBookmark2: ( function() {
781 var isNotText = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_TEXT, true );
782
783 // Returns true for limit anchored in element and placed between text nodes.
784 //
785 // v
786 // <p>[text node] [text node]</p> -> true
787 //
788 // v
789 // <p> [text node]</p> -> false
790 //
791 // v
792 // <p>[text node][text node]</p> -> false (limit is anchored in text node)
793 function betweenTextNodes( container, offset ) {
794 // Not anchored in element or limit is on the edge.
795 if ( container.type != CKEDITOR.NODE_ELEMENT || offset === 0 || offset == container.getChildCount() )
796 return 0;
797
798 return container.getChild( offset - 1 ).type == CKEDITOR.NODE_TEXT &&
799 container.getChild( offset ).type == CKEDITOR.NODE_TEXT;
800 }
801
802 // Sums lengths of all preceding text nodes.
803 function getLengthOfPrecedingTextNodes( node ) {
804 var sum = 0;
805
806 while ( ( node = node.getPrevious() ) && node.type == CKEDITOR.NODE_TEXT )
807 sum += node.getText().replace( CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE, '' ).length;
808
809 return sum;
810 }
811
812 function normalizeTextNodes( limit ) {
813 var container = limit.container,
814 offset = limit.offset;
815
816 // If limit is between text nodes move it to the end of preceding one,
817 // because they will be merged.
818 if ( betweenTextNodes( container, offset ) ) {
819 container = container.getChild( offset - 1 );
820 offset = container.getLength();
821 }
822
823 // Now, if limit is anchored in element and has at least one node before it,
824 // it may happen that some of them will be merged. Normalize the offset
825 // by setting it to normalized index of its preceding, safe node.
826 // (safe == one for which getIndex(true) does not return -1, so one which won't disappear).
827 if ( container.type == CKEDITOR.NODE_ELEMENT && offset > 0 ) {
828 offset = getPrecedingSafeNodeIndex( container, offset ) + 1;
829 }
830
831 // The last step - fix the offset inside text node by adding
832 // lengths of preceding text nodes which will be merged with container.
833 if ( container.type == CKEDITOR.NODE_TEXT ) {
834 var precedingLength = getLengthOfPrecedingTextNodes( container );
835
836 // Normal case - text node is not empty.
837 if ( container.getText() ) {
838 offset += precedingLength;
839
840 // Awful case - the text node is empty and thus will be totally lost.
841 // In this case we are trying to normalize the limit to the left:
842 // * either to the preceding text node,
843 // * or to the "gap" after the preceding element.
844 } else {
845 // Find the closest non-text sibling.
846 var precedingContainer = container.getPrevious( isNotText );
847
848 // If there are any characters on the left, that means that we can anchor
849 // there, because this text node will not be lost.
850 if ( precedingLength ) {
851 offset = precedingLength;
852
853 if ( precedingContainer ) {
854 // The text node is the first node after the closest non-text sibling.
855 container = precedingContainer.getNext();
856 } else {
857 // But if there was no non-text sibling, then the text node is the first child.
858 container = container.getParent().getFirst();
859 }
860
861 // If there are no characters on the left, then anchor after the previous non-text node.
862 // E.g. (see tests for a legend :D):
863 // <b>x</b>(foo)({}bar) -> <b>x</b>[](foo)(bar)
864 } else {
865 container = container.getParent();
866 offset = precedingContainer ? ( precedingContainer.getIndex( true ) + 1 ) : 0;
867 }
868 }
869 }
870
871 limit.container = container;
872 limit.offset = offset;
873 }
874
875 function normalizeFCSeq( limit, root ) {
876 var fcseq = root.getCustomData( 'cke-fillingChar' );
877
878 if ( !fcseq ) {
879 return;
880 }
881
882 var container = limit.container;
883
884 if ( fcseq.equals( container ) ) {
885 limit.offset -= CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE.length;
886
887 // == 0 handles case when limit was at the end of FCS.
888 // < 0 handles all cases where limit was somewhere in the middle or at the beginning.
889 // > 0 (the "else" case) means cases where there are some more characters in the FCS node (FCSabc^def).
890 if ( limit.offset <= 0 ) {
891 limit.offset = container.getIndex();
892 limit.container = container.getParent();
893 }
894 return;
895 }
896
897 // And here goes the funny part - all other cases are handled inside node.getAddress() and getIndex() thanks to
898 // node.getIndex() being aware of FCS (handling it as an empty node).
899 }
900
901 // Finds a normalized index of a safe node preceding this one.
902 // Safe == one that will not disappear, so one for which getIndex( true ) does not return -1.
903 // Return -1 if there's no safe preceding node.
904 function getPrecedingSafeNodeIndex( container, offset ) {
905 var index;
906
907 while ( offset-- ) {
908 index = container.getChild( offset ).getIndex( true );
909
910 if ( index >= 0 )
911 return index;
912 }
913
914 return -1;
915 }
916
917 return function( normalized ) {
918 var collapsed = this.collapsed,
919 bmStart = {
920 container: this.startContainer,
921 offset: this.startOffset
922 },
923 bmEnd = {
924 container: this.endContainer,
925 offset: this.endOffset
926 };
927
928 if ( normalized ) {
929 normalizeTextNodes( bmStart );
930 normalizeFCSeq( bmStart, this.root );
931
932 if ( !collapsed ) {
933 normalizeTextNodes( bmEnd );
934 normalizeFCSeq( bmEnd, this.root );
935 }
936 }
937
938 return {
939 start: bmStart.container.getAddress( normalized ),
940 end: collapsed ? null : bmEnd.container.getAddress( normalized ),
941 startOffset: bmStart.offset,
942 endOffset: bmEnd.offset,
943 normalized: normalized,
944 collapsed: collapsed,
945 is2: true // It's a createBookmark2 bookmark.
946 };
947 };
948 } )(),
949
950 /**
951 * Moves this range to the given bookmark. See {@link #createBookmark} and {@link #createBookmark2}.
952 *
953 * If serializable bookmark passed, then its `<span>` markers will be removed.
954 *
955 * @param {Object} bookmark
956 */
957 moveToBookmark: function( bookmark ) {
958 // Created with createBookmark2().
959 if ( bookmark.is2 ) {
960 // Get the start information.
961 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
962 startOffset = bookmark.startOffset;
963
964 // Get the end information.
965 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
966 endOffset = bookmark.endOffset;
967
968 // Set the start boundary.
969 this.setStart( startContainer, startOffset );
970
971 // Set the end boundary. If not available, collapse it.
972 if ( endContainer )
973 this.setEnd( endContainer, endOffset );
974 else
975 this.collapse( true );
976 }
977 // Created with createBookmark().
978 else {
979 var serializable = bookmark.serializable,
980 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
981 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
982
983 // Set the range start at the bookmark start node position.
984 this.setStartBefore( startNode );
985
986 // Remove it, because it may interfere in the setEndBefore call.
987 startNode.remove();
988
989 // Set the range end at the bookmark end node position, or simply
990 // collapse it if it is not available.
991 if ( endNode ) {
992 this.setEndBefore( endNode );
993 endNode.remove();
994 } else {
995 this.collapse( true );
996 }
997 }
998 },
999
1000 /**
1001 * Returns two nodes which are on the boundaries of this range.
1002 *
1003 * @returns {Object}
1004 * @returns {CKEDITOR.dom.node} return.startNode
1005 * @returns {CKEDITOR.dom.node} return.endNode
1006 * @todo precise desc/algorithm
1007 */
1008 getBoundaryNodes: function() {
1009 var startNode = this.startContainer,
1010 endNode = this.endContainer,
1011 startOffset = this.startOffset,
1012 endOffset = this.endOffset,
1013 childCount;
1014
1015 if ( startNode.type == CKEDITOR.NODE_ELEMENT ) {
1016 childCount = startNode.getChildCount();
1017 if ( childCount > startOffset ) {
1018 startNode = startNode.getChild( startOffset );
1019 } else if ( childCount < 1 ) {
1020 startNode = startNode.getPreviousSourceNode();
1021 }
1022 // startOffset > childCount but childCount is not 0
1023 else {
1024 // Try to take the node just after the current position.
1025 startNode = startNode.$;
1026 while ( startNode.lastChild )
1027 startNode = startNode.lastChild;
1028 startNode = new CKEDITOR.dom.node( startNode );
1029
1030 // Normally we should take the next node in DFS order. But it
1031 // is also possible that we've already reached the end of
1032 // document.
1033 startNode = startNode.getNextSourceNode() || startNode;
1034 }
1035 }
1036
1037 if ( endNode.type == CKEDITOR.NODE_ELEMENT ) {
1038 childCount = endNode.getChildCount();
1039 if ( childCount > endOffset ) {
1040 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
1041 } else if ( childCount < 1 ) {
1042 endNode = endNode.getPreviousSourceNode();
1043 }
1044 // endOffset > childCount but childCount is not 0.
1045 else {
1046 // Try to take the node just before the current position.
1047 endNode = endNode.$;
1048 while ( endNode.lastChild )
1049 endNode = endNode.lastChild;
1050 endNode = new CKEDITOR.dom.node( endNode );
1051 }
1052 }
1053
1054 // Sometimes the endNode will come right before startNode for collapsed
1055 // ranges. Fix it. (#3780)
1056 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
1057 startNode = endNode;
1058
1059 return { startNode: startNode, endNode: endNode };
1060 },
1061
1062 /**
1063 * Find the node which fully contains the range.
1064 *
1065 * @param {Boolean} [includeSelf=false]
1066 * @param {Boolean} [ignoreTextNode=false] Whether ignore {@link CKEDITOR#NODE_TEXT} type.
1067 * @returns {CKEDITOR.dom.element}
1068 */
1069 getCommonAncestor: function( includeSelf, ignoreTextNode ) {
1070 var start = this.startContainer,
1071 end = this.endContainer,
1072 ancestor;
1073
1074 if ( start.equals( end ) ) {
1075 if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 )
1076 ancestor = start.getChild( this.startOffset );
1077 else
1078 ancestor = start;
1079 } else {
1080 ancestor = start.getCommonAncestor( end );
1081 }
1082
1083 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
1084 },
1085
1086 /**
1087 * Transforms the {@link #startContainer} and {@link #endContainer} properties from text
1088 * nodes to element nodes, whenever possible. This is actually possible
1089 * if either of the boundary containers point to a text node, and its
1090 * offset is set to zero, or after the last char in the node.
1091 */
1092 optimize: function() {
1093 var container = this.startContainer;
1094 var offset = this.startOffset;
1095
1096 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1097 if ( !offset )
1098 this.setStartBefore( container );
1099 else if ( offset >= container.getLength() )
1100 this.setStartAfter( container );
1101 }
1102
1103 container = this.endContainer;
1104 offset = this.endOffset;
1105
1106 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1107 if ( !offset )
1108 this.setEndBefore( container );
1109 else if ( offset >= container.getLength() )
1110 this.setEndAfter( container );
1111 }
1112 },
1113
1114 /**
1115 * Move the range out of bookmark nodes if they'd been the container.
1116 */
1117 optimizeBookmark: function() {
1118 var startNode = this.startContainer,
1119 endNode = this.endContainer;
1120
1121 if ( startNode.is && startNode.is( 'span' ) && startNode.data( 'cke-bookmark' ) )
1122 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
1123 if ( endNode && endNode.is && endNode.is( 'span' ) && endNode.data( 'cke-bookmark' ) )
1124 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
1125 },
1126
1127 /**
1128 * @param {Boolean} [ignoreStart=false]
1129 * @param {Boolean} [ignoreEnd=false]
1130 * @todo precise desc/algorithm
1131 */
1132 trim: function( ignoreStart, ignoreEnd ) {
1133 var startContainer = this.startContainer,
1134 startOffset = this.startOffset,
1135 collapsed = this.collapsed;
1136 if ( ( !ignoreStart || collapsed ) && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1137 // If the offset is zero, we just insert the new node before
1138 // the start.
1139 if ( !startOffset ) {
1140 startOffset = startContainer.getIndex();
1141 startContainer = startContainer.getParent();
1142 }
1143 // If the offset is at the end, we'll insert it after the text
1144 // node.
1145 else if ( startOffset >= startContainer.getLength() ) {
1146 startOffset = startContainer.getIndex() + 1;
1147 startContainer = startContainer.getParent();
1148 }
1149 // In other case, we split the text node and insert the new
1150 // node at the split point.
1151 else {
1152 var nextText = startContainer.split( startOffset );
1153
1154 startOffset = startContainer.getIndex() + 1;
1155 startContainer = startContainer.getParent();
1156
1157 // Check all necessity of updating the end boundary.
1158 if ( this.startContainer.equals( this.endContainer ) )
1159 this.setEnd( nextText, this.endOffset - this.startOffset );
1160 else if ( startContainer.equals( this.endContainer ) )
1161 this.endOffset += 1;
1162 }
1163
1164 this.setStart( startContainer, startOffset );
1165
1166 if ( collapsed ) {
1167 this.collapse( true );
1168 return;
1169 }
1170 }
1171
1172 var endContainer = this.endContainer;
1173 var endOffset = this.endOffset;
1174
1175 if ( !( ignoreEnd || collapsed ) && endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1176 // If the offset is zero, we just insert the new node before
1177 // the start.
1178 if ( !endOffset ) {
1179 endOffset = endContainer.getIndex();
1180 endContainer = endContainer.getParent();
1181 }
1182 // If the offset is at the end, we'll insert it after the text
1183 // node.
1184 else if ( endOffset >= endContainer.getLength() ) {
1185 endOffset = endContainer.getIndex() + 1;
1186 endContainer = endContainer.getParent();
1187 }
1188 // In other case, we split the text node and insert the new
1189 // node at the split point.
1190 else {
1191 endContainer.split( endOffset );
1192
1193 endOffset = endContainer.getIndex() + 1;
1194 endContainer = endContainer.getParent();
1195 }
1196
1197 this.setEnd( endContainer, endOffset );
1198 }
1199 },
1200
1201 /**
1202 * Expands the range so that partial units are completely contained.
1203 *
1204 * @param {Number} unit The unit type to expand with. Use one of following values: {@link CKEDITOR#ENLARGE_BLOCK_CONTENTS},
1205 * {@link CKEDITOR#ENLARGE_ELEMENT}, {@link CKEDITOR#ENLARGE_INLINE}, {@link CKEDITOR#ENLARGE_LIST_ITEM_CONTENTS}.
1206 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
1207 */
1208 enlarge: function( unit, excludeBrs ) {
1209 var leadingWhitespaceRegex = new RegExp( /[^\s\ufeff]/ );
1210
1211 switch ( unit ) {
1212 case CKEDITOR.ENLARGE_INLINE:
1213 var enlargeInlineOnly = 1;
1214
1215 /* falls through */
1216 case CKEDITOR.ENLARGE_ELEMENT:
1217
1218 if ( this.collapsed )
1219 return;
1220
1221 // Get the common ancestor.
1222 var commonAncestor = this.getCommonAncestor();
1223
1224 var boundary = this.root;
1225
1226 // For each boundary
1227 // a. Depending on its position, find out the first node to be checked (a sibling) or,
1228 // if not available, to be enlarge.
1229 // b. Go ahead checking siblings and enlarging the boundary as much as possible until the
1230 // common ancestor is not reached. After reaching the common ancestor, just save the
1231 // enlargeable node to be used later.
1232
1233 var startTop, endTop;
1234
1235 var enlargeable, sibling, commonReached;
1236
1237 // Indicates that the node can be added only if whitespace
1238 // is available before it.
1239 var needsWhiteSpace = false;
1240 var isWhiteSpace;
1241 var siblingText;
1242
1243 // Process the start boundary.
1244
1245 var container = this.startContainer;
1246 var offset = this.startOffset;
1247
1248 if ( container.type == CKEDITOR.NODE_TEXT ) {
1249 if ( offset ) {
1250 // Check if there is any non-space text before the
1251 // offset. Otherwise, container is null.
1252 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
1253
1254 // If we found only whitespace in the node, it
1255 // means that we'll need more whitespace to be able
1256 // to expand. For example, <i> can be expanded in
1257 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1258 needsWhiteSpace = !!container;
1259 }
1260
1261 if ( container ) {
1262 if ( !( sibling = container.getPrevious() ) )
1263 enlargeable = container.getParent();
1264 }
1265 } else {
1266 // If we have offset, get the node preceeding it as the
1267 // first sibling to be checked.
1268 if ( offset )
1269 sibling = container.getChild( offset - 1 ) || container.getLast();
1270
1271 // If there is no sibling, mark the container to be
1272 // enlarged.
1273 if ( !sibling )
1274 enlargeable = container;
1275 }
1276
1277 // Ensures that enlargeable can be indeed enlarged, if not it will be nulled.
1278 enlargeable = getValidEnlargeable( enlargeable );
1279
1280 while ( enlargeable || sibling ) {
1281 if ( enlargeable && !sibling ) {
1282 // If we reached the common ancestor, mark the flag
1283 // for it.
1284 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1285 commonReached = true;
1286
1287 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1288 break;
1289
1290 // If we don't need space or this element breaks
1291 // the line, then enlarge it.
1292 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1293 needsWhiteSpace = false;
1294
1295 // If the common ancestor has been reached,
1296 // we'll not enlarge it immediately, but just
1297 // mark it to be enlarged later if the end
1298 // boundary also enlarges it.
1299 if ( commonReached )
1300 startTop = enlargeable;
1301 else
1302 this.setStartBefore( enlargeable );
1303 }
1304
1305 sibling = enlargeable.getPrevious();
1306 }
1307
1308 // Check all sibling nodes preceeding the enlargeable
1309 // node. The node wil lbe enlarged only if none of them
1310 // blocks it.
1311 while ( sibling ) {
1312 // This flag indicates that this node has
1313 // whitespaces at the end.
1314 isWhiteSpace = false;
1315
1316 if ( sibling.type == CKEDITOR.NODE_COMMENT ) {
1317 sibling = sibling.getPrevious();
1318 continue;
1319 } else if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1320 siblingText = sibling.getText();
1321
1322 if ( leadingWhitespaceRegex.test( siblingText ) )
1323 sibling = null;
1324
1325 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
1326 } else {
1327 // #12221 (Chrome) plus #11111 (Safari).
1328 var offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
1329
1330 // If this is a visible element.
1331 // We need to check for the bookmark attribute because IE insists on
1332 // rendering the display:none nodes we use for bookmarks. (#3363)
1333 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1334 if ( ( sibling.$.offsetWidth > offsetWidth0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1335 // We'll accept it only if we need
1336 // whitespace, and this is an inline
1337 // element with whitespace only.
1338 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1339 // It must contains spaces and inline elements only.
1340
1341 siblingText = sibling.getText();
1342
1343 if ( leadingWhitespaceRegex.test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
1344 sibling = null;
1345 else {
1346 var allChildren = sibling.$.getElementsByTagName( '*' );
1347 for ( var i = 0, child; child = allChildren[ i++ ]; ) {
1348 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1349 sibling = null;
1350 break;
1351 }
1352 }
1353 }
1354
1355 if ( sibling )
1356 isWhiteSpace = !!siblingText.length;
1357 } else {
1358 sibling = null;
1359 }
1360 }
1361 }
1362
1363 // A node with whitespaces has been found.
1364 if ( isWhiteSpace ) {
1365 // Enlarge the last enlargeable node, if we
1366 // were waiting for spaces.
1367 if ( needsWhiteSpace ) {
1368 if ( commonReached )
1369 startTop = enlargeable;
1370 else if ( enlargeable )
1371 this.setStartBefore( enlargeable );
1372 } else {
1373 needsWhiteSpace = true;
1374 }
1375 }
1376
1377 if ( sibling ) {
1378 var next = sibling.getPrevious();
1379
1380 if ( !enlargeable && !next ) {
1381 // Set the sibling as enlargeable, so it's
1382 // parent will be get later outside this while.
1383 enlargeable = sibling;
1384 sibling = null;
1385 break;
1386 }
1387
1388 sibling = next;
1389 } else {
1390 // If sibling has been set to null, then we
1391 // need to stop enlarging.
1392 enlargeable = null;
1393 }
1394 }
1395
1396 if ( enlargeable )
1397 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1398 }
1399
1400 // Process the end boundary. This is basically the same
1401 // code used for the start boundary, with small changes to
1402 // make it work in the oposite side (to the right). This
1403 // makes it difficult to reuse the code here. So, fixes to
1404 // the above code are likely to be replicated here.
1405
1406 container = this.endContainer;
1407 offset = this.endOffset;
1408
1409 // Reset the common variables.
1410 enlargeable = sibling = null;
1411 commonReached = needsWhiteSpace = false;
1412
1413 // Function check if there are only whitespaces from the given starting point
1414 // (startContainer and startOffset) till the end on block.
1415 // Examples ("[" is the start point):
1416 // - <p>foo[ </p> - will return true,
1417 // - <p><b>foo[ </b> </p> - will return true,
1418 // - <p>foo[ bar</p> - will return false,
1419 // - <p><b>foo[ </b>bar</p> - will return false,
1420 // - <p>foo[ <b></b></p> - will return false.
1421 function onlyWhiteSpaces( startContainer, startOffset ) {
1422 // We need to enlarge range if there is white space at the end of the block,
1423 // because it is not displayed in WYSIWYG mode and user can not select it. So
1424 // "<p>foo[bar] </p>" should be changed to "<p>foo[bar ]</p>". On the other hand
1425 // we should do nothing if we are not at the end of the block, so this should not
1426 // be changed: "<p><i>[foo] </i>bar</p>".
1427 var walkerRange = new CKEDITOR.dom.range( boundary );
1428 walkerRange.setStart( startContainer, startOffset );
1429 // The guard will find the end of range so I put boundary here.
1430 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1431
1432 var walker = new CKEDITOR.dom.walker( walkerRange ),
1433 node;
1434
1435 walker.guard = function( node ) {
1436 // Stop if you exit block.
1437 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() );
1438 };
1439
1440 while ( ( node = walker.next() ) ) {
1441 if ( node.type != CKEDITOR.NODE_TEXT ) {
1442 // Stop if you enter to any node (walker.next() will return node only
1443 // it goes out, not if it is go into node).
1444 return false;
1445 } else {
1446 // Trim the first node to startOffset.
1447 if ( node != startContainer )
1448 siblingText = node.getText();
1449 else
1450 siblingText = node.substring( startOffset );
1451
1452 // Check if it is white space.
1453 if ( leadingWhitespaceRegex.test( siblingText ) )
1454 return false;
1455 }
1456 }
1457
1458 return true;
1459 }
1460
1461 if ( container.type == CKEDITOR.NODE_TEXT ) {
1462 // Check if there is only white space after the offset.
1463 if ( CKEDITOR.tools.trim( container.substring( offset ) ).length ) {
1464 // If we found only whitespace in the node, it
1465 // means that we'll need more whitespace to be able
1466 // to expand. For example, <i> can be expanded in
1467 // "A <i> [B]</i>", but not in "A<i> [B]</i>".
1468 needsWhiteSpace = true;
1469 } else {
1470 needsWhiteSpace = !container.getLength();
1471
1472 if ( offset == container.getLength() ) {
1473 // If we are at the end of container and this is the last text node,
1474 // we should enlarge end to the parent.
1475 if ( !( sibling = container.getNext() ) )
1476 enlargeable = container.getParent();
1477 } else {
1478 // If we are in the middle on text node and there are only whitespaces
1479 // till the end of block, we should enlarge element.
1480 if ( onlyWhiteSpaces( container, offset ) )
1481 enlargeable = container.getParent();
1482 }
1483 }
1484 } else {
1485 // Get the node right after the boudary to be checked
1486 // first.
1487 sibling = container.getChild( offset );
1488
1489 if ( !sibling )
1490 enlargeable = container;
1491 }
1492
1493 while ( enlargeable || sibling ) {
1494 if ( enlargeable && !sibling ) {
1495 if ( !commonReached && enlargeable.equals( commonAncestor ) )
1496 commonReached = true;
1497
1498 if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
1499 break;
1500
1501 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
1502 needsWhiteSpace = false;
1503
1504 if ( commonReached )
1505 endTop = enlargeable;
1506 else if ( enlargeable )
1507 this.setEndAfter( enlargeable );
1508 }
1509
1510 sibling = enlargeable.getNext();
1511 }
1512
1513 while ( sibling ) {
1514 isWhiteSpace = false;
1515
1516 if ( sibling.type == CKEDITOR.NODE_TEXT ) {
1517 siblingText = sibling.getText();
1518
1519 // Check if there are not whitespace characters till the end of editable.
1520 // If so stop expanding.
1521 if ( !onlyWhiteSpaces( sibling, 0 ) )
1522 sibling = null;
1523
1524 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
1525 } else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) {
1526 // If this is a visible element.
1527 // We need to check for the bookmark attribute because IE insists on
1528 // rendering the display:none nodes we use for bookmarks. (#3363)
1529 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
1530 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1531 // We'll accept it only if we need
1532 // whitespace, and this is an inline
1533 // element with whitespace only.
1534 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
1535 // It must contains spaces and inline elements only.
1536
1537 siblingText = sibling.getText();
1538
1539 if ( leadingWhitespaceRegex.test( siblingText ) )
1540 sibling = null;
1541 else {
1542 allChildren = sibling.$.getElementsByTagName( '*' );
1543 for ( i = 0; child = allChildren[ i++ ]; ) {
1544 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
1545 sibling = null;
1546 break;
1547 }
1548 }
1549 }
1550
1551 if ( sibling )
1552 isWhiteSpace = !!siblingText.length;
1553 } else {
1554 sibling = null;
1555 }
1556 }
1557 } else {
1558 isWhiteSpace = 1;
1559 }
1560
1561 if ( isWhiteSpace ) {
1562 if ( needsWhiteSpace ) {
1563 if ( commonReached )
1564 endTop = enlargeable;
1565 else
1566 this.setEndAfter( enlargeable );
1567 }
1568 }
1569
1570 if ( sibling ) {
1571 next = sibling.getNext();
1572
1573 if ( !enlargeable && !next ) {
1574 enlargeable = sibling;
1575 sibling = null;
1576 break;
1577 }
1578
1579 sibling = next;
1580 } else {
1581 // If sibling has been set to null, then we
1582 // need to stop enlarging.
1583 enlargeable = null;
1584 }
1585 }
1586
1587 if ( enlargeable )
1588 enlargeable = getValidEnlargeable( enlargeable.getParent() );
1589 }
1590
1591 // If the common ancestor can be enlarged by both boundaries, then include it also.
1592 if ( startTop && endTop ) {
1593 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
1594
1595 this.setStartBefore( commonAncestor );
1596 this.setEndAfter( commonAncestor );
1597 }
1598 break;
1599
1600 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
1601 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
1602
1603 // Enlarging the start boundary.
1604 var walkerRange = new CKEDITOR.dom.range( this.root );
1605
1606 boundary = this.root;
1607
1608 walkerRange.setStartAt( boundary, CKEDITOR.POSITION_AFTER_START );
1609 walkerRange.setEnd( this.startContainer, this.startOffset );
1610
1611 var walker = new CKEDITOR.dom.walker( walkerRange ),
1612 blockBoundary, // The node on which the enlarging should stop.
1613 tailBr, // In case BR as block boundary.
1614 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary( ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br: 1 } : null ),
1615 inNonEditable = null,
1616 // Record the encountered 'blockBoundary' for later use.
1617 boundaryGuard = function( node ) {
1618 // We should not check contents of non-editable elements. It may happen
1619 // that inline widget has display:table child which should not block range#enlarge.
1620 // When encoutered non-editable element...
1621 if ( node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'false' ) {
1622 if ( inNonEditable ) {
1623 // ... in which we already were, reset it (because we're leaving it) and return.
1624 if ( inNonEditable.equals( node ) ) {
1625 inNonEditable = null;
1626 return;
1627 }
1628 // ... which we're entering, remember it but check it (no return).
1629 } else {
1630 inNonEditable = node;
1631 }
1632 // When we are in non-editable element, do not check if current node is a block boundary.
1633 } else if ( inNonEditable ) {
1634 return;
1635 }
1636
1637 var retval = notBlockBoundary( node );
1638 if ( !retval )
1639 blockBoundary = node;
1640 return retval;
1641 },
1642 // Record the encounted 'tailBr' for later use.
1643 tailBrGuard = function( node ) {
1644 var retval = boundaryGuard( node );
1645 if ( !retval && node.is && node.is( 'br' ) )
1646 tailBr = node;
1647 return retval;
1648 };
1649
1650 walker.guard = boundaryGuard;
1651
1652 enlargeable = walker.lastBackward();
1653
1654 // It's the body which stop the enlarging if no block boundary found.
1655 blockBoundary = blockBoundary || boundary;
1656
1657 // Start the range either after the end of found block (<p>...</p>[text)
1658 // or at the start of block (<p>[text...), by comparing the document position
1659 // with 'enlargeable' node.
1660 this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() ||
1661 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END );
1662
1663 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
1664 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) {
1665 var theRange = this.clone();
1666 walker = new CKEDITOR.dom.walker( theRange );
1667
1668 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
1669 bookmark = CKEDITOR.dom.walker.bookmark();
1670
1671 walker.evaluator = function( node ) {
1672 return !whitespaces( node ) && !bookmark( node );
1673 };
1674 var previous = walker.previous();
1675 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
1676 return;
1677 }
1678
1679 // Enlarging the end boundary.
1680 // Set up new range and reset all flags (blockBoundary, inNonEditable, tailBr).
1681
1682 walkerRange = this.clone();
1683 walkerRange.collapse();
1684 walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
1685 walker = new CKEDITOR.dom.walker( walkerRange );
1686
1687 // tailBrGuard only used for on range end.
1688 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? tailBrGuard : boundaryGuard;
1689 blockBoundary = inNonEditable = tailBr = null;
1690
1691 // End the range right before the block boundary node.
1692 enlargeable = walker.lastForward();
1693
1694 // It's the body which stop the enlarging if no block boundary found.
1695 blockBoundary = blockBoundary || boundary;
1696
1697 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>)
1698 // by comparing the document position with 'enlargeable' node.
1699 this.setEndAt( blockBoundary, ( !enlargeable && this.checkEndOfBlock() ||
1700 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_BEFORE_START );
1701 // We must include the <br> at the end of range if there's
1702 // one and we're expanding list item contents
1703 if ( tailBr ) {
1704 this.setEndAfter( tailBr );
1705 }
1706 }
1707
1708 // Ensures that returned element can be enlarged by selection, null otherwise.
1709 // @param {CKEDITOR.dom.element} enlargeable
1710 // @returns {CKEDITOR.dom.element/null}
1711 function getValidEnlargeable( enlargeable ) {
1712 return enlargeable && enlargeable.type == CKEDITOR.NODE_ELEMENT && enlargeable.hasAttribute( 'contenteditable' ) ?
1713 null : enlargeable;
1714 }
1715 },
1716
1717 /**
1718 * Descrease the range to make sure that boundaries
1719 * always anchor beside text nodes or innermost element.
1720 *
1721 * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}).
1722 *
1723 * * {@link CKEDITOR#SHRINK_ELEMENT} - Shrink the range boundaries to the edge of the innermost element.
1724 * * {@link CKEDITOR#SHRINK_TEXT} - Shrink the range boudaries to anchor by the side of enclosed text
1725 * node, range remains if there's no text nodes on boundaries at all.
1726 *
1727 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
1728 */
1729 shrink: function( mode, selectContents, shrinkOnBlockBoundary ) {
1730 // Unable to shrink a collapsed range.
1731 if ( !this.collapsed ) {
1732 mode = mode || CKEDITOR.SHRINK_TEXT;
1733
1734 var walkerRange = this.clone();
1735
1736 var startContainer = this.startContainer,
1737 endContainer = this.endContainer,
1738 startOffset = this.startOffset,
1739 endOffset = this.endOffset;
1740
1741 // Whether the start/end boundary is moveable.
1742 var moveStart = 1,
1743 moveEnd = 1;
1744
1745 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
1746 if ( !startOffset )
1747 walkerRange.setStartBefore( startContainer );
1748 else if ( startOffset >= startContainer.getLength() )
1749 walkerRange.setStartAfter( startContainer );
1750 else {
1751 // Enlarge the range properly to avoid walker making
1752 // DOM changes caused by triming the text nodes later.
1753 walkerRange.setStartBefore( startContainer );
1754 moveStart = 0;
1755 }
1756 }
1757
1758 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
1759 if ( !endOffset )
1760 walkerRange.setEndBefore( endContainer );
1761 else if ( endOffset >= endContainer.getLength() )
1762 walkerRange.setEndAfter( endContainer );
1763 else {
1764 walkerRange.setEndAfter( endContainer );
1765 moveEnd = 0;
1766 }
1767 }
1768
1769 var walker = new CKEDITOR.dom.walker( walkerRange ),
1770 isBookmark = CKEDITOR.dom.walker.bookmark();
1771
1772 walker.evaluator = function( node ) {
1773 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
1774 };
1775
1776 var currentElement;
1777 walker.guard = function( node, movingOut ) {
1778 if ( isBookmark( node ) )
1779 return true;
1780
1781 // Stop when we're shrink in element mode while encountering a text node.
1782 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
1783 return false;
1784
1785 // Stop when we've already walked "through" an element.
1786 if ( movingOut && node.equals( currentElement ) )
1787 return false;
1788
1789 if ( shrinkOnBlockBoundary === false && node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() )
1790 return false;
1791
1792 // Stop shrinking when encountering an editable border.
1793 if ( node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'contenteditable' ) )
1794 return false;
1795
1796 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
1797 currentElement = node;
1798
1799 return true;
1800 };
1801
1802 if ( moveStart ) {
1803 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next' ]();
1804 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
1805 }
1806
1807 if ( moveEnd ) {
1808 walker.reset();
1809 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous' ]();
1810 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
1811 }
1812
1813 return !!( moveStart || moveEnd );
1814 }
1815 },
1816
1817 /**
1818 * Inserts a node at the start of the range. The range will be expanded
1819 * the contain the node.
1820 *
1821 * @param {CKEDITOR.dom.node} node
1822 */
1823 insertNode: function( node ) {
1824 this.optimizeBookmark();
1825 this.trim( false, true );
1826
1827 var startContainer = this.startContainer;
1828 var startOffset = this.startOffset;
1829
1830 var nextNode = startContainer.getChild( startOffset );
1831
1832 if ( nextNode )
1833 node.insertBefore( nextNode );
1834 else
1835 startContainer.append( node );
1836
1837 // Check if we need to update the end boundary.
1838 if ( node.getParent() && node.getParent().equals( this.endContainer ) )
1839 this.endOffset++;
1840
1841 // Expand the range to embrace the new node.
1842 this.setStartBefore( node );
1843 },
1844
1845 /**
1846 * Moves the range to given position according to specified node.
1847 *
1848 * // HTML: <p>Foo <b>bar</b></p>
1849 * range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START );
1850 * // Range will be moved to: <p>Foo ^<b>bar</b></p>
1851 *
1852 * See also {@link #setStartAt} and {@link #setEndAt}.
1853 *
1854 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
1855 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1856 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1857 * {@link CKEDITOR#POSITION_AFTER_END}.
1858 */
1859 moveToPosition: function( node, position ) {
1860 this.setStartAt( node, position );
1861 this.collapse( true );
1862 },
1863
1864 /**
1865 * Moves the range to the exact position of the specified range.
1866 *
1867 * @param {CKEDITOR.dom.range} range
1868 */
1869 moveToRange: function( range ) {
1870 this.setStart( range.startContainer, range.startOffset );
1871 this.setEnd( range.endContainer, range.endOffset );
1872 },
1873
1874 /**
1875 * Select nodes content. Range will start and end inside this node.
1876 *
1877 * @param {CKEDITOR.dom.node} node
1878 */
1879 selectNodeContents: function( node ) {
1880 this.setStart( node, 0 );
1881 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
1882 },
1883
1884 /**
1885 * Sets the start position of a range.
1886 *
1887 * @param {CKEDITOR.dom.node} startNode The node to start the range.
1888 * @param {Number} startOffset An integer greater than or equal to zero
1889 * representing the offset for the start of the range from the start
1890 * of `startNode`.
1891 */
1892 setStart: function( startNode, startOffset ) {
1893 // W3C requires a check for the new position. If it is after the end
1894 // boundary, the range should be collapsed to the new start. It seams
1895 // we will not need this check for our use of this class so we can
1896 // ignore it for now.
1897
1898 // Fixing invalid range start inside dtd empty elements.
1899 if ( startNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ startNode.getName() ] )
1900 startOffset = startNode.getIndex(), startNode = startNode.getParent();
1901
1902 this._setStartContainer( startNode );
1903 this.startOffset = startOffset;
1904
1905 if ( !this.endContainer ) {
1906 this._setEndContainer( startNode );
1907 this.endOffset = startOffset;
1908 }
1909
1910 updateCollapsed( this );
1911 },
1912
1913 /**
1914 * Sets the end position of a Range.
1915 *
1916 * @param {CKEDITOR.dom.node} endNode The node to end the range.
1917 * @param {Number} endOffset An integer greater than or equal to zero
1918 * representing the offset for the end of the range from the start
1919 * of `endNode`.
1920 */
1921 setEnd: function( endNode, endOffset ) {
1922 // W3C requires a check for the new position. If it is before the start
1923 // boundary, the range should be collapsed to the new end. It seams we
1924 // will not need this check for our use of this class so we can ignore
1925 // it for now.
1926
1927 // Fixing invalid range end inside dtd empty elements.
1928 if ( endNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ endNode.getName() ] )
1929 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
1930
1931 this._setEndContainer( endNode );
1932 this.endOffset = endOffset;
1933
1934 if ( !this.startContainer ) {
1935 this._setStartContainer( endNode );
1936 this.startOffset = endOffset;
1937 }
1938
1939 updateCollapsed( this );
1940 },
1941
1942 /**
1943 * Sets start of this range after the specified node.
1944 *
1945 * // Range: <p>foo<b>bar</b>^</p>
1946 * range.setStartAfter( textFoo );
1947 * // The range will be changed to:
1948 * // <p>foo[<b>bar</b>]</p>
1949 *
1950 * @param {CKEDITOR.dom.node} node
1951 */
1952 setStartAfter: function( node ) {
1953 this.setStart( node.getParent(), node.getIndex() + 1 );
1954 },
1955
1956 /**
1957 * Sets start of this range after the specified node.
1958 *
1959 * // Range: <p>foo<b>bar</b>^</p>
1960 * range.setStartBefore( elB );
1961 * // The range will be changed to:
1962 * // <p>foo[<b>bar</b>]</p>
1963 *
1964 * @param {CKEDITOR.dom.node} node
1965 */
1966 setStartBefore: function( node ) {
1967 this.setStart( node.getParent(), node.getIndex() );
1968 },
1969
1970 /**
1971 * Sets end of this range after the specified node.
1972 *
1973 * // Range: <p>foo^<b>bar</b></p>
1974 * range.setEndAfter( elB );
1975 * // The range will be changed to:
1976 * // <p>foo[<b>bar</b>]</p>
1977 *
1978 * @param {CKEDITOR.dom.node} node
1979 */
1980 setEndAfter: function( node ) {
1981 this.setEnd( node.getParent(), node.getIndex() + 1 );
1982 },
1983
1984 /**
1985 * Sets end of this range before the specified node.
1986 *
1987 * // Range: <p>^foo<b>bar</b></p>
1988 * range.setStartAfter( textBar );
1989 * // The range will be changed to:
1990 * // <p>[foo<b>]bar</b></p>
1991 *
1992 * @param {CKEDITOR.dom.node} node
1993 */
1994 setEndBefore: function( node ) {
1995 this.setEnd( node.getParent(), node.getIndex() );
1996 },
1997
1998 /**
1999 * Moves the start of this range to given position according to specified node.
2000 *
2001 * // HTML: <p>Foo <b>bar</b>^</p>
2002 * range.setStartAt( elB, CKEDITOR.POSITION_AFTER_START );
2003 * // The range will be changed to:
2004 * // <p>Foo <b>[bar</b>]</p>
2005 *
2006 * See also {@link #setEndAt} and {@link #moveToPosition}.
2007 *
2008 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2009 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2010 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2011 * {@link CKEDITOR#POSITION_AFTER_END}.
2012 */
2013 setStartAt: function( node, position ) {
2014 switch ( position ) {
2015 case CKEDITOR.POSITION_AFTER_START:
2016 this.setStart( node, 0 );
2017 break;
2018
2019 case CKEDITOR.POSITION_BEFORE_END:
2020 if ( node.type == CKEDITOR.NODE_TEXT )
2021 this.setStart( node, node.getLength() );
2022 else
2023 this.setStart( node, node.getChildCount() );
2024 break;
2025
2026 case CKEDITOR.POSITION_BEFORE_START:
2027 this.setStartBefore( node );
2028 break;
2029
2030 case CKEDITOR.POSITION_AFTER_END:
2031 this.setStartAfter( node );
2032 }
2033
2034 updateCollapsed( this );
2035 },
2036
2037 /**
2038 * Moves the end of this range to given position according to specified node.
2039 *
2040 * // HTML: <p>^Foo <b>bar</b></p>
2041 * range.setEndAt( textBar, CKEDITOR.POSITION_BEFORE_START );
2042 * // The range will be changed to:
2043 * // <p>[Foo <b>]bar</b></p>
2044 *
2045 * See also {@link #setStartAt} and {@link #moveToPosition}.
2046 *
2047 * @param {CKEDITOR.dom.node} node The node according to which position will be set.
2048 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
2049 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
2050 * {@link CKEDITOR#POSITION_AFTER_END}.
2051 */
2052 setEndAt: function( node, position ) {
2053 switch ( position ) {
2054 case CKEDITOR.POSITION_AFTER_START:
2055 this.setEnd( node, 0 );
2056 break;
2057
2058 case CKEDITOR.POSITION_BEFORE_END:
2059 if ( node.type == CKEDITOR.NODE_TEXT )
2060 this.setEnd( node, node.getLength() );
2061 else
2062 this.setEnd( node, node.getChildCount() );
2063 break;
2064
2065 case CKEDITOR.POSITION_BEFORE_START:
2066 this.setEndBefore( node );
2067 break;
2068
2069 case CKEDITOR.POSITION_AFTER_END:
2070 this.setEndAfter( node );
2071 }
2072
2073 updateCollapsed( this );
2074 },
2075
2076 /**
2077 * Wraps inline content found around the range's start or end boundary
2078 * with a block element.
2079 *
2080 * // Assuming the following range:
2081 * // <h1>foo</h1>ba^r<br />bom<p>foo</p>
2082 * // The result of executing:
2083 * range.fixBlock( true, 'p' );
2084 * // will be:
2085 * // <h1>foo</h1><p>ba^r<br />bom</p><p>foo</p>
2086 *
2087 * Non-collapsed range:
2088 *
2089 * // Assuming the following range:
2090 * // ba[r<p>foo</p>bo]m
2091 * // The result of executing:
2092 * range.fixBlock( false, 'p' );
2093 * // will be:
2094 * // ba[r<p>foo</p><p>bo]m</p>
2095 *
2096 * @param {Boolean} isStart Whether the start or end boundary of a range should be checked.
2097 * @param {String} blockTag The name of a block element in which content will be wrapped.
2098 * For example: `'p'`.
2099 * @returns {CKEDITOR.dom.element} Created block wrapper.
2100 */
2101 fixBlock: function( isStart, blockTag ) {
2102 var bookmark = this.createBookmark(),
2103 fixedBlock = this.document.createElement( blockTag );
2104
2105 this.collapse( isStart );
2106
2107 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
2108
2109 this.extractContents().appendTo( fixedBlock );
2110 fixedBlock.trim();
2111
2112 this.insertNode( fixedBlock );
2113
2114 // Bogus <br> could already exist in the range's container before fixBlock() was called. In such case it was
2115 // extracted and appended to the fixBlock. However, we are not sure that it's at the end of
2116 // the fixedBlock, because of FF's terrible bug. When creating a bookmark in an empty editable
2117 // FF moves the bogus <br> before that bookmark (<editable><br /><bm />[]</editable>).
2118 // So even if the initial range was placed before the bogus <br>, after creating the bookmark it
2119 // is placed before the bookmark.
2120 // Fortunately, getBogus() is able to skip the bookmark so it finds the bogus <br> in this case.
2121 // We remove incorrectly placed one and add a brand new one. (#13001)
2122 var bogus = fixedBlock.getBogus();
2123 if ( bogus ) {
2124 bogus.remove();
2125 }
2126 fixedBlock.appendBogus();
2127
2128 this.moveToBookmark( bookmark );
2129
2130 return fixedBlock;
2131 },
2132
2133 /**
2134 * @todo
2135 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result blocks.
2136 */
2137 splitBlock: function( blockTag, cloneId ) {
2138 var startPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ),
2139 endPath = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2140
2141 var startBlockLimit = startPath.blockLimit,
2142 endBlockLimit = endPath.blockLimit;
2143
2144 var startBlock = startPath.block,
2145 endBlock = endPath.block;
2146
2147 var elementPath = null;
2148 // Do nothing if the boundaries are in different block limits.
2149 if ( !startBlockLimit.equals( endBlockLimit ) )
2150 return null;
2151
2152 // Get or fix current blocks.
2153 if ( blockTag != 'br' ) {
2154 if ( !startBlock ) {
2155 startBlock = this.fixBlock( true, blockTag );
2156 endBlock = new CKEDITOR.dom.elementPath( this.endContainer, this.root ).block;
2157 }
2158
2159 if ( !endBlock )
2160 endBlock = this.fixBlock( false, blockTag );
2161 }
2162
2163 // Get the range position.
2164 var isStartOfBlock = startBlock && this.checkStartOfBlock(),
2165 isEndOfBlock = endBlock && this.checkEndOfBlock();
2166
2167 // Delete the current contents.
2168 // TODO: Why is 2.x doing CheckIsEmpty()?
2169 this.deleteContents();
2170
2171 if ( startBlock && startBlock.equals( endBlock ) ) {
2172 if ( isEndOfBlock ) {
2173 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2174 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
2175 endBlock = null;
2176 } else if ( isStartOfBlock ) {
2177 elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2178 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
2179 startBlock = null;
2180 } else {
2181 endBlock = this.splitElement( startBlock, cloneId || false );
2182
2183 // In Gecko, the last child node must be a bogus <br>.
2184 // Note: bogus <br> added under <ul> or <ol> would cause
2185 // lists to be incorrectly rendered.
2186 if ( !startBlock.is( 'ul', 'ol' ) )
2187 startBlock.appendBogus();
2188 }
2189 }
2190
2191 return {
2192 previousBlock: startBlock,
2193 nextBlock: endBlock,
2194 wasStartOfBlock: isStartOfBlock,
2195 wasEndOfBlock: isEndOfBlock,
2196 elementPath: elementPath
2197 };
2198 },
2199
2200 /**
2201 * Branch the specified element from the collapsed range position and
2202 * place the caret between the two result branches.
2203 *
2204 * **Note:** The range must be collapsed and been enclosed by this element.
2205 *
2206 * @param {CKEDITOR.dom.element} element
2207 * @param {Boolean} [cloneId=false] Whether to preserve ID attributes in the result elements.
2208 * @returns {CKEDITOR.dom.element} Root element of the new branch after the split.
2209 */
2210 splitElement: function( toSplit, cloneId ) {
2211 if ( !this.collapsed )
2212 return null;
2213
2214 // Extract the contents of the block from the selection point to the end
2215 // of its contents.
2216 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
2217 var documentFragment = this.extractContents( false, cloneId || false );
2218
2219 // Duplicate the element after it.
2220 var clone = toSplit.clone( false, cloneId || false );
2221
2222 // Place the extracted contents into the duplicated element.
2223 documentFragment.appendTo( clone );
2224 clone.insertAfter( toSplit );
2225 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
2226 return clone;
2227 },
2228
2229 /**
2230 * Recursively remove any empty path blocks at the range boundary.
2231 *
2232 * @method
2233 * @param {Boolean} atEnd Removal to perform at the end boundary,
2234 * otherwise to perform at the start.
2235 */
2236 removeEmptyBlocksAtEnd: ( function() {
2237
2238 var whitespace = CKEDITOR.dom.walker.whitespaces(),
2239 bookmark = CKEDITOR.dom.walker.bookmark( false );
2240
2241 function childEval( parent ) {
2242 return function( node ) {
2243 // Whitespace, bookmarks, empty inlines.
2244 if ( whitespace( node ) || bookmark( node ) ||
2245 node.type == CKEDITOR.NODE_ELEMENT &&
2246 node.isEmptyInlineRemoveable() ) {
2247 return false;
2248 } else if ( parent.is( 'table' ) && node.is( 'caption' ) ) {
2249 return false;
2250 }
2251
2252 return true;
2253 };
2254 }
2255
2256 return function( atEnd ) {
2257
2258 var bm = this.createBookmark();
2259 var path = this[ atEnd ? 'endPath' : 'startPath' ]();
2260 var block = path.block || path.blockLimit, parent;
2261
2262 // Remove any childless block, including list and table.
2263 while ( block && !block.equals( path.root ) &&
2264 !block.getFirst( childEval( block ) ) ) {
2265 parent = block.getParent();
2266 this[ atEnd ? 'setEndAt' : 'setStartAt' ]( block, CKEDITOR.POSITION_AFTER_END );
2267 block.remove( 1 );
2268 block = parent;
2269 }
2270
2271 this.moveToBookmark( bm );
2272 };
2273
2274 } )(),
2275
2276 /**
2277 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #startContainer}.
2278 *
2279 * @returns {CKEDITOR.dom.elementPath}
2280 */
2281 startPath: function() {
2282 return new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2283 },
2284
2285 /**
2286 * Gets {@link CKEDITOR.dom.elementPath} for the {@link #endContainer}.
2287 *
2288 * @returns {CKEDITOR.dom.elementPath}
2289 */
2290 endPath: function() {
2291 return new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2292 },
2293
2294 /**
2295 * Check whether a range boundary is at the inner boundary of a given
2296 * element.
2297 *
2298 * @param {CKEDITOR.dom.element} element The target element to check.
2299 * @param {Number} checkType The boundary to check for both the range
2300 * and the element. It can be {@link CKEDITOR#START} or {@link CKEDITOR#END}.
2301 * @returns {Boolean} `true` if the range boundary is at the inner
2302 * boundary of the element.
2303 */
2304 checkBoundaryOfElement: function( element, checkType ) {
2305 var checkStart = ( checkType == CKEDITOR.START );
2306
2307 // Create a copy of this range, so we can manipulate it for our checks.
2308 var walkerRange = this.clone();
2309
2310 // Collapse the range at the proper size.
2311 walkerRange.collapse( checkStart );
2312
2313 // Expand the range to element boundary.
2314 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
2315
2316 // Create the walker, which will check if we have anything useful
2317 // in the range.
2318 var walker = new CKEDITOR.dom.walker( walkerRange );
2319 walker.evaluator = elementBoundaryEval( checkStart );
2320
2321 return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
2322 },
2323
2324 /**
2325 * **Note:** Calls to this function may produce changes to the DOM. The range may
2326 * be updated to reflect such changes.
2327 *
2328 * @returns {Boolean}
2329 * @todo
2330 */
2331 checkStartOfBlock: function() {
2332 var startContainer = this.startContainer,
2333 startOffset = this.startOffset;
2334
2335 // [IE] Special handling for range start in text with a leading NBSP,
2336 // we it to be isolated, for bogus check.
2337 if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) {
2338 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
2339 if ( nbspRegExp.test( textBefore ) )
2340 this.trim( 0, 1 );
2341 }
2342
2343 // Antecipate the trim() call here, so the walker will not make
2344 // changes to the DOM, which would not get reflected into this
2345 // range otherwise.
2346 this.trim();
2347
2348 // We need to grab the block element holding the start boundary, so
2349 // let's use an element path for it.
2350 var path = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
2351
2352 // Creates a range starting at the block start until the range start.
2353 var walkerRange = this.clone();
2354 walkerRange.collapse( true );
2355 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
2356
2357 var walker = new CKEDITOR.dom.walker( walkerRange );
2358 walker.evaluator = getCheckStartEndBlockEvalFunction();
2359
2360 return walker.checkBackward();
2361 },
2362
2363 /**
2364 * **Note:** Calls to this function may produce changes to the DOM. The range may
2365 * be updated to reflect such changes.
2366 *
2367 * @returns {Boolean}
2368 * @todo
2369 */
2370 checkEndOfBlock: function() {
2371 var endContainer = this.endContainer,
2372 endOffset = this.endOffset;
2373
2374 // [IE] Special handling for range end in text with a following NBSP,
2375 // we it to be isolated, for bogus check.
2376 if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT ) {
2377 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
2378 if ( nbspRegExp.test( textAfter ) )
2379 this.trim( 1, 0 );
2380 }
2381
2382 // Antecipate the trim() call here, so the walker will not make
2383 // changes to the DOM, which would not get reflected into this
2384 // range otherwise.
2385 this.trim();
2386
2387 // We need to grab the block element holding the start boundary, so
2388 // let's use an element path for it.
2389 var path = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
2390
2391 // Creates a range starting at the block start until the range start.
2392 var walkerRange = this.clone();
2393 walkerRange.collapse( false );
2394 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
2395
2396 var walker = new CKEDITOR.dom.walker( walkerRange );
2397 walker.evaluator = getCheckStartEndBlockEvalFunction();
2398
2399 return walker.checkForward();
2400 },
2401
2402 /**
2403 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.
2404 *
2405 * @param {Function} evaluator Function used as the walker's evaluator.
2406 * @param {Function} [guard] Function used as the walker's guard.
2407 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2408 * default to the root editable if not defined.
2409 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2410 */
2411 getPreviousNode: function( evaluator, guard, boundary ) {
2412 var walkerRange = this.clone();
2413 walkerRange.collapse( 1 );
2414 walkerRange.setStartAt( boundary || this.root, CKEDITOR.POSITION_AFTER_START );
2415
2416 var walker = new CKEDITOR.dom.walker( walkerRange );
2417 walker.evaluator = evaluator;
2418 walker.guard = guard;
2419 return walker.previous();
2420 },
2421
2422 /**
2423 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.
2424 *
2425 * @param {Function} evaluator Function used as the walker's evaluator.
2426 * @param {Function} [guard] Function used as the walker's guard.
2427 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
2428 * default to the root editable if not defined.
2429 * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
2430 */
2431 getNextNode: function( evaluator, guard, boundary ) {
2432 var walkerRange = this.clone();
2433 walkerRange.collapse();
2434 walkerRange.setEndAt( boundary || this.root, CKEDITOR.POSITION_BEFORE_END );
2435
2436 var walker = new CKEDITOR.dom.walker( walkerRange );
2437 walker.evaluator = evaluator;
2438 walker.guard = guard;
2439 return walker.next();
2440 },
2441
2442 /**
2443 * Check if elements at which the range boundaries anchor are read-only,
2444 * with respect to `contenteditable` attribute.
2445 *
2446 * @returns {Boolean}
2447 */
2448 checkReadOnly: ( function() {
2449 function checkNodesEditable( node, anotherEnd ) {
2450 while ( node ) {
2451 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
2452 if ( node.getAttribute( 'contentEditable' ) == 'false' && !node.data( 'cke-editable' ) )
2453 return 0;
2454
2455 // Range enclosed entirely in an editable element.
2456 else if ( node.is( 'html' ) || node.getAttribute( 'contentEditable' ) == 'true' && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
2457 break;
2458
2459 }
2460 node = node.getParent();
2461 }
2462
2463 return 1;
2464 }
2465
2466 return function() {
2467 var startNode = this.startContainer,
2468 endNode = this.endContainer;
2469
2470 // Check if elements path at both boundaries are editable.
2471 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
2472 };
2473 } )(),
2474
2475 /**
2476 * Moves the range boundaries to the first/end editing point inside an
2477 * element.
2478 *
2479 * For example, in an element tree like
2480 * `<p><b><i></i></b> Text</p>`, the start editing point is
2481 * `<p><b><i>^</i></b> Text</p>` (inside `<i>`).
2482 *
2483 * @param {CKEDITOR.dom.element} el The element into which look for the
2484 * editing spot.
2485 * @param {Boolean} isMoveToEnd Whether move to the end editable position.
2486 * @returns {Boolean} Whether range was moved.
2487 */
2488 moveToElementEditablePosition: function( el, isMoveToEnd ) {
2489
2490 function nextDFS( node, childOnly ) {
2491 var next;
2492
2493 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )
2494 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( notIgnoredEval );
2495
2496 if ( !childOnly && !next )
2497 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( notIgnoredEval );
2498
2499 return next;
2500 }
2501
2502 // Handle non-editable element e.g. HR.
2503 if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) {
2504 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2505 return true;
2506 }
2507
2508 var found = 0;
2509
2510 while ( el ) {
2511 // Stop immediately if we've found a text node.
2512 if ( el.type == CKEDITOR.NODE_TEXT ) {
2513 // Put cursor before block filler.
2514 if ( isMoveToEnd && this.endContainer && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )
2515 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2516 else
2517 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
2518 found = 1;
2519 break;
2520 }
2521
2522 // If an editable element is found, move inside it, but not stop the searching.
2523 if ( el.type == CKEDITOR.NODE_ELEMENT ) {
2524 if ( el.isEditable() ) {
2525 this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_START );
2526 found = 1;
2527 }
2528 // Put cursor before padding block br.
2529 else if ( isMoveToEnd && el.is( 'br' ) && this.endContainer && this.checkEndOfBlock() )
2530 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
2531 // Special case - non-editable block. Select entire element, because it does not make sense
2532 // to place collapsed selection next to it, because browsers can't handle that.
2533 else if ( el.getAttribute( 'contenteditable' ) == 'false' && el.is( CKEDITOR.dtd.$block ) ) {
2534 this.setStartBefore( el );
2535 this.setEndAfter( el );
2536 return true;
2537 }
2538 }
2539
2540 el = nextDFS( el, found );
2541 }
2542
2543 return !!found;
2544 },
2545
2546 /**
2547 * Moves the range boundaries to the closest editing point after/before an
2548 * element or the current range position (depends on whether the element was specified).
2549 *
2550 * For example, if the start element has `id="start"`,
2551 * `<p><b>foo</b><span id="start">start</start></p>`, the closest previous editing point is
2552 * `<p><b>foo</b>^<span id="start">start</start></p>` (between `<b>` and `<span>`).
2553 *
2554 * See also: {@link #moveToElementEditablePosition}.
2555 *
2556 * @since 4.3
2557 * @param {CKEDITOR.dom.element} [element] The starting element. If not specified, the current range
2558 * position will be used.
2559 * @param {Boolean} [isMoveForward] Whether move to the end of editable. Otherwise, look back.
2560 * @returns {Boolean} Whether the range was moved.
2561 */
2562 moveToClosestEditablePosition: function( element, isMoveForward ) {
2563 // We don't want to modify original range if there's no editable position.
2564 var range,
2565 found = 0,
2566 sibling,
2567 isElement,
2568 positions = [ CKEDITOR.POSITION_AFTER_END, CKEDITOR.POSITION_BEFORE_START ];
2569
2570 if ( element ) {
2571 // Set collapsed range at one of ends of element.
2572 // Can't clone this range, because this range might not be yet positioned (no containers => errors).
2573 range = new CKEDITOR.dom.range( this.root );
2574 range.moveToPosition( element, positions[ isMoveForward ? 0 : 1 ] );
2575 } else {
2576 range = this.clone();
2577 }
2578
2579 // Start element isn't a block, so we can automatically place range
2580 // next to it.
2581 if ( element && !element.is( CKEDITOR.dtd.$block ) )
2582 found = 1;
2583 else {
2584 // Look for first node that fulfills eval function and place range next to it.
2585 sibling = range[ isMoveForward ? 'getNextEditableNode' : 'getPreviousEditableNode' ]();
2586 if ( sibling ) {
2587 found = 1;
2588 isElement = sibling.type == CKEDITOR.NODE_ELEMENT;
2589
2590 // Special case - eval accepts block element only if it's a non-editable block,
2591 // which we want to select, not place collapsed selection next to it (which browsers
2592 // can't handle).
2593 if ( isElement && sibling.is( CKEDITOR.dtd.$block ) && sibling.getAttribute( 'contenteditable' ) == 'false' ) {
2594 range.setStartAt( sibling, CKEDITOR.POSITION_BEFORE_START );
2595 range.setEndAt( sibling, CKEDITOR.POSITION_AFTER_END );
2596 }
2597 // Handle empty blocks which can be selection containers on old IEs.
2598 else if ( !CKEDITOR.env.needsBrFiller && isElement && sibling.is( CKEDITOR.dom.walker.validEmptyBlockContainers ) ) {
2599 range.setEnd( sibling, 0 );
2600 range.collapse();
2601 } else {
2602 range.moveToPosition( sibling, positions[ isMoveForward ? 1 : 0 ] );
2603 }
2604 }
2605 }
2606
2607 if ( found )
2608 this.moveToRange( range );
2609
2610 return !!found;
2611 },
2612
2613 /**
2614 * See {@link #moveToElementEditablePosition}.
2615 *
2616 * @returns {Boolean} Whether range was moved.
2617 */
2618 moveToElementEditStart: function( target ) {
2619 return this.moveToElementEditablePosition( target );
2620 },
2621
2622 /**
2623 * See {@link #moveToElementEditablePosition}.
2624 *
2625 * @returns {Boolean} Whether range was moved.
2626 */
2627 moveToElementEditEnd: function( target ) {
2628 return this.moveToElementEditablePosition( target, true );
2629 },
2630
2631 /**
2632 * Get the single node enclosed within the range if there's one.
2633 *
2634 * @returns {CKEDITOR.dom.node}
2635 */
2636 getEnclosedNode: function() {
2637 var walkerRange = this.clone();
2638
2639 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
2640 walkerRange.optimize();
2641 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
2642 return null;
2643
2644 var walker = new CKEDITOR.dom.walker( walkerRange ),
2645 isNotBookmarks = CKEDITOR.dom.walker.bookmark( false, true ),
2646 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true );
2647
2648 walker.evaluator = function( node ) {
2649 return isNotWhitespaces( node ) && isNotBookmarks( node );
2650 };
2651 var node = walker.next();
2652 walker.reset();
2653 return node && node.equals( walker.previous() ) ? node : null;
2654 },
2655
2656 /**
2657 * Get the node adjacent to the range start or {@link #startContainer}.
2658 *
2659 * @returns {CKEDITOR.dom.node}
2660 */
2661 getTouchedStartNode: function() {
2662 var container = this.startContainer;
2663
2664 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2665 return container;
2666
2667 return container.getChild( this.startOffset ) || container;
2668 },
2669
2670 /**
2671 * Get the node adjacent to the range end or {@link #endContainer}.
2672 *
2673 * @returns {CKEDITOR.dom.node}
2674 */
2675 getTouchedEndNode: function() {
2676 var container = this.endContainer;
2677
2678 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
2679 return container;
2680
2681 return container.getChild( this.endOffset - 1 ) || container;
2682 },
2683
2684 /**
2685 * Gets next node which can be a container of a selection.
2686 * This methods mimics a behavior of right/left arrow keys in case of
2687 * collapsed selection. It does not return an exact position (with offset) though,
2688 * but just a selection's container.
2689 *
2690 * Note: use this method on a collapsed range.
2691 *
2692 * @since 4.3
2693 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2694 */
2695 getNextEditableNode: getNextEditableNode(),
2696
2697 /**
2698 * See {@link #getNextEditableNode}.
2699 *
2700 * @since 4.3
2701 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
2702 */
2703 getPreviousEditableNode: getNextEditableNode( 1 ),
2704
2705 /**
2706 * Scrolls the start of current range into view.
2707 */
2708 scrollIntoView: function() {
2709
2710 // The reference element contains a zero-width space to avoid
2711 // a premature removal. The view is to be scrolled with respect
2712 // to this element.
2713 var reference = new CKEDITOR.dom.element.createFromHtml( '<span>&nbsp;</span>', this.document ),
2714 afterCaretNode, startContainerText, isStartText;
2715
2716 var range = this.clone();
2717
2718 // Work with the range to obtain a proper caret position.
2719 range.optimize();
2720
2721 // Currently in a text node, so we need to split it into two
2722 // halves and put the reference between.
2723 if ( isStartText = range.startContainer.type == CKEDITOR.NODE_TEXT ) {
2724 // Keep the original content. It will be restored.
2725 startContainerText = range.startContainer.getText();
2726
2727 // Split the startContainer at the this position.
2728 afterCaretNode = range.startContainer.split( range.startOffset );
2729
2730 // Insert the reference between two text nodes.
2731 reference.insertAfter( range.startContainer );
2732 }
2733
2734 // If not in a text node, simply insert the reference into the range.
2735 else {
2736 range.insertNode( reference );
2737 }
2738
2739 // Scroll with respect to the reference element.
2740 reference.scrollIntoView();
2741
2742 // Get rid of split parts if "in a text node" case.
2743 // Revert the original text of the startContainer.
2744 if ( isStartText ) {
2745 range.startContainer.setText( startContainerText );
2746 afterCaretNode.remove();
2747 }
2748
2749 // Get rid of the reference node. It is no longer necessary.
2750 reference.remove();
2751 },
2752
2753 /**
2754 * Setter for the {@link #startContainer}.
2755 *
2756 * @since 4.4.6
2757 * @private
2758 * @param {CKEDITOR.dom.element} startContainer
2759 */
2760 _setStartContainer: function( startContainer ) {
2761 // %REMOVE_START%
2762 var isRootAscendantOrSelf = this.root.equals( startContainer ) || this.root.contains( startContainer );
2763
2764 if ( !isRootAscendantOrSelf ) {
2765 CKEDITOR.warn( 'range-startcontainer', { startContainer: startContainer, root: this.root } );
2766 }
2767 // %REMOVE_END%
2768 this.startContainer = startContainer;
2769 },
2770
2771 /**
2772 * Setter for the {@link #endContainer}.
2773 *
2774 * @since 4.4.6
2775 * @private
2776 * @param {CKEDITOR.dom.element} endContainer
2777 */
2778 _setEndContainer: function( endContainer ) {
2779 // %REMOVE_START%
2780 var isRootAscendantOrSelf = this.root.equals( endContainer ) || this.root.contains( endContainer );
2781
2782 if ( !isRootAscendantOrSelf ) {
2783 CKEDITOR.warn( 'range-endcontainer', { endContainer: endContainer, root: this.root } );
2784 }
2785 // %REMOVE_END%
2786 this.endContainer = endContainer;
2787 },
2788
2789 /**
2790 * Looks for elements matching the `query` selector within a range.
2791 *
2792 * @since 4.5.11
2793 * @private
2794 * @param {String} query
2795 * @param {Boolean} [includeNonEditables=false] Whether elements with `contenteditable` set to `false` should
2796 * be included.
2797 * @returns {CKEDITOR.dom.element[]}
2798 */
2799 _find: function( query, includeNonEditables ) {
2800 var ancestor = this.getCommonAncestor(),
2801 boundaries = this.getBoundaryNodes(),
2802 // Contrary to CKEDITOR.dom.element#find we're returning array, that's because NodeList is immutable, and we need
2803 // to do some filtering in returned list.
2804 ret = [],
2805 curItem,
2806 i,
2807 initialMatches,
2808 isStartGood,
2809 isEndGood;
2810
2811 if ( ancestor && ancestor.find ) {
2812 initialMatches = ancestor.find( query );
2813
2814 for ( i = 0; i < initialMatches.count(); i++ ) {
2815 curItem = initialMatches.getItem( i );
2816
2817 // Using isReadOnly() method to filterout non editables. It checks isContentEditable including all browser quirks.
2818 if ( !includeNonEditables && curItem.isReadOnly() ) {
2819 continue;
2820 }
2821
2822 // It's not enough to get elements from common ancestor, because it migth contain too many matches.
2823 // We need to ensure that returned items are between boundary points.
2824 isStartGood = ( curItem.getPosition( boundaries.startNode ) & CKEDITOR.POSITION_FOLLOWING ) || boundaries.startNode.equals( curItem );
2825 isEndGood = ( curItem.getPosition( boundaries.endNode ) & ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IS_CONTAINED ) );
2826
2827 if ( isStartGood && isEndGood ) {
2828 ret.push( curItem );
2829 }
2830 }
2831 }
2832
2833 return ret;
2834 }
2835 };
2836
2837
2838} )();
2839
2840/**
2841 * Indicates a position after start of a node.
2842 *
2843 * // When used according to an element:
2844 * // <element>^contents</element>
2845 *
2846 * // When used according to a text node:
2847 * // "^text" (range is anchored in the text node)
2848 *
2849 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2850 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2851 *
2852 * @readonly
2853 * @member CKEDITOR
2854 * @property {Number} [=1]
2855 */
2856CKEDITOR.POSITION_AFTER_START = 1;
2857
2858/**
2859 * Indicates a position before end of a node.
2860 *
2861 * // When used according to an element:
2862 * // <element>contents^</element>
2863 *
2864 * // When used according to a text node:
2865 * // "text^" (range is anchored in the text node)
2866 *
2867 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2868 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2869 *
2870 * @readonly
2871 * @member CKEDITOR
2872 * @property {Number} [=2]
2873 */
2874CKEDITOR.POSITION_BEFORE_END = 2;
2875
2876/**
2877 * Indicates a position before start of a node.
2878 *
2879 * // When used according to an element:
2880 * // ^<element>contents</element> (range is anchored in element's parent)
2881 *
2882 * // When used according to a text node:
2883 * // ^"text" (range is anchored in text node's parent)
2884 *
2885 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2886 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2887 *
2888 * @readonly
2889 * @member CKEDITOR
2890 * @property {Number} [=3]
2891 */
2892CKEDITOR.POSITION_BEFORE_START = 3;
2893
2894/**
2895 * Indicates a position after end of a node.
2896 *
2897 * // When used according to an element:
2898 * // <element>contents</element>^ (range is anchored in element's parent)
2899 *
2900 * // When used according to a text node:
2901 * // "text"^ (range is anchored in text node's parent)
2902 *
2903 * It is used as a parameter of methods like: {@link CKEDITOR.dom.range#moveToPosition},
2904 * {@link CKEDITOR.dom.range#setStartAt} and {@link CKEDITOR.dom.range#setEndAt}.
2905 *
2906 * @readonly
2907 * @member CKEDITOR
2908 * @property {Number} [=4]
2909 */
2910CKEDITOR.POSITION_AFTER_END = 4;
2911
2912/**
2913 * @readonly
2914 * @member CKEDITOR
2915 * @property {Number} [=1]
2916 */
2917CKEDITOR.ENLARGE_ELEMENT = 1;
2918
2919/**
2920 * @readonly
2921 * @member CKEDITOR
2922 * @property {Number} [=2]
2923 */
2924CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
2925
2926/**
2927 * @readonly
2928 * @member CKEDITOR
2929 * @property {Number} [=3]
2930 */
2931CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
2932
2933/**
2934 * @readonly
2935 * @member CKEDITOR
2936 * @property {Number} [=4]
2937 */
2938CKEDITOR.ENLARGE_INLINE = 4;
2939
2940// Check boundary types.
2941
2942/**
2943 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2944 *
2945 * @readonly
2946 * @member CKEDITOR
2947 * @property {Number} [=1]
2948 */
2949CKEDITOR.START = 1;
2950
2951/**
2952 * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
2953 *
2954 * @readonly
2955 * @member CKEDITOR
2956 * @property {Number} [=2]
2957 */
2958CKEDITOR.END = 2;
2959
2960// Shrink range types.
2961
2962/**
2963 * See {@link CKEDITOR.dom.range#shrink}.
2964 *
2965 * @readonly
2966 * @member CKEDITOR
2967 * @property {Number} [=1]
2968 */
2969CKEDITOR.SHRINK_ELEMENT = 1;
2970
2971/**
2972 * See {@link CKEDITOR.dom.range#shrink}.
2973 *
2974 * @readonly
2975 * @member CKEDITOR
2976 * @property {Number} [=2]
2977 */
2978CKEDITOR.SHRINK_TEXT = 2;
diff --git a/sources/core/dom/rangelist.js b/sources/core/dom/rangelist.js
new file mode 100644
index 0000000..250dfd9
--- /dev/null
+++ b/sources/core/dom/rangelist.js
@@ -0,0 +1,199 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /**
8 * Represents a list os CKEDITOR.dom.range objects, which can be easily
9 * iterated sequentially.
10 *
11 * @class
12 * @extends Array
13 * @constructor Creates a rangeList class instance.
14 * @param {CKEDITOR.dom.range/CKEDITOR.dom.range[]} [ranges] The ranges contained on this list.
15 * Note that, if an array of ranges is specified, the range sequence
16 * should match its DOM order. This class will not help to sort them.
17 */
18 CKEDITOR.dom.rangeList = function( ranges ) {
19 if ( ranges instanceof CKEDITOR.dom.rangeList )
20 return ranges;
21
22 if ( !ranges )
23 ranges = [];
24 else if ( ranges instanceof CKEDITOR.dom.range )
25 ranges = [ ranges ];
26
27 return CKEDITOR.tools.extend( ranges, mixins );
28 };
29
30 var mixins = {
31 /**
32 * Creates an instance of the rangeList iterator, it should be used
33 * only when the ranges processing could be DOM intrusive, which
34 * means it may pollute and break other ranges in this list.
35 * Otherwise, it's enough to just iterate over this array in a for loop.
36 *
37 * @returns {CKEDITOR.dom.rangeListIterator}
38 */
39 createIterator: function() {
40 var rangeList = this,
41 bookmark = CKEDITOR.dom.walker.bookmark(),
42 bookmarks = [],
43 current;
44
45 return {
46 /**
47 * Retrieves the next range in the list.
48 *
49 * @member CKEDITOR.dom.rangeListIterator
50 * @param {Boolean} [mergeConsequent=false] Whether join two adjacent
51 * ranges into single, e.g. consequent table cells.
52 */
53 getNextRange: function( mergeConsequent ) {
54 current = current === undefined ? 0 : current + 1;
55
56 var range = rangeList[ current ];
57
58 // Multiple ranges might be mangled by each other.
59 if ( range && rangeList.length > 1 ) {
60 // Bookmarking all other ranges on the first iteration,
61 // the range correctness after it doesn't matter since we'll
62 // restore them before the next iteration.
63 if ( !current ) {
64 // Make sure bookmark correctness by reverse processing.
65 for ( var i = rangeList.length - 1; i >= 0; i-- )
66 bookmarks.unshift( rangeList[ i ].createBookmark( true ) );
67 }
68
69 if ( mergeConsequent ) {
70 // Figure out how many ranges should be merged.
71 var mergeCount = 0;
72 while ( rangeList[ current + mergeCount + 1 ] ) {
73 var doc = range.document,
74 found = 0,
75 left = doc.getById( bookmarks[ mergeCount ].endNode ),
76 right = doc.getById( bookmarks[ mergeCount + 1 ].startNode ),
77 next;
78
79 // Check subsequent range.
80 while ( 1 ) {
81 next = left.getNextSourceNode( false );
82 if ( !right.equals( next ) ) {
83 // This could be yet another bookmark or
84 // walking across block boundaries.
85 if ( bookmark( next ) || ( next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() ) ) {
86 left = next;
87 continue;
88 }
89 } else {
90 found = 1;
91 }
92
93 break;
94 }
95
96 if ( !found )
97 break;
98
99 mergeCount++;
100 }
101 }
102
103 range.moveToBookmark( bookmarks.shift() );
104
105 // Merge ranges finally after moving to bookmarks.
106 while ( mergeCount-- ) {
107 next = rangeList[ ++current ];
108 next.moveToBookmark( bookmarks.shift() );
109 range.setEnd( next.endContainer, next.endOffset );
110 }
111 }
112
113 return range;
114 }
115 };
116 },
117
118 /**
119 * Create bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark}.
120 *
121 * @param {Boolean} [serializable=false] See {@link CKEDITOR.dom.range#createBookmark}.
122 * @returns {Array} Array of bookmarks.
123 */
124 createBookmarks: function( serializable ) {
125 var retval = [],
126 bookmark;
127 for ( var i = 0; i < this.length; i++ ) {
128 retval.push( bookmark = this[ i ].createBookmark( serializable, true ) );
129
130 // Updating the container & offset values for ranges
131 // that have been touched.
132 for ( var j = i + 1; j < this.length; j++ ) {
133 this[ j ] = updateDirtyRange( bookmark, this[ j ] );
134 this[ j ] = updateDirtyRange( bookmark, this[ j ], true );
135 }
136 }
137 return retval;
138 },
139
140 /**
141 * Create "unobtrusive" bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark2}.
142 *
143 * @param {Boolean} [normalized=false] See {@link CKEDITOR.dom.range#createBookmark2}.
144 * @returns {Array} Array of bookmarks.
145 */
146 createBookmarks2: function( normalized ) {
147 var bookmarks = [];
148
149 for ( var i = 0; i < this.length; i++ )
150 bookmarks.push( this[ i ].createBookmark2( normalized ) );
151
152 return bookmarks;
153 },
154
155 /**
156 * Move each range in the list to the position specified by a list of bookmarks.
157 *
158 * @param {Array} bookmarks The list of bookmarks, each one matching a range in the list.
159 */
160 moveToBookmarks: function( bookmarks ) {
161 for ( var i = 0; i < this.length; i++ )
162 this[ i ].moveToBookmark( bookmarks[ i ] );
163 }
164 };
165
166 // Update the specified range which has been mangled by previous insertion of
167 // range bookmark nodes.(#3256)
168 function updateDirtyRange( bookmark, dirtyRange, checkEnd ) {
169 var serializable = bookmark.serializable,
170 container = dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ],
171 offset = checkEnd ? 'endOffset' : 'startOffset';
172
173 var bookmarkStart = serializable ? dirtyRange.document.getById( bookmark.startNode ) : bookmark.startNode;
174
175 var bookmarkEnd = serializable ? dirtyRange.document.getById( bookmark.endNode ) : bookmark.endNode;
176
177 if ( container.equals( bookmarkStart.getPrevious() ) ) {
178 dirtyRange.startOffset = dirtyRange.startOffset - container.getLength() - bookmarkEnd.getPrevious().getLength();
179 container = bookmarkEnd.getNext();
180 } else if ( container.equals( bookmarkEnd.getPrevious() ) ) {
181 dirtyRange.startOffset = dirtyRange.startOffset - container.getLength();
182 container = bookmarkEnd.getNext();
183 }
184
185 container.equals( bookmarkStart.getParent() ) && dirtyRange[ offset ]++;
186 container.equals( bookmarkEnd.getParent() ) && dirtyRange[ offset ]++;
187
188 // Update and return this range.
189 dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ] = container;
190 return dirtyRange;
191 }
192} )();
193
194/**
195 * (Virtual Class) Do not call this constructor. This class is not really part
196 * of the API. It just describes the return type of {@link CKEDITOR.dom.rangeList#createIterator}.
197 *
198 * @class CKEDITOR.dom.rangeListIterator
199 */
diff --git a/sources/core/dom/text.js b/sources/core/dom/text.js
new file mode 100644
index 0000000..ce20ffe
--- /dev/null
+++ b/sources/core/dom/text.js
@@ -0,0 +1,135 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.text} class, which represents
8 * a DOM text node.
9 */
10
11/**
12 * Represents a DOM text node.
13 *
14 * var nativeNode = document.createTextNode( 'Example' );
15 * var text = new CKEDITOR.dom.text( nativeNode );
16 *
17 * var text = new CKEDITOR.dom.text( 'Example' );
18 *
19 * @class
20 * @extends CKEDITOR.dom.node
21 * @constructor Creates a text class instance.
22 * @param {Object/String} text A native DOM text node or a string containing
23 * the text to use to create a new text node.
24 * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
25 * the node in case of new node creation. Defaults to the current document.
26 */
27CKEDITOR.dom.text = function( text, ownerDocument ) {
28 if ( typeof text == 'string' )
29 text = ( ownerDocument ? ownerDocument.$ : document ).createTextNode( text );
30
31 // Theoretically, we should call the base constructor here
32 // (not CKEDITOR.dom.node though). But, IE doesn't support expando
33 // properties on text node, so the features provided by domObject will not
34 // work for text nodes (which is not a big issue for us).
35 //
36 // CKEDITOR.dom.domObject.call( this, element );
37
38 this.$ = text;
39};
40
41CKEDITOR.dom.text.prototype = new CKEDITOR.dom.node();
42
43CKEDITOR.tools.extend( CKEDITOR.dom.text.prototype, {
44 /**
45 * The node type. This is a constant value set to {@link CKEDITOR#NODE_TEXT}.
46 *
47 * @readonly
48 * @property {Number} [=CKEDITOR.NODE_TEXT]
49 */
50 type: CKEDITOR.NODE_TEXT,
51
52 /**
53 * Gets length of node's value.
54 *
55 * @returns {Number}
56 */
57 getLength: function() {
58 return this.$.nodeValue.length;
59 },
60
61 /**
62 * Gets node's value.
63 *
64 * @returns {String}
65 */
66 getText: function() {
67 return this.$.nodeValue;
68 },
69
70 /**
71 * Sets node's value.
72 *
73 * @param {String} text
74 */
75 setText: function( text ) {
76 this.$.nodeValue = text;
77 },
78
79 /**
80 * Breaks this text node into two nodes at the specified offset,
81 * keeping both in the tree as siblings. This node then only contains
82 * all the content up to the offset point. A new text node, which is
83 * inserted as the next sibling of this node, contains all the content
84 * at and after the offset point. When the offset is equal to the
85 * length of this node, the new node has no data.
86 *
87 * @param {Number} The position at which to split, starting from zero.
88 * @returns {CKEDITOR.dom.text} The new text node.
89 */
90 split: function( offset ) {
91
92 // Saved the children count and text length beforehand.
93 var parent = this.$.parentNode,
94 count = parent.childNodes.length,
95 length = this.getLength();
96
97 var doc = this.getDocument();
98 var retval = new CKEDITOR.dom.text( this.$.splitText( offset ), doc );
99
100 if ( parent.childNodes.length == count ) {
101 // If the offset is after the last char, IE creates the text node
102 // on split, but don't include it into the DOM. So, we have to do
103 // that manually here.
104 if ( offset >= length ) {
105 retval = doc.createText( '' );
106 retval.insertAfter( this );
107 } else {
108 // IE BUG: IE8+ does not update the childNodes array in DOM after splitText(),
109 // we need to make some DOM changes to make it update. (#3436)
110 var workaround = doc.createText( '' );
111 workaround.insertAfter( retval );
112 workaround.remove();
113 }
114 }
115
116 return retval;
117 },
118
119 /**
120 * Extracts characters from indexA up to but not including `indexB`.
121 *
122 * @param {Number} indexA An integer between `0` and one less than the
123 * length of the text.
124 * @param {Number} [indexB] An integer between `0` and the length of the
125 * string. If omitted, extracts characters to the end of the text.
126 */
127 substring: function( indexA, indexB ) {
128 // We need the following check due to a Firefox bug
129 // https://bugzilla.mozilla.org/show_bug.cgi?id=458886
130 if ( typeof indexB != 'number' )
131 return this.$.nodeValue.substr( indexA );
132 else
133 return this.$.nodeValue.substring( indexA, indexB );
134 }
135} );
diff --git a/sources/core/dom/walker.js b/sources/core/dom/walker.js
new file mode 100644
index 0000000..cec4574
--- /dev/null
+++ b/sources/core/dom/walker.js
@@ -0,0 +1,652 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 // This function is to be called under a "walker" instance scope.
8 function iterate( rtl, breakOnFalse ) {
9 var range = this.range;
10
11 // Return null if we have reached the end.
12 if ( this._.end )
13 return null;
14
15 // This is the first call. Initialize it.
16 if ( !this._.start ) {
17 this._.start = 1;
18
19 // A collapsed range must return null at first call.
20 if ( range.collapsed ) {
21 this.end();
22 return null;
23 }
24
25 // Move outside of text node edges.
26 range.optimize();
27 }
28
29 var node,
30 startCt = range.startContainer,
31 endCt = range.endContainer,
32 startOffset = range.startOffset,
33 endOffset = range.endOffset,
34 guard,
35 userGuard = this.guard,
36 type = this.type,
37 getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' );
38
39 // Create the LTR guard function, if necessary.
40 if ( !rtl && !this._.guardLTR ) {
41 // The node that stops walker from moving up.
42 var limitLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt : endCt.getParent();
43
44 // The node that stops the walker from going to next.
45 var blockerLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt.getChild( endOffset ) : endCt.getNext();
46
47 this._.guardLTR = function( node, movingOut ) {
48 return ( ( !movingOut || !limitLTR.equals( node ) ) && ( !blockerLTR || !node.equals( blockerLTR ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
49 };
50 }
51
52 // Create the RTL guard function, if necessary.
53 if ( rtl && !this._.guardRTL ) {
54 // The node that stops walker from moving up.
55 var limitRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startCt : startCt.getParent();
56
57 // The node that stops the walker from going to next.
58 var blockerRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startOffset ? startCt.getChild( startOffset - 1 ) : null : startCt.getPrevious();
59
60 this._.guardRTL = function( node, movingOut ) {
61 return ( ( !movingOut || !limitRTL.equals( node ) ) && ( !blockerRTL || !node.equals( blockerRTL ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
62 };
63 }
64
65 // Define which guard function to use.
66 var stopGuard = rtl ? this._.guardRTL : this._.guardLTR;
67
68 // Make the user defined guard function participate in the process,
69 // otherwise simply use the boundary guard.
70 if ( userGuard ) {
71 guard = function( node, movingOut ) {
72 if ( stopGuard( node, movingOut ) === false )
73 return false;
74
75 return userGuard( node, movingOut );
76 };
77 } else {
78 guard = stopGuard;
79 }
80
81 if ( this.current )
82 node = this.current[ getSourceNodeFn ]( false, type, guard );
83 else {
84 // Get the first node to be returned.
85 if ( rtl ) {
86 node = endCt;
87
88 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
89 if ( endOffset > 0 )
90 node = node.getChild( endOffset - 1 );
91 else
92 node = ( guard( node, true ) === false ) ? null : node.getPreviousSourceNode( true, type, guard );
93 }
94 } else {
95 node = startCt;
96
97 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
98 if ( !( node = node.getChild( startOffset ) ) )
99 node = ( guard( startCt, true ) === false ) ? null : startCt.getNextSourceNode( true, type, guard );
100 }
101 }
102
103 if ( node && guard( node ) === false )
104 node = null;
105 }
106
107 while ( node && !this._.end ) {
108 this.current = node;
109
110 if ( !this.evaluator || this.evaluator( node ) !== false ) {
111 if ( !breakOnFalse )
112 return node;
113 } else if ( breakOnFalse && this.evaluator ) {
114 return false;
115 }
116
117 node = node[ getSourceNodeFn ]( false, type, guard );
118 }
119
120 this.end();
121 return this.current = null;
122 }
123
124 function iterateToLast( rtl ) {
125 var node,
126 last = null;
127
128 while ( ( node = iterate.call( this, rtl ) ) )
129 last = node;
130
131 return last;
132 }
133
134 /**
135 * Utility class to "walk" the DOM inside range boundaries. If the
136 * range starts or ends in the middle of the text node, this node will
137 * be included as a whole. Outside changes to the range may break the walker.
138 *
139 * The walker may return nodes that are not totally included in the
140 * range boundaries. Let us take the following range representation,
141 * where the square brackets indicate the boundaries:
142 *
143 * [<p>Some <b>sample] text</b>
144 *
145 * While walking forward into the above range, the following nodes are
146 * returned: `<p>`, `"Some "`, `<b>` and `"sample"`. Going
147 * backwards instead we have: `"sample"` and `"Some "`. So note that the
148 * walker always returns nodes when "entering" them, but not when
149 * "leaving" them. The {@link #guard} function is instead called both when
150 * entering and when leaving nodes.
151 *
152 * @class
153 */
154 CKEDITOR.dom.walker = CKEDITOR.tools.createClass( {
155 /**
156 * Creates a walker class instance.
157 *
158 * @constructor
159 * @param {CKEDITOR.dom.range} range The range within which to walk.
160 */
161 $: function( range ) {
162 this.range = range;
163
164 /**
165 * A function executed for every matched node to check whether
166 * it is to be considered in the walk or not. If not provided, all
167 * matched nodes are considered good.
168 *
169 * If the function returns `false`, the node is ignored.
170 *
171 * @property {Function} evaluator
172 */
173 // this.evaluator = null;
174
175 /**
176 * A function executed for every node the walk passes by to check
177 * whether the walk is to be finished. It is called both when
178 * entering and when exiting nodes, as well as for the matched nodes.
179 *
180 * If this function returns `false`, the walking ends and no more
181 * nodes are evaluated.
182
183 * @property {Function} guard
184 */
185 // this.guard = null;
186
187 /** @private */
188 this._ = {};
189 },
190
191 // statics :
192 // {
193 // /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes.
194 // * @param {CKEDITOR.dom.node} startNode The node from which the walk
195 // * will start.
196 // * @param {CKEDITOR.dom.node} [endNode] The last node to be considered
197 // * in the walk. No more nodes are retrieved after touching or
198 // * passing it. If not provided, the walker stops at the
199 // * &lt;body&gt; closing boundary.
200 // * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the
201 // * provided nodes.
202 // */
203 // createOnNodes : function( startNode, endNode, startInclusive, endInclusive )
204 // {
205 // var range = new CKEDITOR.dom.range();
206 // if ( startNode )
207 // range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ;
208 // else
209 // range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ;
210 //
211 // if ( endNode )
212 // range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ;
213 // else
214 // range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ;
215 //
216 // return new CKEDITOR.dom.walker( range );
217 // }
218 // },
219 //
220 proto: {
221 /**
222 * Stops walking. No more nodes are retrieved if this function is called.
223 */
224 end: function() {
225 this._.end = 1;
226 },
227
228 /**
229 * Retrieves the next node (on the right).
230 *
231 * @returns {CKEDITOR.dom.node} The next node or `null` if no more
232 * nodes are available.
233 */
234 next: function() {
235 return iterate.call( this );
236 },
237
238 /**
239 * Retrieves the previous node (on the left).
240 *
241 * @returns {CKEDITOR.dom.node} The previous node or `null` if no more
242 * nodes are available.
243 */
244 previous: function() {
245 return iterate.call( this, 1 );
246 },
247
248 /**
249 * Checks all nodes on the right, executing the evaluation function.
250 *
251 * @returns {Boolean} `false` if the evaluator function returned
252 * `false` for any of the matched nodes. Otherwise `true`.
253 */
254 checkForward: function() {
255 return iterate.call( this, 0, 1 ) !== false;
256 },
257
258 /**
259 * Check all nodes on the left, executing the evaluation function.
260 *
261 * @returns {Boolean} `false` if the evaluator function returned
262 * `false` for any of the matched nodes. Otherwise `true`.
263 */
264 checkBackward: function() {
265 return iterate.call( this, 1, 1 ) !== false;
266 },
267
268 /**
269 * Executes a full walk forward (to the right), until no more nodes
270 * are available, returning the last valid node.
271 *
272 * @returns {CKEDITOR.dom.node} The last node on the right or `null`
273 * if no valid nodes are available.
274 */
275 lastForward: function() {
276 return iterateToLast.call( this );
277 },
278
279 /**
280 * Executes a full walk backwards (to the left), until no more nodes
281 * are available, returning the last valid node.
282 *
283 * @returns {CKEDITOR.dom.node} The last node on the left or `null`
284 * if no valid nodes are available.
285 */
286 lastBackward: function() {
287 return iterateToLast.call( this, 1 );
288 },
289
290 /**
291 * Resets the walker.
292 */
293 reset: function() {
294 delete this.current;
295 this._ = {};
296 }
297
298 }
299 } );
300
301 // Anything whose display computed style is block, list-item, table,
302 // table-row-group, table-header-group, table-footer-group, table-row,
303 // table-column-group, table-column, table-cell, table-caption, or whose node
304 // name is hr, br (when enterMode is br only) is a block boundary.
305 var blockBoundaryDisplayMatch = {
306 block: 1, 'list-item': 1, table: 1, 'table-row-group': 1,
307 'table-header-group': 1, 'table-footer-group': 1, 'table-row': 1, 'table-column-group': 1,
308 'table-column': 1, 'table-cell': 1, 'table-caption': 1
309 },
310 outOfFlowPositions = { absolute: 1, fixed: 1 };
311
312 /**
313 * Checks whether an element is displayed as a block.
314 *
315 * @member CKEDITOR.dom.element
316 * @param [customNodeNames] Custom list of nodes which will extend
317 * the default {@link CKEDITOR.dtd#$block} list.
318 * @returns {Boolean}
319 */
320 CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) {
321 // Whether element is in normal page flow. Floated or positioned elements are out of page flow.
322 // Don't consider floated or positioned formatting as block boundary, fall back to dtd check in that case. (#6297)
323 var inPageFlow = this.getComputedStyle( 'float' ) == 'none' && !( this.getComputedStyle( 'position' ) in outOfFlowPositions );
324
325 if ( inPageFlow && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] )
326 return true;
327
328 // Either in $block or in customNodeNames if defined.
329 return !!( this.is( CKEDITOR.dtd.$block ) || customNodeNames && this.is( customNodeNames ) );
330 };
331
332 /**
333 * Returns a function which checks whether the node is a block boundary.
334 * See {@link CKEDITOR.dom.element#isBlockBoundary}.
335 *
336 * @static
337 * @param customNodeNames
338 * @returns {Function}
339 */
340 CKEDITOR.dom.walker.blockBoundary = function( customNodeNames ) {
341 return function( node ) {
342 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary( customNodeNames ) );
343 };
344 };
345
346 /**
347 * @static
348 * @todo
349 */
350 CKEDITOR.dom.walker.listItemBoundary = function() {
351 return this.blockBoundary( { br: 1 } );
352 };
353
354 /**
355 * Returns a function which checks whether the node is a bookmark node or the bookmark node
356 * inner content.
357 *
358 * @static
359 * @param {Boolean} [contentOnly=false] Whether only test against the text content of
360 * a bookmark node instead of the element itself (default).
361 * @param {Boolean} [isReject=false] Whether to return `false` for the bookmark
362 * node instead of `true` (default).
363 * @returns {Function}
364 */
365 CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject ) {
366 function isBookmarkNode( node ) {
367 return ( node && node.getName && node.getName() == 'span' && node.data( 'cke-bookmark' ) );
368 }
369
370 return function( node ) {
371 var isBookmark, parent;
372 // Is bookmark inner text node?
373 isBookmark = ( node && node.type != CKEDITOR.NODE_ELEMENT && ( parent = node.getParent() ) && isBookmarkNode( parent ) );
374 // Is bookmark node?
375 isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node );
376 return !!( isReject ^ isBookmark );
377 };
378 };
379
380 /**
381 * Returns a function which checks whether the node is a text node containing only whitespace characters.
382 *
383 * @static
384 * @param {Boolean} [isReject=false]
385 * @returns {Function}
386 */
387 CKEDITOR.dom.walker.whitespaces = function( isReject ) {
388 return function( node ) {
389 var isWhitespace;
390 if ( node && node.type == CKEDITOR.NODE_TEXT ) {
391 // Whitespace, as well as the Filling Char Sequence text node used in Webkit. (#9384, #13816)
392 isWhitespace = !CKEDITOR.tools.trim( node.getText() ) ||
393 CKEDITOR.env.webkit && node.getText() == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE;
394 }
395
396 return !!( isReject ^ isWhitespace );
397 };
398 };
399
400 /**
401 * Returns a function which checks whether the node is invisible in the WYSIWYG mode.
402 *
403 * @static
404 * @param {Boolean} [isReject=false]
405 * @returns {Function}
406 */
407 CKEDITOR.dom.walker.invisible = function( isReject ) {
408 var whitespace = CKEDITOR.dom.walker.whitespaces(),
409 // #12221 (Chrome) plus #11111 (Safari).
410 offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
411
412 return function( node ) {
413 var invisible;
414
415 if ( whitespace( node ) )
416 invisible = 1;
417 else {
418 // Visibility should be checked on element.
419 if ( node.type == CKEDITOR.NODE_TEXT )
420 node = node.getParent();
421
422 // Nodes that take no spaces in wysiwyg:
423 // 1. White-spaces but not including NBSP.
424 // 2. Empty inline elements, e.g. <b></b>.
425 // 3. <br> elements (bogus, surrounded by text) (#12423).
426 invisible = node.$.offsetWidth <= offsetWidth0;
427 }
428
429 return !!( isReject ^ invisible );
430 };
431 };
432
433 /**
434 * Returns a function which checks whether the node type is equal to the passed one.
435 *
436 * @static
437 * @param {Number} type
438 * @param {Boolean} [isReject=false]
439 * @returns {Function}
440 */
441 CKEDITOR.dom.walker.nodeType = function( type, isReject ) {
442 return function( node ) {
443 return !!( isReject ^ ( node.type == type ) );
444 };
445 };
446
447 /**
448 * Returns a function which checks whether the node is a bogus (filler) node from
449 * `contenteditable` element's point of view.
450 *
451 * @static
452 * @param {Boolean} [isReject=false]
453 * @returns {Function}
454 */
455 CKEDITOR.dom.walker.bogus = function( isReject ) {
456 function nonEmpty( node ) {
457 return !isWhitespaces( node ) && !isBookmark( node );
458 }
459
460 return function( node ) {
461 var isBogus = CKEDITOR.env.needsBrFiller ? node.is && node.is( 'br' ) : node.getText && tailNbspRegex.test( node.getText() );
462
463 if ( isBogus ) {
464 var parent = node.getParent(),
465 next = node.getNext( nonEmpty );
466
467 isBogus = parent.isBlockBoundary() && ( !next || next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() );
468 }
469
470 return !!( isReject ^ isBogus );
471 };
472 };
473
474 /**
475 * Returns a function which checks whether the node is a temporary element
476 * (element with the `data-cke-temp` attribute) or its child.
477 *
478 * @since 4.3
479 * @static
480 * @param {Boolean} [isReject=false] Whether to return `false` for the
481 * temporary element instead of `true` (default).
482 * @returns {Function}
483 */
484 CKEDITOR.dom.walker.temp = function( isReject ) {
485 return function( node ) {
486 if ( node.type != CKEDITOR.NODE_ELEMENT )
487 node = node.getParent();
488
489 var isTemp = node && node.hasAttribute( 'data-cke-temp' );
490
491 return !!( isReject ^ isTemp );
492 };
493 };
494
495 var tailNbspRegex = /^[\t\r\n ]*(?:&nbsp;|\xa0)$/,
496 isWhitespaces = CKEDITOR.dom.walker.whitespaces(),
497 isBookmark = CKEDITOR.dom.walker.bookmark(),
498 isTemp = CKEDITOR.dom.walker.temp(),
499 toSkip = function( node ) {
500 return isBookmark( node ) ||
501 isWhitespaces( node ) ||
502 node.type == CKEDITOR.NODE_ELEMENT && node.is( CKEDITOR.dtd.$inline ) && !node.is( CKEDITOR.dtd.$empty );
503 };
504
505 /**
506 * Returns a function which checks whether the node should be ignored in terms of "editability".
507 *
508 * This includes:
509 *
510 * * whitespaces (see {@link CKEDITOR.dom.walker#whitespaces}),
511 * * bookmarks (see {@link CKEDITOR.dom.walker#bookmark}),
512 * * temporary elements (see {@link CKEDITOR.dom.walker#temp}).
513 *
514 * @since 4.3
515 * @static
516 * @param {Boolean} [isReject=false] Whether to return `false` for the
517 * ignored element instead of `true` (default).
518 * @returns {Function}
519 */
520 CKEDITOR.dom.walker.ignored = function( isReject ) {
521 return function( node ) {
522 var isIgnored = isWhitespaces( node ) || isBookmark( node ) || isTemp( node );
523
524 return !!( isReject ^ isIgnored );
525 };
526 };
527
528 var isIgnored = CKEDITOR.dom.walker.ignored();
529
530 /**
531 * Returns a function which checks whether the node is empty.
532 *
533 * @since 4.5
534 * @static
535 * @param {Boolean} [isReject=false] Whether to return `false` for the
536 * ignored element instead of `true` (default).
537 * @returns {Function}
538 */
539 CKEDITOR.dom.walker.empty = function( isReject ) {
540 return function( node ) {
541 var i = 0,
542 l = node.getChildCount();
543
544 for ( ; i < l; ++i ) {
545 if ( !isIgnored( node.getChild( i ) ) ) {
546 return !!isReject;
547 }
548 }
549
550 return !isReject;
551 };
552 };
553
554 var isEmpty = CKEDITOR.dom.walker.empty();
555
556 function filterTextContainers( dtd ) {
557 var hash = {},
558 name;
559
560 for ( name in dtd ) {
561 if ( CKEDITOR.dtd[ name ][ '#' ] )
562 hash[ name ] = 1;
563 }
564 return hash;
565 }
566
567 /**
568 * A hash of element names which in browsers that {@link CKEDITOR.env#needsBrFiller do not need `<br>` fillers}
569 * can be selection containers despite being empty.
570 *
571 * @since 4.5
572 * @static
573 * @property {Object} validEmptyBlockContainers
574 */
575 var validEmptyBlocks = CKEDITOR.dom.walker.validEmptyBlockContainers = CKEDITOR.tools.extend(
576 filterTextContainers( CKEDITOR.dtd.$block ),
577 { caption: 1, td: 1, th: 1 }
578 );
579
580 function isEditable( node ) {
581 // Skip temporary elements, bookmarks and whitespaces.
582 if ( isIgnored( node ) )
583 return false;
584
585 if ( node.type == CKEDITOR.NODE_TEXT )
586 return true;
587
588 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
589 // All inline and non-editable elements are valid editable places.
590 // Note: the <hr> is currently the only element in CKEDITOR.dtd.$empty and CKEDITOR.dtd.$block,
591 // but generally speaking we need an intersection of these two sets.
592 // Note: non-editable block has to be treated differently (should be selected entirely).
593 if ( node.is( CKEDITOR.dtd.$inline ) || node.is( 'hr' ) || node.getAttribute( 'contenteditable' ) == 'false' )
594 return true;
595
596 // Empty blocks are editable on IE.
597 if ( !CKEDITOR.env.needsBrFiller && node.is( validEmptyBlocks ) && isEmpty( node ) )
598 return true;
599 }
600
601 // Skip all other nodes.
602 return false;
603 }
604
605 /**
606 * Returns a function which checks whether the node can be a container or a sibling
607 * of the selection end.
608 *
609 * This includes:
610 *
611 * * text nodes (but not whitespaces),
612 * * inline elements,
613 * * intersection of {@link CKEDITOR.dtd#$empty} and {@link CKEDITOR.dtd#$block} (currently
614 * it is only `<hr>`),
615 * * non-editable blocks (special case &mdash; such blocks cannot be containers nor
616 * siblings, they need to be selected entirely),
617 * * empty {@link #validEmptyBlockContainers blocks} which can contain text
618 * ({@link CKEDITOR.env#needsBrFiller old IEs only}).
619 *
620 * @since 4.3
621 * @static
622 * @param {Boolean} [isReject=false] Whether to return `false` for the
623 * ignored element instead of `true` (default).
624 * @returns {Function}
625 */
626 CKEDITOR.dom.walker.editable = function( isReject ) {
627 return function( node ) {
628 return !!( isReject ^ isEditable( node ) );
629 };
630 };
631
632 /**
633 * Checks if there is a filler node at the end of an element, and returns it.
634 *
635 * @member CKEDITOR.dom.element
636 * @returns {CKEDITOR.dom.node/Boolean} Bogus node or `false`.
637 */
638 CKEDITOR.dom.element.prototype.getBogus = function() {
639 // Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (#7070).
640 var tail = this;
641 do {
642 tail = tail.getPreviousSourceNode();
643 }
644 while ( toSkip( tail ) );
645
646 if ( tail && ( CKEDITOR.env.needsBrFiller ? tail.is && tail.is( 'br' ) : tail.getText && tailNbspRegex.test( tail.getText() ) ) )
647 return tail;
648
649 return false;
650 };
651
652} )();
diff --git a/sources/core/dom/window.js b/sources/core/dom/window.js
new file mode 100644
index 0000000..ceeaeff
--- /dev/null
+++ b/sources/core/dom/window.js
@@ -0,0 +1,95 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.dom.document} class, which
8 * represents a DOM document.
9 */
10
11/**
12 * Represents a DOM window.
13 *
14 * var document = new CKEDITOR.dom.window( window );
15 *
16 * @class
17 * @extends CKEDITOR.dom.domObject
18 * @constructor Creates a window class instance.
19 * @param {Object} domWindow A native DOM window.
20 */
21CKEDITOR.dom.window = function( domWindow ) {
22 CKEDITOR.dom.domObject.call( this, domWindow );
23};
24
25CKEDITOR.dom.window.prototype = new CKEDITOR.dom.domObject();
26
27CKEDITOR.tools.extend( CKEDITOR.dom.window.prototype, {
28 /**
29 * Moves the selection focus to this window.
30 *
31 * var win = new CKEDITOR.dom.window( window );
32 * win.focus();
33 */
34 focus: function() {
35 this.$.focus();
36 },
37
38 /**
39 * Gets the width and height of this window's viewable area.
40 *
41 * var win = new CKEDITOR.dom.window( window );
42 * var size = win.getViewPaneSize();
43 * alert( size.width );
44 * alert( size.height );
45 *
46 * @returns {Object} An object with the `width` and `height`
47 * properties containing the size.
48 */
49 getViewPaneSize: function() {
50 var doc = this.$.document,
51 stdMode = doc.compatMode == 'CSS1Compat';
52 return {
53 width: ( stdMode ? doc.documentElement.clientWidth : doc.body.clientWidth ) || 0,
54 height: ( stdMode ? doc.documentElement.clientHeight : doc.body.clientHeight ) || 0
55 };
56 },
57
58 /**
59 * Gets the current position of the window's scroll.
60 *
61 * var win = new CKEDITOR.dom.window( window );
62 * var pos = win.getScrollPosition();
63 * alert( pos.x );
64 * alert( pos.y );
65 *
66 * @returns {Object} An object with the `x` and `y` properties
67 * containing the scroll position.
68 */
69 getScrollPosition: function() {
70 var $ = this.$;
71
72 if ( 'pageXOffset' in $ ) {
73 return {
74 x: $.pageXOffset || 0,
75 y: $.pageYOffset || 0
76 };
77 } else {
78 var doc = $.document;
79 return {
80 x: doc.documentElement.scrollLeft || doc.body.scrollLeft || 0,
81 y: doc.documentElement.scrollTop || doc.body.scrollTop || 0
82 };
83 }
84 },
85
86 /**
87 * Gets the frame element containing this window context.
88 *
89 * @returns {CKEDITOR.dom.element} The frame element or `null` if not in a frame context.
90 */
91 getFrame: function() {
92 var iframe = this.$.frameElement;
93 return iframe ? new CKEDITOR.dom.element.get( iframe ) : null;
94 }
95} );
diff --git a/sources/core/dtd.js b/sources/core/dtd.js
new file mode 100644
index 0000000..6059d48
--- /dev/null
+++ b/sources/core/dtd.js
@@ -0,0 +1,349 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 0000000..c50ec7f
--- /dev/null
+++ b/sources/core/editable.js
@@ -0,0 +1,3266 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 var isNotWhitespace, isNotBookmark, isEmpty, isBogus, emptyParagraphRegexp,
8 insert, fixTableAfterContentsDeletion, fixListAfterContentsDelete, getHtmlFromRangeHelpers, extractHtmlFromRangeHelpers;
9
10 /**
11 * Editable class which provides all editing related activities by
12 * the `contenteditable` element, dynamically get attached to editor instance.
13 *
14 * @class CKEDITOR.editable
15 * @extends CKEDITOR.dom.element
16 */
17 CKEDITOR.editable = CKEDITOR.tools.createClass( {
18 base: CKEDITOR.dom.element,
19 /**
20 * The constructor only stores generic editable creation logic that is commonly shared among
21 * all different editable elements.
22 *
23 * @constructor Creates an editable class instance.
24 * @param {CKEDITOR.editor} editor The editor instance on which the editable operates.
25 * @param {HTMLElement/CKEDITOR.dom.element} element Any DOM element that was as the editor's
26 * editing container, e.g. it could be either an HTML element with the `contenteditable` attribute
27 * set to the `true` that handles WYSIWYG editing or a `<textarea>` element that handles source editing.
28 */
29 $: function( editor, element ) {
30 // Transform the element into a CKEDITOR.dom.element instance.
31 this.base( element.$ || element );
32
33 this.editor = editor;
34
35 /**
36 * Indicates the initialization status of the editable element. The following statuses are available:
37 *
38 * * **unloaded** &ndash; the initial state. The editable's instance was created but
39 * is not fully loaded (in particular it has no data).
40 * * **ready** &ndash; the editable is fully initialized. The `ready` status is set after
41 * the first {@link CKEDITOR.editor#method-setData} is called.
42 * * **detached** &ndash; the editable was detached.
43 *
44 * @since 4.3.3
45 * @readonly
46 * @property {String}
47 */
48 this.status = 'unloaded';
49
50 /**
51 * Indicates whether the editable element gained focus.
52 *
53 * @property {Boolean} hasFocus
54 */
55 this.hasFocus = false;
56
57 // The bootstrapping logic.
58 this.setup();
59 },
60
61 proto: {
62 focus: function() {
63
64 var active;
65
66 // [Webkit] When DOM focus is inside of nested contenteditable elements,
67 // apply focus on the main editable will compromise it's text selection.
68 if ( CKEDITOR.env.webkit && !this.hasFocus ) {
69 // Restore focus on element which we cached (on selectionCheck) as previously active.
70 active = this.editor._.previousActive || this.getDocument().getActive();
71 if ( this.contains( active ) ) {
72 active.focus();
73 return;
74 }
75 }
76
77 // [Edge] Starting from EdgeHTML 14.14393, it does not support `setActive`. We need to use focus which
78 // causes unexpected scroll. Store scrollTop value so it can be restored after focusing editor.
79 // Scroll only happens if the editor is focused for the first time. (#14825)
80 if ( CKEDITOR.env.edge && CKEDITOR.env.version > 14 && !this.hasFocus && this.getDocument().equals( CKEDITOR.document ) ) {
81 this.editor._.previousScrollTop = this.$.scrollTop;
82 }
83
84 // [IE] Use instead "setActive" method to focus the editable if it belongs to the host page document,
85 // to avoid bringing an unexpected scroll.
86 try {
87 if ( CKEDITOR.env.ie && !( CKEDITOR.env.edge && CKEDITOR.env.version > 14 ) && this.getDocument().equals( CKEDITOR.document ) ) {
88 this.$.setActive();
89 } else {
90 this.$.focus();
91 }
92 } catch ( e ) {
93 // IE throws unspecified error when focusing editable after closing dialog opened on nested editable.
94 if ( !CKEDITOR.env.ie )
95 throw e;
96 }
97
98 // Remedy if Safari doens't applies focus properly. (#279)
99 if ( CKEDITOR.env.safari && !this.isInline() ) {
100 active = CKEDITOR.document.getActive();
101 if ( !active.equals( this.getWindow().getFrame() ) )
102 this.getWindow().focus();
103
104 }
105 },
106
107 /**
108 * Overrides {@link CKEDITOR.dom.element#on} to have special `focus/blur` handling.
109 * The `focusin/focusout` events are used in IE to replace regular `focus/blur` events
110 * because we want to avoid the asynchronous nature of later ones.
111 */
112 on: function( name, fn ) {
113 var args = Array.prototype.slice.call( arguments, 0 );
114
115 if ( CKEDITOR.env.ie && ( /^focus|blur$/ ).exec( name ) ) {
116 name = name == 'focus' ? 'focusin' : 'focusout';
117
118 // The "focusin/focusout" events bubbled, e.g. If there are elements with layout
119 // they fire this event when clicking in to edit them but it must be ignored
120 // to allow edit their contents. (#4682)
121 fn = isNotBubbling( fn, this );
122 args[ 0 ] = name;
123 args[ 1 ] = fn;
124 }
125
126 return CKEDITOR.dom.element.prototype.on.apply( this, args );
127 },
128
129 /**
130 * Registers an event listener that needs to be removed when detaching this editable.
131 * This means that it will be automatically removed when {@link #detach} is executed,
132 * for example on {@link CKEDITOR.editor#setMode changing editor mode} or destroying editor.
133 *
134 * Except for `obj` all other arguments have the same meaning as in {@link CKEDITOR.event#on}.
135 *
136 * This method is strongly related to the {@link CKEDITOR.editor#contentDom} and
137 * {@link CKEDITOR.editor#contentDomUnload} events, because they are fired
138 * when an editable is being attached and detached. Therefore, this method is usually used
139 * in the following way:
140 *
141 * editor.on( 'contentDom', function() {
142 * var editable = editor.editable();
143 * editable.attachListener( editable, 'mousedown', function() {
144 * // ...
145 * } );
146 * } );
147 *
148 * This code will attach the `mousedown` listener every time a new editable is attached
149 * to the editor, which in classic (`iframe`-based) editor happens every time the
150 * data or the mode is set. This listener will also be removed when that editable is detached.
151 *
152 * It is also possible to attach a listener to another object (e.g. to a document).
153 *
154 * editor.on( 'contentDom', function() {
155 * editor.editable().attachListener( editor.document, 'mousedown', function() {
156 * // ...
157 * } );
158 * } );
159 *
160 * @param {CKEDITOR.event} obj The element/object to which the listener will be attached. Every object
161 * which inherits from {@link CKEDITOR.event} may be used including {@link CKEDITOR.dom.element},
162 * {@link CKEDITOR.dom.document}, and {@link CKEDITOR.editable}.
163 * @param {String} eventName The name of the event that will be listened to.
164 * @param {Function} listenerFunction The function listening to the
165 * event. A single {@link CKEDITOR.eventInfo} object instance
166 * containing all the event data is passed to this function.
167 * @param {Object} [scopeObj] The object used to scope the listener
168 * call (the `this` object). If omitted, the current object is used.
169 * @param {Object} [listenerData] Data to be sent as the
170 * {@link CKEDITOR.eventInfo#listenerData} when calling the listener.
171 * @param {Number} [priority=10] The listener priority. Lower priority
172 * listeners are called first. Listeners with the same priority
173 * value are called in the registration order.
174 * @returns {Object} An object containing the `removeListener`
175 * function that can be used to remove the listener at any time.
176 */
177 attachListener: function( obj /*, event, fn, scope, listenerData, priority*/ ) {
178 !this._.listeners && ( this._.listeners = [] );
179 // Register the listener.
180 var args = Array.prototype.slice.call( arguments, 1 ),
181 listener = obj.on.apply( obj, args );
182
183 this._.listeners.push( listener );
184
185 return listener;
186 },
187
188 /**
189 * Remove all event listeners registered from {@link #attachListener}.
190 */
191 clearListeners: function() {
192 var listeners = this._.listeners;
193 // Don't get broken by this.
194 try {
195 while ( listeners.length )
196 listeners.pop().removeListener();
197 } catch ( e ) {}
198 },
199
200 /**
201 * Restore all attribution changes made by {@link #changeAttr }.
202 */
203 restoreAttrs: function() {
204 var changes = this._.attrChanges, orgVal;
205 for ( var attr in changes ) {
206 if ( changes.hasOwnProperty( attr ) ) {
207 orgVal = changes[ attr ];
208 // Restore original attribute.
209 orgVal !== null ? this.setAttribute( attr, orgVal ) : this.removeAttribute( attr );
210 }
211 }
212 },
213
214 /**
215 * Adds a CSS class name to this editable that needs to be removed on detaching.
216 *
217 * @param {String} className The class name to be added.
218 * @see CKEDITOR.dom.element#addClass
219 */
220 attachClass: function( cls ) {
221 var classes = this.getCustomData( 'classes' );
222 if ( !this.hasClass( cls ) ) {
223 !classes && ( classes = [] ), classes.push( cls );
224 this.setCustomData( 'classes', classes );
225 this.addClass( cls );
226 }
227 },
228
229 /**
230 * Make an attribution change that would be reverted on editable detaching.
231 * @param {String} attr The attribute name to be changed.
232 * @param {String} val The value of specified attribute.
233 */
234 changeAttr: function( attr, val ) {
235 var orgVal = this.getAttribute( attr );
236 if ( val !== orgVal ) {
237 !this._.attrChanges && ( this._.attrChanges = {} );
238
239 // Saved the original attribute val.
240 if ( !( attr in this._.attrChanges ) )
241 this._.attrChanges[ attr ] = orgVal;
242
243 this.setAttribute( attr, val );
244 }
245 },
246
247 /**
248 * Low-level method for inserting text into the editable.
249 * See the {@link CKEDITOR.editor#method-insertText} method which is the editor-level API
250 * for this purpose.
251 *
252 * @param {String} text
253 */
254 insertText: function( text ) {
255 // Focus the editor before calling transformPlainTextToHtml. (#12726)
256 this.editor.focus();
257 this.insertHtml( this.transformPlainTextToHtml( text ), 'text' );
258 },
259
260 /**
261 * Transforms plain text to HTML based on current selection and {@link CKEDITOR.editor#activeEnterMode}.
262 *
263 * @since 4.5
264 * @param {String} text Text to transform.
265 * @returns {String} HTML generated from the text.
266 */
267 transformPlainTextToHtml: function( text ) {
268 var enterMode = this.editor.getSelection().getStartElement().hasAscendant( 'pre', true ) ?
269 CKEDITOR.ENTER_BR :
270 this.editor.activeEnterMode;
271
272 return CKEDITOR.tools.transformPlainTextToHtml( text, enterMode );
273 },
274
275 /**
276 * Low-level method for inserting HTML into the editable.
277 * See the {@link CKEDITOR.editor#method-insertHtml} method which is the editor-level API
278 * for this purpose.
279 *
280 * This method will insert HTML into the current selection or a given range. It also creates an undo snapshot,
281 * scrolls the viewport to the insertion and selects the range next to the inserted content.
282 * If you want to insert HTML without additional operations use {@link #method-insertHtmlIntoRange}.
283 *
284 * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
285 *
286 * @param {String} data The HTML to be inserted.
287 * @param {String} [mode='html'] See {@link CKEDITOR.editor#method-insertHtml}'s param.
288 * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range
289 * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case).
290 * Introduced in CKEditor 4.5.
291 */
292 insertHtml: function( data, mode, range ) {
293 var editor = this.editor;
294
295 editor.focus();
296 editor.fire( 'saveSnapshot' );
297
298 if ( !range ) {
299 // HTML insertion only considers the first range.
300 // Note: getRanges will be overwritten for tests since we want to test
301 // custom ranges and bypass native selections.
302 range = editor.getSelection().getRanges()[ 0 ];
303 }
304
305 // Default mode is 'html'.
306 insert( this, mode || 'html', data, range );
307
308 // Make the final range selection.
309 range.select();
310
311 afterInsert( this );
312
313 this.editor.fire( 'afterInsertHtml', {} );
314 },
315
316 /**
317 * Inserts HTML into the position in the editor determined by the range.
318 *
319 * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects inserted
320 * HTML. If you want to do it, use {@link #method-insertHtml}.
321 *
322 * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
323 *
324 * @since 4.5
325 * @param {String} data HTML code to be inserted into the editor.
326 * @param {CKEDITOR.dom.range} range The range as a place of insertion.
327 * @param {String} [mode='html'] Mode in which HTML will be inserted.
328 * See {@link CKEDITOR.editor#method-insertHtml}.
329 */
330 insertHtmlIntoRange: function( data, range, mode ) {
331 // Default mode is 'html'
332 insert( this, mode || 'html', data, range );
333
334 this.editor.fire( 'afterInsertHtml', { intoRange: range } );
335 },
336
337 /**
338 * Low-level method for inserting an element into the editable.
339 * See the {@link CKEDITOR.editor#method-insertElement} method which is the editor-level API
340 * for this purpose.
341 *
342 * This method will insert the element into the current selection or a given range. It also creates an undo
343 * snapshot, scrolls the viewport to the insertion and selects the range next to the inserted content.
344 * If you want to insert an element without additional operations use {@link #method-insertElementIntoRange}.
345 *
346 * @param {CKEDITOR.dom.element} element The element to insert.
347 * @param {CKEDITOR.dom.range} [range] If specified, the element will be inserted into the range
348 * instead of into the selection.
349 */
350 insertElement: function( element, range ) {
351 var editor = this.editor;
352
353 // Prepare for the insertion. For example - focus editor (#11848).
354 editor.focus();
355 editor.fire( 'saveSnapshot' );
356
357 var enterMode = editor.activeEnterMode,
358 selection = editor.getSelection(),
359 elementName = element.getName(),
360 isBlock = CKEDITOR.dtd.$block[ elementName ];
361
362 if ( !range ) {
363 range = selection.getRanges()[ 0 ];
364 }
365
366 // Insert element into first range only and ignore the rest (#11183).
367 if ( this.insertElementIntoRange( element, range ) ) {
368 range.moveToPosition( element, CKEDITOR.POSITION_AFTER_END );
369
370 // If we're inserting a block element, the new cursor position must be
371 // optimized. (#3100,#5436,#8950)
372 if ( isBlock ) {
373 // Find next, meaningful element.
374 var next = element.getNext( function( node ) {
375 return isNotEmpty( node ) && !isBogus( node );
376 } );
377
378 if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.is( CKEDITOR.dtd.$block ) ) {
379 // If the next one is a text block, move cursor to the start of it's content.
380 if ( next.getDtd()[ '#' ] )
381 range.moveToElementEditStart( next );
382 // Otherwise move cursor to the before end of the last element.
383 else
384 range.moveToElementEditEnd( element );
385 }
386 // Open a new line if the block is inserted at the end of parent.
387 else if ( !next && enterMode != CKEDITOR.ENTER_BR ) {
388 next = range.fixBlock( true, enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' );
389 range.moveToElementEditStart( next );
390 }
391 }
392 }
393
394 // Set up the correct selection.
395 selection.selectRanges( [ range ] );
396
397 afterInsert( this );
398 },
399
400 /**
401 * Alias for {@link #insertElement}.
402 *
403 * @deprecated
404 * @param {CKEDITOR.dom.element} element The element to be inserted.
405 */
406 insertElementIntoSelection: function( element ) {
407 this.insertElement( element );
408 },
409
410 /**
411 * Inserts an element into the position in the editor determined by the range.
412 *
413 * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects the inserted
414 * element. If you want to do it, use the {@link #method-insertElement} method.
415 *
416 * @param {CKEDITOR.dom.element} element The element to be inserted.
417 * @param {CKEDITOR.dom.range} range The range as a place of insertion.
418 * @returns {Boolean} Informs whether the insertion was successful.
419 */
420 insertElementIntoRange: function( element, range ) {
421 var editor = this.editor,
422 enterMode = editor.config.enterMode,
423 elementName = element.getName(),
424 isBlock = CKEDITOR.dtd.$block[ elementName ];
425
426 if ( range.checkReadOnly() )
427 return false;
428
429 // Remove the original contents, merge split nodes.
430 range.deleteContents( 1 );
431
432 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT ) {
433 // If range is placed in intermediate element (not td or th), we need to do three things:
434 // * fill emptied <td/th>s with if browser needs them,
435 // * remove empty text nodes so IE8 won't crash
436 // (http://dev.ckeditor.com/ticket/11183#comment:8),
437 // * fix structure and move range into the <td/th> element.
438 if ( range.startContainer.is( { tr: 1, table: 1, tbody: 1, thead: 1, tfoot: 1 } ) ) {
439 fixTableAfterContentsDeletion( range );
440 } else if ( range.startContainer.is( CKEDITOR.dtd.$list ) ) {
441 // Similarly there's a need for lists.
442 fixListAfterContentsDelete( range );
443 }
444 }
445
446 // If we're inserting a block at dtd-violated position, split
447 // the parent blocks until we reach blockLimit.
448 var current, dtd;
449
450 if ( isBlock ) {
451 while ( ( current = range.getCommonAncestor( 0, 1 ) ) &&
452 ( dtd = CKEDITOR.dtd[ current.getName() ] ) &&
453 !( dtd && dtd[ elementName ] ) ) {
454 // Split up inline elements.
455 if ( current.getName() in CKEDITOR.dtd.span )
456 range.splitElement( current );
457
458 // If we're in an empty block which indicate a new paragraph,
459 // simply replace it with the inserting block.(#3664)
460 else if ( range.checkStartOfBlock() && range.checkEndOfBlock() ) {
461 range.setStartBefore( current );
462 range.collapse( true );
463 current.remove();
464 } else {
465 range.splitBlock( enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p', editor.editable() );
466 }
467 }
468 }
469
470 // Insert the new node.
471 range.insertNode( element );
472
473 // Return true if insertion was successful.
474 return true;
475 },
476
477 /**
478 * @see CKEDITOR.editor#setData
479 */
480 setData: function( data, isSnapshot ) {
481 if ( !isSnapshot )
482 data = this.editor.dataProcessor.toHtml( data );
483
484 this.setHtml( data );
485 this.fixInitialSelection();
486
487 // Editable is ready after first setData.
488 if ( this.status == 'unloaded' )
489 this.status = 'ready';
490
491 this.editor.fire( 'dataReady' );
492 },
493
494 /**
495 * @see CKEDITOR.editor#getData
496 */
497 getData: function( isSnapshot ) {
498 var data = this.getHtml();
499
500 if ( !isSnapshot )
501 data = this.editor.dataProcessor.toDataFormat( data );
502
503 return data;
504 },
505
506 /**
507 * Changes the read-only state of this editable.
508 *
509 * @param {Boolean} isReadOnly
510 */
511 setReadOnly: function( isReadOnly ) {
512 this.setAttribute( 'contenteditable', !isReadOnly );
513 },
514
515 /**
516 * Detaches this editable object from the DOM (removes classes, listeners, etc.)
517 */
518 detach: function() {
519 // Cleanup the element.
520 this.removeClass( 'cke_editable' );
521
522 this.status = 'detached';
523
524 // Save the editor reference which will be lost after
525 // calling detach from super class.
526 var editor = this.editor;
527
528 this._.detach();
529
530 delete editor.document;
531 delete editor.window;
532 },
533
534 /**
535 * Checks if the editable is one of the host page elements, indicates
536 * an inline editing environment.
537 *
538 * @returns {Boolean}
539 */
540 isInline: function() {
541 return this.getDocument().equals( CKEDITOR.document );
542 },
543
544 /**
545 * Fixes the selection and focus which may be in incorrect state after
546 * editable's inner HTML was overwritten.
547 *
548 * If the editable did not have focus, then the selection will be fixed when the editable
549 * is focused for the first time. If the editable already had focus, then the selection will
550 * be fixed immediately.
551 *
552 * To understand the problem see:
553 *
554 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
555 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusafterundoing
556 * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/selectionafterfocusing
557 * * http://tests.ckeditor.dev:1030/tests/plugins/newpage/manual/selectionafternewpage
558 *
559 * @since 4.4.6
560 * @private
561 */
562 fixInitialSelection: function() {
563 var that = this;
564
565 // Deal with IE8- IEQM (the old MS selection) first.
566 if ( CKEDITOR.env.ie && ( CKEDITOR.env.version < 9 || CKEDITOR.env.quirks ) ) {
567 if ( this.hasFocus ) {
568 this.focus();
569 fixMSSelection();
570 }
571
572 return;
573 }
574
575 // If editable did not have focus, fix the selection when it is first focused.
576 if ( !this.hasFocus ) {
577 this.once( 'focus', function() {
578 fixSelection();
579 }, null, null, -999 );
580 // If editable had focus, fix the selection immediately.
581 } else {
582 this.focus();
583 fixSelection();
584 }
585
586 function fixSelection() {
587 var $doc = that.getDocument().$,
588 $sel = $doc.getSelection();
589
590 if ( requiresFix( $sel ) ) {
591 var range = new CKEDITOR.dom.range( that );
592 range.moveToElementEditStart( that );
593
594 var $range = $doc.createRange();
595 $range.setStart( range.startContainer.$, range.startOffset );
596 $range.collapse( true );
597
598 $sel.removeAllRanges();
599 $sel.addRange( $range );
600 }
601 }
602
603 function requiresFix( $sel ) {
604 // This condition covers most broken cases after setting data.
605 if ( $sel.anchorNode && $sel.anchorNode == that.$ ) {
606 return true;
607 }
608
609 // Fix for:
610 // http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
611 // (the inline editor TC)
612 if ( CKEDITOR.env.webkit ) {
613 var active = that.getDocument().getActive();
614 if ( active && active.equals( that ) && !$sel.anchorNode ) {
615 return true;
616 }
617 }
618 }
619
620 function fixMSSelection() {
621 var $doc = that.getDocument().$,
622 $sel = $doc.selection,
623 active = that.getDocument().getActive();
624
625 if ( $sel.type == 'None' && active.equals( that ) ) {
626 var range = new CKEDITOR.dom.range( that ),
627 parentElement,
628 $range = $doc.body.createTextRange();
629
630 range.moveToElementEditStart( that );
631
632 parentElement = range.startContainer;
633 if ( parentElement.type != CKEDITOR.NODE_ELEMENT ) {
634 parentElement = parentElement.getParent();
635 }
636
637 $range.moveToElementText( parentElement.$ );
638 $range.collapse( true );
639 $range.select();
640 }
641 }
642 },
643
644 /**
645 * The base of the {@link CKEDITOR.editor#getSelectedHtml} method.
646 *
647 * @since 4.5
648 * @method getHtmlFromRange
649 * @param {CKEDITOR.dom.range} range
650 * @returns {CKEDITOR.dom.documentFragment}
651 */
652 getHtmlFromRange: function( range ) {
653 // There's nothing to return if range is collapsed.
654 if ( range.collapsed )
655 return new CKEDITOR.dom.documentFragment( range.document );
656
657 // Info object passed between methods.
658 var that = {
659 doc: this.getDocument(),
660 // Leave original range object untouched.
661 range: range.clone()
662 };
663
664 getHtmlFromRangeHelpers.eol.detect( that, this );
665 getHtmlFromRangeHelpers.bogus.exclude( that );
666 getHtmlFromRangeHelpers.cell.shrink( that );
667
668 that.fragment = that.range.cloneContents();
669
670 getHtmlFromRangeHelpers.tree.rebuild( that, this );
671 getHtmlFromRangeHelpers.eol.fix( that, this );
672
673 return new CKEDITOR.dom.documentFragment( that.fragment.$ );
674 },
675
676 /**
677 * The base of the {@link CKEDITOR.editor#extractSelectedHtml} method.
678 *
679 * **Note:** The range is modified so it matches the desired selection after extraction
680 * even though the selection is not made.
681 *
682 * @since 4.5
683 * @param {CKEDITOR.dom.range} range
684 * @param {Boolean} [removeEmptyBlock=false] See {@link CKEDITOR.editor#extractSelectedHtml}'s parameter.
685 * Note that the range will not be modified if this parameter is set to `true`.
686 * @returns {CKEDITOR.dom.documentFragment} The extracted fragment of the editable content.
687 */
688 extractHtmlFromRange: function( range, removeEmptyBlock ) {
689 var helpers = extractHtmlFromRangeHelpers,
690 that = {
691 range: range,
692 doc: range.document
693 },
694 // Since it is quite hard to build a valid documentFragment
695 // out of extracted contents because DOM changes, let's mimic
696 // extracted HTML with #getHtmlFromRange. Yep. It's a hack.
697 extractedFragment = this.getHtmlFromRange( range );
698
699 // Collapsed range means that there's nothing to extract.
700 if ( range.collapsed ) {
701 range.optimize();
702 return extractedFragment;
703 }
704
705 // Include inline element if possible.
706 range.enlarge( CKEDITOR.ENLARGE_INLINE, 1 );
707
708 // This got to be done before bookmarks are created because purging
709 // depends on the position of the range at the boundaries of the table,
710 // usually distorted by bookmark spans.
711 helpers.table.detectPurge( that );
712
713 // We'll play with DOM, let's hold the position of the range.
714 that.bookmark = range.createBookmark();
715 // While bookmarked, make unaccessible, to make sure that none of the methods
716 // will try to use it (they should use that.bookmark).
717 // This is done because ranges get desynchronized with the DOM when more bookmarks
718 // is created (as for instance that.targetBookmark).
719 delete that.range;
720
721 // The range to be restored after extraction should be kept
722 // outside of the range, so it's not removed by range.extractContents.
723 var targetRange = this.editor.createRange();
724 targetRange.moveToPosition( that.bookmark.startNode, CKEDITOR.POSITION_BEFORE_START );
725 that.targetBookmark = targetRange.createBookmark();
726
727 // Execute content-specific detections.
728 helpers.list.detectMerge( that, this );
729 helpers.table.detectRanges( that, this );
730 helpers.block.detectMerge( that, this );
731
732 // Simply, do the job.
733 if ( that.tableContentsRanges ) {
734 helpers.table.deleteRanges( that );
735
736 // Done here only to remove bookmark's spans.
737 range.moveToBookmark( that.bookmark );
738 that.range = range;
739 } else {
740 // To use the range we need to restore the bookmark and make
741 // the range accessible again.
742 range.moveToBookmark( that.bookmark );
743 that.range = range;
744 range.extractContents( helpers.detectExtractMerge( that ) );
745 }
746
747 // Move working range to desired, pre-computed position.
748 range.moveToBookmark( that.targetBookmark );
749
750 // Make sure range is always anchored in an element. For consistency.
751 range.optimize();
752
753 // It my happen that the uncollapsed range which referred to a valid selection,
754 // will be placed in an uneditable location after being collapsed:
755 // <tr>[<td>x</td>]</tr> -> <tr>[]<td>x</td></tr> -> <tr><td>[]x</td></tr>
756 helpers.fixUneditableRangePosition( range );
757
758 // Execute content-specific post-extract routines.
759 helpers.list.merge( that, this );
760 helpers.table.purge( that, this );
761 helpers.block.merge( that, this );
762
763 // Remove empty block, duh!
764 if ( removeEmptyBlock ) {
765 var path = range.startPath();
766
767 // <p><b>^</b></p> is empty block.
768 if (
769 range.checkStartOfBlock() &&
770 range.checkEndOfBlock() &&
771 path.block &&
772 !range.root.equals( path.block ) &&
773 // Do not remove a block with bookmarks. (#13465)
774 !hasBookmarks( path.block ) ) {
775 range.moveToPosition( path.block, CKEDITOR.POSITION_BEFORE_START );
776 path.block.remove();
777 }
778 } else {
779 // Auto paragraph, if needed.
780 helpers.autoParagraph( this.editor, range );
781
782 // Let's have a bogus next to the caret, if needed.
783 if ( isEmpty( range.startContainer ) )
784 range.startContainer.appendBogus();
785 }
786
787 // Merge inline siblings if any around the caret.
788 range.startContainer.mergeSiblings();
789
790 return extractedFragment;
791 },
792
793 /**
794 * Editable element bootstrapping.
795 *
796 * @private
797 */
798 setup: function() {
799 var editor = this.editor;
800
801 // Handle the load/read of editor data/snapshot.
802 this.attachListener( editor, 'beforeGetData', function() {
803 var data = this.getData();
804
805 // Post processing html output of wysiwyg editable.
806 if ( !this.is( 'textarea' ) ) {
807 // Reset empty if the document contains only one empty paragraph.
808 if ( editor.config.ignoreEmptyParagraph !== false )
809 data = data.replace( emptyParagraphRegexp, function( match, lookback ) {
810 return lookback;
811 } );
812 }
813
814 editor.setData( data, null, 1 );
815 }, this );
816
817 this.attachListener( editor, 'getSnapshot', function( evt ) {
818 evt.data = this.getData( 1 );
819 }, this );
820
821 this.attachListener( editor, 'afterSetData', function() {
822 this.setData( editor.getData( 1 ) );
823 }, this );
824 this.attachListener( editor, 'loadSnapshot', function( evt ) {
825 this.setData( evt.data, 1 );
826 }, this );
827
828 // Delegate editor focus/blur to editable.
829 this.attachListener( editor, 'beforeFocus', function() {
830 var sel = editor.getSelection(),
831 ieSel = sel && sel.getNative();
832
833 // IE considers control-type element as separate
834 // focus host when selected, avoid destroying the
835 // selection in such case. (#5812) (#8949)
836 if ( ieSel && ieSel.type == 'Control' )
837 return;
838
839 this.focus();
840 }, this );
841
842 this.attachListener( editor, 'insertHtml', function( evt ) {
843 this.insertHtml( evt.data.dataValue, evt.data.mode, evt.data.range );
844 }, this );
845 this.attachListener( editor, 'insertElement', function( evt ) {
846 this.insertElement( evt.data );
847 }, this );
848 this.attachListener( editor, 'insertText', function( evt ) {
849 this.insertText( evt.data );
850 }, this );
851
852 // Update editable state.
853 this.setReadOnly( editor.readOnly );
854
855 // The editable class.
856 this.attachClass( 'cke_editable' );
857
858 // The element mode css class.
859 if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) {
860 this.attachClass( 'cke_editable_inline' );
861 } else if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ||
862 editor.elementMode == CKEDITOR.ELEMENT_MODE_APPENDTO ) {
863 this.attachClass( 'cke_editable_themed' );
864 }
865
866 this.attachClass( 'cke_contents_' + editor.config.contentsLangDirection );
867
868 // Setup editor keystroke handlers on this element.
869 var keystrokeHandler = editor.keystrokeHandler;
870
871 // If editor is read-only, then make sure that BACKSPACE key
872 // is blocked to prevent browser history navigation.
873 keystrokeHandler.blockedKeystrokes[ 8 ] = +editor.readOnly;
874
875 editor.keystrokeHandler.attach( this );
876
877 // Update focus states.
878 this.on( 'blur', function() {
879 this.hasFocus = false;
880 }, null, null, -1 );
881
882 this.on( 'focus', function() {
883 this.hasFocus = true;
884 }, null, null, -1 );
885
886 if ( CKEDITOR.env.webkit ) {
887 // [WebKit] Save scrollTop value so it can be used when restoring locked selection. (#14659)
888 this.on( 'scroll', function() {
889 editor._.previousScrollTop = editor.editable().$.scrollTop;
890 }, null, null, -1 );
891 }
892
893 // [Edge] This is the other part of the workaround for Edge which restores saved
894 // scrollTop value and removes listener which is not needed anymore. (#14825)
895 if ( CKEDITOR.env.edge && CKEDITOR.env.version > 14 ) {
896
897 var fixScrollOnFocus = function() {
898 var editable = editor.editable();
899
900 if ( editor._.previousScrollTop != null && editable.getDocument().equals( CKEDITOR.document ) ) {
901 editable.$.scrollTop = editor._.previousScrollTop;
902 editor._.previousScrollTop = null;
903 this.removeListener( 'scroll', fixScrollOnFocus );
904 }
905 };
906
907 this.on( 'scroll', fixScrollOnFocus );
908 }
909
910 // Register to focus manager.
911 editor.focusManager.add( this );
912
913 // Inherit the initial focus on editable element.
914 if ( this.equals( CKEDITOR.document.getActive() ) ) {
915 this.hasFocus = true;
916 // Pending until this editable has attached.
917 editor.once( 'contentDom', function() {
918 editor.focusManager.focus( this );
919 }, this );
920 }
921
922 // Apply tab index on demand, with original direction saved.
923 if ( this.isInline() ) {
924
925 // tabIndex of the editable is different than editor's one.
926 // Update the attribute of the editable.
927 this.changeAttr( 'tabindex', editor.tabIndex );
928 }
929
930 // The above is all we'll be doing for a <textarea> editable.
931 if ( this.is( 'textarea' ) )
932 return;
933
934 // The DOM document which the editing acts upon.
935 editor.document = this.getDocument();
936 editor.window = this.getWindow();
937
938 var doc = editor.document;
939
940 this.changeAttr( 'spellcheck', !editor.config.disableNativeSpellChecker );
941
942 // Apply contents direction on demand, with original direction saved.
943 var dir = editor.config.contentsLangDirection;
944 if ( this.getDirection( 1 ) != dir )
945 this.changeAttr( 'dir', dir );
946
947 // Create the content stylesheet for this document.
948 var styles = CKEDITOR.getCss();
949 if ( styles ) {
950 var head = doc.getHead(),
951 stylesElement = head.getCustomData( 'stylesheet' );
952
953 if ( !stylesElement ) {
954 var sheet = doc.appendStyleText( styles );
955 sheet = new CKEDITOR.dom.element( sheet.ownerNode || sheet.owningElement );
956 head.setCustomData( 'stylesheet', sheet );
957 sheet.data( 'cke-temp', 1 );
958 } else if ( styles != stylesElement.getText() ) {
959 CKEDITOR.env.ie && CKEDITOR.env.version < 9 ? stylesElement.$.styleSheet.cssText = styles : stylesElement.setText( styles );
960 }
961 }
962
963 // Update the stylesheet sharing count.
964 var ref = doc.getCustomData( 'stylesheet_ref' ) || 0;
965 doc.setCustomData( 'stylesheet_ref', ref + 1 );
966
967 // Pass this configuration to styles system.
968 this.setCustomData( 'cke_includeReadonly', !editor.config.disableReadonlyStyling );
969
970 // Prevent the browser opening read-only links. (#6032 & #10912)
971 this.attachListener( this, 'click', function( evt ) {
972 evt = evt.data;
973
974 var link = new CKEDITOR.dom.elementPath( evt.getTarget(), this ).contains( 'a' );
975
976 if ( link && evt.$.button != 2 && link.isReadOnly() )
977 evt.preventDefault();
978 } );
979
980 var backspaceOrDelete = { 8: 1, 46: 1 };
981
982 // Override keystrokes which should have deletion behavior
983 // on fully selected element . (#4047) (#7645)
984 this.attachListener( editor, 'key', function( evt ) {
985 if ( editor.readOnly )
986 return true;
987
988 // Use getKey directly in order to ignore modifiers.
989 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
990 var keyCode = evt.data.domEvent.getKey(),
991 isHandled;
992
993 // Backspace OR Delete.
994 if ( keyCode in backspaceOrDelete ) {
995 var sel = editor.getSelection(),
996 selected,
997 range = sel.getRanges()[ 0 ],
998 path = range.startPath(),
999 block,
1000 parent,
1001 next,
1002 rtl = keyCode == 8;
1003
1004 if (
1005 // [IE<11] Remove selected image/anchor/etc here to avoid going back in history. (#10055)
1006 ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 && ( selected = sel.getSelectedElement() ) ) ||
1007 // Remove the entire list/table on fully selected content. (#7645)
1008 ( selected = getSelectedTableList( sel ) ) ) {
1009 // Make undo snapshot.
1010 editor.fire( 'saveSnapshot' );
1011
1012 // Delete any element that 'hasLayout' (e.g. hr,table) in IE8 will
1013 // break up the selection, safely manage it here. (#4795)
1014 range.moveToPosition( selected, CKEDITOR.POSITION_BEFORE_START );
1015 // Remove the control manually.
1016 selected.remove();
1017 range.select();
1018
1019 editor.fire( 'saveSnapshot' );
1020
1021 isHandled = 1;
1022 } else if ( range.collapsed ) {
1023 // Handle the following special cases: (#6217)
1024 // 1. Del/Backspace key before/after table;
1025 // 2. Backspace Key after start of table.
1026 if ( ( block = path.block ) &&
1027 ( next = block[ rtl ? 'getPrevious' : 'getNext' ]( isNotWhitespace ) ) &&
1028 ( next.type == CKEDITOR.NODE_ELEMENT ) &&
1029 next.is( 'table' ) &&
1030 range[ rtl ? 'checkStartOfBlock' : 'checkEndOfBlock' ]() ) {
1031 editor.fire( 'saveSnapshot' );
1032
1033 // Remove the current empty block.
1034 if ( range[ rtl ? 'checkEndOfBlock' : 'checkStartOfBlock' ]() )
1035 block.remove();
1036
1037 // Move cursor to the beginning/end of table cell.
1038 range[ 'moveToElementEdit' + ( rtl ? 'End' : 'Start' ) ]( next );
1039 range.select();
1040
1041 editor.fire( 'saveSnapshot' );
1042
1043 isHandled = 1;
1044 }
1045 else if ( path.blockLimit && path.blockLimit.is( 'td' ) &&
1046 ( parent = path.blockLimit.getAscendant( 'table' ) ) &&
1047 range.checkBoundaryOfElement( parent, rtl ? CKEDITOR.START : CKEDITOR.END ) &&
1048 ( next = parent[ rtl ? 'getPrevious' : 'getNext' ]( isNotWhitespace ) ) ) {
1049 editor.fire( 'saveSnapshot' );
1050
1051 // Move cursor to the end of previous block.
1052 range[ 'moveToElementEdit' + ( rtl ? 'End' : 'Start' ) ]( next );
1053
1054 // Remove any previous empty block.
1055 if ( range.checkStartOfBlock() && range.checkEndOfBlock() )
1056 next.remove();
1057 else
1058 range.select();
1059
1060 editor.fire( 'saveSnapshot' );
1061
1062 isHandled = 1;
1063 }
1064 // BACKSPACE/DEL pressed at the start/end of table cell.
1065 else if ( ( parent = path.contains( [ 'td', 'th', 'caption' ] ) ) &&
1066 range.checkBoundaryOfElement( parent, rtl ? CKEDITOR.START : CKEDITOR.END ) ) {
1067 isHandled = 1;
1068 }
1069 }
1070
1071 }
1072
1073 return !isHandled;
1074 } );
1075
1076 // On IE>=11 we need to fill blockless editable with <br> if it was deleted.
1077 if ( editor.blockless && CKEDITOR.env.ie && CKEDITOR.env.needsBrFiller ) {
1078 this.attachListener( this, 'keyup', function( evt ) {
1079 if ( evt.data.getKeystroke() in backspaceOrDelete && !this.getFirst( isNotEmpty ) ) {
1080 this.appendBogus();
1081
1082 // Set the selection before bogus, because IE tends to put it after.
1083 var range = editor.createRange();
1084 range.moveToPosition( this, CKEDITOR.POSITION_AFTER_START );
1085 range.select();
1086 }
1087 } );
1088 }
1089
1090 this.attachListener( this, 'dblclick', function( evt ) {
1091 if ( editor.readOnly )
1092 return false;
1093
1094 var data = { element: evt.data.getTarget() };
1095 editor.fire( 'doubleclick', data );
1096 } );
1097
1098 // Prevent automatic submission in IE #6336
1099 CKEDITOR.env.ie && this.attachListener( this, 'click', blockInputClick );
1100
1101 // Gecko/Webkit need some help when selecting control type elements. (#3448)
1102 // We apply same behavior for IE Edge. (#13386)
1103 if ( !CKEDITOR.env.ie || CKEDITOR.env.edge ) {
1104 this.attachListener( this, 'mousedown', function( ev ) {
1105 var control = ev.data.getTarget();
1106 // #11727. Note: htmlDP assures that input/textarea/select have contenteditable=false
1107 // attributes. However, they also have data-cke-editable attribute, so isReadOnly() returns false,
1108 // and therefore those elements are correctly selected by this code.
1109 if ( control.is( 'img', 'hr', 'input', 'textarea', 'select' ) && !control.isReadOnly() ) {
1110 editor.getSelection().selectElement( control );
1111
1112 // Prevent focus from stealing from the editable. (#9515)
1113 if ( control.is( 'input', 'textarea', 'select' ) )
1114 ev.data.preventDefault();
1115 }
1116 } );
1117 }
1118
1119 // For some reason, after click event is done, IE Edge loses focus on the selected element. (#13386)
1120 if ( CKEDITOR.env.edge ) {
1121 this.attachListener( this, 'mouseup', function( ev ) {
1122 var selectedElement = ev.data.getTarget();
1123 if ( selectedElement && selectedElement.is( 'img' ) ) {
1124 editor.getSelection().selectElement( selectedElement );
1125 }
1126 } );
1127 }
1128
1129 // Prevent right click from selecting an empty block even
1130 // when selection is anchored inside it. (#5845)
1131 if ( CKEDITOR.env.gecko ) {
1132 this.attachListener( this, 'mouseup', function( ev ) {
1133 if ( ev.data.$.button == 2 ) {
1134 var target = ev.data.getTarget();
1135
1136 if ( !target.getOuterHtml().replace( emptyParagraphRegexp, '' ) ) {
1137 var range = editor.createRange();
1138 range.moveToElementEditStart( target );
1139 range.select( true );
1140 }
1141 }
1142 } );
1143 }
1144
1145 // Webkit: avoid from editing form control elements content.
1146 if ( CKEDITOR.env.webkit ) {
1147 // Prevent from tick checkbox/radiobox/select
1148 this.attachListener( this, 'click', function( ev ) {
1149 if ( ev.data.getTarget().is( 'input', 'select' ) )
1150 ev.data.preventDefault();
1151 } );
1152
1153 // Prevent from editig textfield/textarea value.
1154 this.attachListener( this, 'mouseup', function( ev ) {
1155 if ( ev.data.getTarget().is( 'input', 'textarea' ) )
1156 ev.data.preventDefault();
1157 } );
1158 }
1159
1160 // Prevent Webkit/Blink from going rogue when joining
1161 // blocks on BACKSPACE/DEL (#11861,#9998).
1162 if ( CKEDITOR.env.webkit ) {
1163 this.attachListener( editor, 'key', function( evt ) {
1164 if ( editor.readOnly ) {
1165 return true;
1166 }
1167
1168 // Use getKey directly in order to ignore modifiers.
1169 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
1170 var key = evt.data.domEvent.getKey();
1171
1172 if ( !( key in backspaceOrDelete ) )
1173 return;
1174
1175 var backspace = key == 8,
1176 range = editor.getSelection().getRanges()[ 0 ],
1177 startPath = range.startPath();
1178
1179 if ( range.collapsed ) {
1180 if ( !mergeBlocksCollapsedSelection( editor, range, backspace, startPath ) )
1181 return;
1182 } else {
1183 if ( !mergeBlocksNonCollapsedSelection( editor, range, startPath ) )
1184 return;
1185 }
1186
1187 // Scroll to the new position of the caret (#11960).
1188 editor.getSelection().scrollIntoView();
1189 editor.fire( 'saveSnapshot' );
1190
1191 return false;
1192 }, this, null, 100 ); // Later is better – do not override existing listeners.
1193 }
1194 }
1195 },
1196
1197 _: {
1198 detach: function() {
1199 // Update the editor cached data with current data.
1200 this.editor.setData( this.editor.getData(), 0, 1 );
1201
1202 this.clearListeners();
1203 this.restoreAttrs();
1204
1205 // Cleanup our custom classes.
1206 var classes;
1207 if ( ( classes = this.removeCustomData( 'classes' ) ) ) {
1208 while ( classes.length )
1209 this.removeClass( classes.pop() );
1210 }
1211
1212 // Remove contents stylesheet from document if it's the last usage.
1213 if ( !this.is( 'textarea' ) ) {
1214 var doc = this.getDocument(),
1215 head = doc.getHead();
1216 if ( head.getCustomData( 'stylesheet' ) ) {
1217 var refs = doc.getCustomData( 'stylesheet_ref' );
1218 if ( !( --refs ) ) {
1219 doc.removeCustomData( 'stylesheet_ref' );
1220 var sheet = head.removeCustomData( 'stylesheet' );
1221 sheet.remove();
1222 } else {
1223 doc.setCustomData( 'stylesheet_ref', refs );
1224 }
1225 }
1226 }
1227
1228 this.editor.fire( 'contentDomUnload' );
1229
1230 // Free up the editor reference.
1231 delete this.editor;
1232 }
1233 }
1234 } );
1235
1236 /**
1237 * Creates, retrieves or detaches an editable element of the editor.
1238 * This method should always be used instead of calling {@link CKEDITOR.editable} directly.
1239 *
1240 * @method editable
1241 * @member CKEDITOR.editor
1242 * @param {CKEDITOR.dom.element/CKEDITOR.editable} [elementOrEditable] The
1243 * DOM element to become the editable or a {@link CKEDITOR.editable} object.
1244 */
1245 CKEDITOR.editor.prototype.editable = function( element ) {
1246 var editable = this._.editable;
1247
1248 // This editor has already associated with
1249 // an editable element, silently fails.
1250 if ( editable && element )
1251 return 0;
1252
1253 if ( arguments.length ) {
1254 editable = this._.editable = element ? ( element instanceof CKEDITOR.editable ? element : new CKEDITOR.editable( this, element ) ) :
1255 // Detach the editable from editor.
1256 ( editable && editable.detach(), null );
1257 }
1258
1259 // Just retrieve the editable.
1260 return editable;
1261 };
1262
1263 CKEDITOR.on( 'instanceLoaded', function( evt ) {
1264 var editor = evt.editor;
1265
1266 // and flag that the element was locked by our code so it'll be editable by the editor functions (#6046).
1267 editor.on( 'insertElement', function( evt ) {
1268 var element = evt.data;
1269 if ( element.type == CKEDITOR.NODE_ELEMENT && ( element.is( 'input' ) || element.is( 'textarea' ) ) ) {
1270 // // The element is still not inserted yet, force attribute-based check.
1271 if ( element.getAttribute( 'contentEditable' ) != 'false' )
1272 element.data( 'cke-editable', element.hasAttribute( 'contenteditable' ) ? 'true' : '1' );
1273 element.setAttribute( 'contentEditable', false );
1274 }
1275 } );
1276
1277 editor.on( 'selectionChange', function( evt ) {
1278 if ( editor.readOnly )
1279 return;
1280
1281 // Auto fixing on some document structure weakness to enhance usabilities. (#3190 and #3189)
1282 var sel = editor.getSelection();
1283 // Do it only when selection is not locked. (#8222)
1284 if ( sel && !sel.isLocked ) {
1285 var isDirty = editor.checkDirty();
1286
1287 // Lock undoM before touching DOM to prevent
1288 // recording these changes as separate snapshot.
1289 editor.fire( 'lockSnapshot' );
1290 fixDom( evt );
1291 editor.fire( 'unlockSnapshot' );
1292
1293 !isDirty && editor.resetDirty();
1294 }
1295 } );
1296 } );
1297
1298 CKEDITOR.on( 'instanceCreated', function( evt ) {
1299 var editor = evt.editor;
1300
1301 editor.on( 'mode', function() {
1302
1303 var editable = editor.editable();
1304
1305 // Setup proper ARIA roles and properties for inline editable, classic
1306 // (iframe-based) editable is instead handled by plugin.
1307 if ( editable && editable.isInline() ) {
1308
1309 var ariaLabel = editor.title;
1310
1311 editable.changeAttr( 'role', 'textbox' );
1312 editable.changeAttr( 'aria-label', ariaLabel );
1313
1314 if ( ariaLabel )
1315 editable.changeAttr( 'title', ariaLabel );
1316
1317 var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
1318 if ( helpLabel ) {
1319 // Put the voice label in different spaces, depending on element mode, so
1320 // the DOM element get auto detached on mode reload or editor destroy.
1321 var ct = this.ui.space( this.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ? 'top' : 'contents' );
1322 if ( ct ) {
1323 var ariaDescId = CKEDITOR.tools.getNextId(),
1324 desc = CKEDITOR.dom.element.createFromHtml( '<span id="' + ariaDescId + '" class="cke_voice_label">' + helpLabel + '</span>' );
1325 ct.append( desc );
1326 editable.changeAttr( 'aria-describedby', ariaDescId );
1327 }
1328 }
1329 }
1330 } );
1331 } );
1332
1333 // #9222: Show text cursor in Gecko.
1334 // Show default cursor over control elements on all non-IEs.
1335 CKEDITOR.addCss( '.cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}' );
1336
1337 //
1338 //
1339 // Bazillion helpers for the editable class and above listeners.
1340 //
1341 //
1342
1343 isNotWhitespace = CKEDITOR.dom.walker.whitespaces( true ),
1344 isNotBookmark = CKEDITOR.dom.walker.bookmark( false, true ),
1345 isEmpty = CKEDITOR.dom.walker.empty(),
1346 isBogus = CKEDITOR.dom.walker.bogus(),
1347 // Matching an empty paragraph at the end of document.
1348 emptyParagraphRegexp = /(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>|&nbsp;|\u00A0|&#160;)?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi;
1349
1350 // Auto-fixing block-less content by wrapping paragraph (#3190), prevent
1351 // non-exitable-block by padding extra br.(#3189)
1352 // Returns truly value when dom was changed, falsy otherwise.
1353 function fixDom( evt ) {
1354 var editor = evt.editor,
1355 path = evt.data.path,
1356 blockLimit = path.blockLimit,
1357 selection = evt.data.selection,
1358 range = selection.getRanges()[ 0 ],
1359 selectionUpdateNeeded;
1360
1361 if ( CKEDITOR.env.gecko || ( CKEDITOR.env.ie && CKEDITOR.env.needsBrFiller ) ) {
1362 var blockNeedsFiller = needsBrFiller( selection, path );
1363 if ( blockNeedsFiller ) {
1364 blockNeedsFiller.appendBogus();
1365 // IE tends to place selection after appended bogus, so we need to
1366 // select the original range (placed before bogus).
1367 selectionUpdateNeeded = CKEDITOR.env.ie;
1368 }
1369 }
1370
1371 // When we're in block enter mode, a new paragraph will be established
1372 // to encapsulate inline contents inside editable. (#3657)
1373 // Don't autoparagraph if browser (namely - IE) incorrectly anchored selection
1374 // inside non-editable content. This happens e.g. if non-editable block is the only
1375 // content of editable.
1376 if ( shouldAutoParagraph( editor, path.block, blockLimit ) && range.collapsed && !range.getCommonAncestor().isReadOnly() ) {
1377 var testRng = range.clone();
1378 testRng.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
1379 var walker = new CKEDITOR.dom.walker( testRng );
1380 walker.guard = function( node ) {
1381 return !isNotEmpty( node ) ||
1382 node.type == CKEDITOR.NODE_COMMENT ||
1383 node.isReadOnly();
1384 };
1385
1386 // 1. Inline content discovered under cursor;
1387 // 2. Empty editable.
1388 if ( !walker.checkForward() || testRng.checkStartOfBlock() && testRng.checkEndOfBlock() ) {
1389 var fixedBlock = range.fixBlock( true, editor.activeEnterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' );
1390
1391 // For IE<11, we should remove any filler node which was introduced before.
1392 if ( !CKEDITOR.env.needsBrFiller ) {
1393 var first = fixedBlock.getFirst( isNotEmpty );
1394 if ( first && isNbsp( first ) )
1395 first.remove();
1396 }
1397
1398 selectionUpdateNeeded = 1;
1399
1400 // Cancel this selection change in favor of the next (correct). (#6811)
1401 evt.cancel();
1402 }
1403 }
1404
1405 if ( selectionUpdateNeeded )
1406 range.select();
1407 }
1408
1409 // Checks whether current selection requires br filler to be appended.
1410 // @returns Block which needs filler or falsy value.
1411 function needsBrFiller( selection, path ) {
1412 // Fake selection does not need filler, because it is fake.
1413 if ( selection.isFake )
1414 return 0;
1415
1416 // Ensure bogus br could help to move cursor (out of styles) to the end of block. (#7041)
1417 var pathBlock = path.block || path.blockLimit,
1418 lastNode = pathBlock && pathBlock.getLast( isNotEmpty );
1419
1420 // Check some specialities of the current path block:
1421 // 1. It is really displayed as block; (#7221)
1422 // 2. It doesn't end with one inner block; (#7467)
1423 // 3. It doesn't have bogus br yet.
1424 if (
1425 pathBlock && pathBlock.isBlockBoundary() &&
1426 !( lastNode && lastNode.type == CKEDITOR.NODE_ELEMENT && lastNode.isBlockBoundary() ) &&
1427 !pathBlock.is( 'pre' ) && !pathBlock.getBogus()
1428 )
1429 return pathBlock;
1430 }
1431
1432 function blockInputClick( evt ) {
1433 var element = evt.data.getTarget();
1434 if ( element.is( 'input' ) ) {
1435 var type = element.getAttribute( 'type' );
1436 if ( type == 'submit' || type == 'reset' )
1437 evt.data.preventDefault();
1438 }
1439 }
1440
1441 function isNotEmpty( node ) {
1442 return isNotWhitespace( node ) && isNotBookmark( node );
1443 }
1444
1445 function isNbsp( node ) {
1446 return node.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( node.getText() ).match( /^(?:&nbsp;|\xa0)$/ );
1447 }
1448
1449 function isNotBubbling( fn, src ) {
1450 return function( evt ) {
1451 var other = evt.data.$.toElement || evt.data.$.fromElement || evt.data.$.relatedTarget;
1452
1453 // First of all, other may simply be null/undefined.
1454 // Second of all, at least early versions of Spartan returned empty objects from evt.relatedTarget,
1455 // so let's also check the node type.
1456 other = ( other && other.nodeType == CKEDITOR.NODE_ELEMENT ) ? new CKEDITOR.dom.element( other ) : null;
1457
1458 if ( !( other && ( src.equals( other ) || src.contains( other ) ) ) )
1459 fn.call( this, evt );
1460 };
1461 }
1462
1463 function hasBookmarks( element ) {
1464 // We use getElementsByTag() instead of find() to retain compatibility with IE quirks mode.
1465 var potentialBookmarks = element.getElementsByTag( 'span' ),
1466 i = 0,
1467 child;
1468
1469 if ( potentialBookmarks ) {
1470 while ( ( child = potentialBookmarks.getItem( i++ ) ) ) {
1471 if ( !isNotBookmark( child ) ) {
1472 return true;
1473 }
1474 }
1475 }
1476
1477 return false;
1478 }
1479
1480 // Check if the entire table/list contents is selected.
1481 function getSelectedTableList( sel ) {
1482 var selected,
1483 range = sel.getRanges()[ 0 ],
1484 editable = sel.root,
1485 path = range.startPath(),
1486 structural = { table: 1, ul: 1, ol: 1, dl: 1 };
1487
1488 if ( path.contains( structural ) ) {
1489 // Clone the original range.
1490 var walkerRng = range.clone();
1491
1492 // Enlarge the range: X<ul><li>[Y]</li></ul>X => [X<ul><li>]Y</li></ul>X
1493 walkerRng.collapse( 1 );
1494 walkerRng.setStartAt( editable, CKEDITOR.POSITION_AFTER_START );
1495
1496 // Create a new walker.
1497 var walker = new CKEDITOR.dom.walker( walkerRng );
1498
1499 // Assign a new guard to the walker.
1500 walker.guard = guard();
1501
1502 // Go backwards checking for selected structural node.
1503 walker.checkBackward();
1504
1505 // If there's a selected structured element when checking backwards,
1506 // then check the same forwards.
1507 if ( selected ) {
1508 // Clone the original range.
1509 walkerRng = range.clone();
1510
1511 // Enlarge the range (assuming <ul> is selected element from guard):
1512 //
1513 // X<ul><li>[Y]</li></ul>X => X<ul><li>Y[</li></ul>]X
1514 //
1515 // If the walker went deeper down DOM than a while ago when traversing
1516 // backwards, then it doesn't make sense: an element must be selected
1517 // symmetrically. By placing range end **after previously selected node**,
1518 // we make sure we don't go no deeper in DOM when going forwards.
1519 walkerRng.collapse();
1520 walkerRng.setEndAt( selected, CKEDITOR.POSITION_AFTER_END );
1521
1522 // Create a new walker.
1523 walker = new CKEDITOR.dom.walker( walkerRng );
1524
1525 // Assign a new guard to the walker.
1526 walker.guard = guard( true );
1527
1528 // Reset selected node.
1529 selected = false;
1530
1531 // Go forwards checking for selected structural node.
1532 walker.checkForward();
1533
1534 return selected;
1535 }
1536 }
1537
1538 return null;
1539
1540 function guard( forwardGuard ) {
1541 return function( node, isWalkOut ) {
1542 // Save the encountered node as selected if going down the DOM structure
1543 // and the node is structured element.
1544 if ( isWalkOut && node.type == CKEDITOR.NODE_ELEMENT && node.is( structural ) )
1545 selected = node;
1546
1547 // Stop the walker when either traversing another non-empty node at the same
1548 // DOM level as in previous step.
1549 // NOTE: When going forwards, stop if encountered a bogus.
1550 if ( !isWalkOut && isNotEmpty( node ) && !( forwardGuard && isBogus( node ) ) )
1551 return false;
1552 };
1553 }
1554 }
1555
1556 // Whether in given context (pathBlock, pathBlockLimit and editor settings)
1557 // editor should automatically wrap inline contents with blocks.
1558 function shouldAutoParagraph( editor, pathBlock, pathBlockLimit ) {
1559 // Check whether pathBlock equals pathBlockLimit to support nested editable (#12162).
1560 return editor.config.autoParagraph !== false &&
1561 editor.activeEnterMode != CKEDITOR.ENTER_BR &&
1562 (
1563 ( editor.editable().equals( pathBlockLimit ) && !pathBlock ) ||
1564 ( pathBlock && pathBlock.getAttribute( 'contenteditable' ) == 'true' )
1565 );
1566 }
1567
1568 function autoParagraphTag( editor ) {
1569 return ( editor.activeEnterMode != CKEDITOR.ENTER_BR && editor.config.autoParagraph !== false ) ? editor.activeEnterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' : false;
1570 }
1571
1572 //
1573 // Functions related to insertXXX methods
1574 //
1575 insert = ( function() {
1576 'use strict';
1577
1578 var DTD = CKEDITOR.dtd;
1579
1580 // Inserts the given (valid) HTML into the range position (with range content deleted),
1581 // guarantee it's result to be a valid DOM tree.
1582 function insert( editable, type, data, range ) {
1583 var editor = editable.editor,
1584 dontFilter = false;
1585
1586 if ( type == 'unfiltered_html' ) {
1587 type = 'html';
1588 dontFilter = true;
1589 }
1590
1591 // Check range spans in non-editable.
1592 if ( range.checkReadOnly() )
1593 return;
1594
1595 // RANGE PREPARATIONS
1596
1597 var path = new CKEDITOR.dom.elementPath( range.startContainer, range.root ),
1598 // Let root be the nearest block that's impossible to be split
1599 // during html processing.
1600 blockLimit = path.blockLimit || range.root,
1601 // The "state" value.
1602 that = {
1603 type: type,
1604 dontFilter: dontFilter,
1605 editable: editable,
1606 editor: editor,
1607 range: range,
1608 blockLimit: blockLimit,
1609 // During pre-processing / preparations startContainer of affectedRange should be placed
1610 // in this element in which inserted or moved (in case when we merge blocks) content
1611 // could create situation that will need merging inline elements.
1612 // Examples:
1613 // <div><b>A</b>^B</div> + <b>C</b> => <div><b>A</b><b>C</b>B</div> - affected container is <div>.
1614 // <p><b>A[B</b></p><p><b>C]D</b></p> + E => <p><b>AE</b></p><p><b>D</b></p> =>
1615 // <p><b>AE</b><b>D</b></p> - affected container is <p> (in text mode).
1616 mergeCandidates: [],
1617 zombies: []
1618 };
1619
1620 prepareRangeToDataInsertion( that );
1621
1622 // DATA PROCESSING
1623
1624 // Select range and stop execution.
1625 // If data has been totally emptied after the filtering,
1626 // any insertion is pointless (#10339).
1627 if ( data && processDataForInsertion( that, data ) ) {
1628 // DATA INSERTION
1629 insertDataIntoRange( that );
1630 }
1631
1632 // FINAL CLEANUP
1633 // Set final range position and clean up.
1634
1635 cleanupAfterInsertion( that );
1636 }
1637
1638 // Prepare range to its data deletion.
1639 // Delete its contents.
1640 // Prepare it to insertion.
1641 function prepareRangeToDataInsertion( that ) {
1642 var range = that.range,
1643 mergeCandidates = that.mergeCandidates,
1644 node, marker, path, startPath, endPath, previous, bm;
1645
1646 // If range starts in inline element then insert a marker, so empty
1647 // inline elements won't be removed while range.deleteContents
1648 // and we will be able to move range back into this element.
1649 // E.g. 'aa<b>[bb</b>]cc' -> (after deleting) 'aa<b><span/></b>cc'
1650 if ( that.type == 'text' && range.shrink( CKEDITOR.SHRINK_ELEMENT, true, false ) ) {
1651 marker = CKEDITOR.dom.element.createFromHtml( '<span>&nbsp;</span>', range.document );
1652 range.insertNode( marker );
1653 range.setStartAfter( marker );
1654 }
1655
1656 // By using path we can recover in which element was startContainer
1657 // before deleting contents.
1658 // Start and endPathElements will be used to squash selected blocks, after removing
1659 // selection contents. See rule 5.
1660 startPath = new CKEDITOR.dom.elementPath( range.startContainer );
1661 that.endPath = endPath = new CKEDITOR.dom.elementPath( range.endContainer );
1662
1663 if ( !range.collapsed ) {
1664 // Anticipate the possibly empty block at the end of range after deletion.
1665 node = endPath.block || endPath.blockLimit;
1666 var ancestor = range.getCommonAncestor();
1667 if ( node && !( node.equals( ancestor ) || node.contains( ancestor ) ) && range.checkEndOfBlock() ) {
1668 that.zombies.push( node );
1669 }
1670
1671 range.deleteContents();
1672 }
1673
1674 // Rule 4.
1675 // Move range into the previous block.
1676 while (
1677 ( previous = getRangePrevious( range ) ) && checkIfElement( previous ) && previous.isBlockBoundary() &&
1678 // Check if previousNode was parent of range's startContainer before deleteContents.
1679 startPath.contains( previous )
1680 )
1681 range.moveToPosition( previous, CKEDITOR.POSITION_BEFORE_END );
1682
1683 // Rule 5.
1684 mergeAncestorElementsOfSelectionEnds( range, that.blockLimit, startPath, endPath );
1685
1686 // Rule 1.
1687 if ( marker ) {
1688 // If marker was created then move collapsed range into its place.
1689 range.setEndBefore( marker );
1690 range.collapse();
1691 marker.remove();
1692 }
1693
1694 // Split inline elements so HTML will be inserted with its own styles.
1695 path = range.startPath();
1696 if ( ( node = path.contains( isInline, false, 1 ) ) ) {
1697 range.splitElement( node );
1698 that.inlineStylesRoot = node;
1699 that.inlineStylesPeak = path.lastElement;
1700 }
1701
1702 // Record inline merging candidates for later cleanup in place.
1703 bm = range.createBookmark();
1704
1705 // 1. Inline siblings.
1706 node = bm.startNode.getPrevious( isNotEmpty );
1707 node && checkIfElement( node ) && isInline( node ) && mergeCandidates.push( node );
1708 node = bm.startNode.getNext( isNotEmpty );
1709 node && checkIfElement( node ) && isInline( node ) && mergeCandidates.push( node );
1710
1711 // 2. Inline parents.
1712 node = bm.startNode;
1713 while ( ( node = node.getParent() ) && isInline( node ) )
1714 mergeCandidates.push( node );
1715
1716 range.moveToBookmark( bm );
1717 }
1718
1719 function processDataForInsertion( that, data ) {
1720 var range = that.range;
1721
1722 // Rule 8. - wrap entire data in inline styles.
1723 // (e.g. <p><b>x^z</b></p> + <p>a</p><p>b</p> -> <b><p>a</p><p>b</p></b>)
1724 // Incorrect tags order will be fixed by htmlDataProcessor.
1725 if ( that.type == 'text' && that.inlineStylesRoot )
1726 data = wrapDataWithInlineStyles( data, that );
1727
1728
1729 var context = that.blockLimit.getName();
1730
1731 // Wrap data to be inserted, to avoid losing leading whitespaces
1732 // when going through the below procedure.
1733 if ( /^\s+|\s+$/.test( data ) && 'span' in CKEDITOR.dtd[ context ] ) {
1734 var protect = '<span data-cke-marker="1">&nbsp;</span>';
1735 data = protect + data + protect;
1736 }
1737
1738 // Process the inserted html, in context of the insertion root.
1739 // Don't use the "fix for body" feature as auto paragraphing must
1740 // be handled during insertion.
1741 data = that.editor.dataProcessor.toHtml( data, {
1742 context: null,
1743 fixForBody: false,
1744 protectedWhitespaces: !!protect,
1745 dontFilter: that.dontFilter,
1746 // Use the current, contextual settings.
1747 filter: that.editor.activeFilter,
1748 enterMode: that.editor.activeEnterMode
1749 } );
1750
1751
1752 // Build the node list for insertion.
1753 var doc = range.document,
1754 wrapper = doc.createElement( 'body' );
1755
1756 wrapper.setHtml( data );
1757
1758 // Eventually remove the temporaries.
1759 if ( protect ) {
1760 wrapper.getFirst().remove();
1761 wrapper.getLast().remove();
1762 }
1763
1764 // Rule 7.
1765 var block = range.startPath().block;
1766 if ( block && // Apply when there exists path block after deleting selection's content...
1767 !( block.getChildCount() == 1 && block.getBogus() ) ) { // ... and the only content of this block isn't a bogus.
1768 stripBlockTagIfSingleLine( wrapper );
1769 }
1770
1771 that.dataWrapper = wrapper;
1772
1773 return data;
1774 }
1775
1776 function insertDataIntoRange( that ) {
1777 var range = that.range,
1778 doc = range.document,
1779 path,
1780 blockLimit = that.blockLimit,
1781 nodesData, nodeData, node,
1782 nodeIndex = 0,
1783 bogus,
1784 bogusNeededBlocks = [],
1785 pathBlock, fixBlock,
1786 splittingContainer = 0,
1787 dontMoveCaret = 0,
1788 insertionContainer, toSplit, newContainer,
1789 startContainer = range.startContainer,
1790 endContainer = that.endPath.elements[ 0 ],
1791 filteredNodes,
1792 // If endContainer was merged into startContainer: <p>a[b</p><p>c]d</p>
1793 // or it's equal to startContainer: <p>a^b</p>
1794 // or different situation happened :P
1795 // then there's no separate container for the end of selection.
1796 pos = endContainer.getPosition( startContainer ),
1797 separateEndContainer = !!endContainer.getCommonAncestor( startContainer ) && // endC is not detached.
1798 pos != CKEDITOR.POSITION_IDENTICAL && !( pos & CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_IS_CONTAINED ); // endC & endS are in separate branches.
1799
1800 nodesData = extractNodesData( that.dataWrapper, that );
1801
1802 removeBrsAdjacentToPastedBlocks( nodesData, range );
1803
1804 for ( ; nodeIndex < nodesData.length; nodeIndex++ ) {
1805 nodeData = nodesData[ nodeIndex ];
1806
1807 // Ignore trailing <brs>
1808 if ( nodeData.isLineBreak && splitOnLineBreak( range, blockLimit, nodeData ) ) {
1809 // Do not move caret towards the text (in cleanupAfterInsertion),
1810 // because caret was placed after a line break.
1811 dontMoveCaret = nodeIndex > 0;
1812 continue;
1813 }
1814
1815 path = range.startPath();
1816
1817 // Auto paragraphing.
1818 if ( !nodeData.isBlock && shouldAutoParagraph( that.editor, path.block, path.blockLimit ) && ( fixBlock = autoParagraphTag( that.editor ) ) ) {
1819 fixBlock = doc.createElement( fixBlock );
1820 fixBlock.appendBogus();
1821 range.insertNode( fixBlock );
1822 if ( CKEDITOR.env.needsBrFiller && ( bogus = fixBlock.getBogus() ) )
1823 bogus.remove();
1824 range.moveToPosition( fixBlock, CKEDITOR.POSITION_BEFORE_END );
1825 }
1826
1827 node = range.startPath().block;
1828
1829 // Remove any bogus element on the current path block for now, and mark
1830 // it for later compensation.
1831 if ( node && !node.equals( pathBlock ) ) {
1832 bogus = node.getBogus();
1833 if ( bogus ) {
1834 bogus.remove();
1835 bogusNeededBlocks.push( node );
1836 }
1837
1838 pathBlock = node;
1839 }
1840
1841 // First not allowed node reached - start splitting original container
1842 if ( nodeData.firstNotAllowed )
1843 splittingContainer = 1;
1844
1845 if ( splittingContainer && nodeData.isElement ) {
1846 insertionContainer = range.startContainer;
1847 toSplit = null;
1848
1849 // Find the first ancestor that can contain current node.
1850 // This one won't be split.
1851 while ( insertionContainer && !DTD[ insertionContainer.getName() ][ nodeData.name ] ) {
1852 if ( insertionContainer.equals( blockLimit ) ) {
1853 insertionContainer = null;
1854 break;
1855 }
1856
1857 toSplit = insertionContainer;
1858 insertionContainer = insertionContainer.getParent();
1859 }
1860
1861 // If split has to be done - do it and mark both ends as a possible zombies.
1862 if ( insertionContainer ) {
1863 if ( toSplit ) {
1864 newContainer = range.splitElement( toSplit );
1865 that.zombies.push( newContainer );
1866 that.zombies.push( toSplit );
1867 }
1868 }
1869 // Unable to make the insertion happen in place, resort to the content filter.
1870 else {
1871 // If everything worked fine insertionContainer == blockLimit here.
1872 filteredNodes = filterElement( nodeData.node, blockLimit.getName(), !nodeIndex, nodeIndex == nodesData.length - 1 );
1873 }
1874 }
1875
1876 if ( filteredNodes ) {
1877 while ( ( node = filteredNodes.pop() ) )
1878 range.insertNode( node );
1879 filteredNodes = 0;
1880 } else {
1881 // Insert current node at the start of range.
1882 range.insertNode( nodeData.node );
1883 }
1884
1885 // Move range to the endContainer for the final allowed elements.
1886 if ( nodeData.lastNotAllowed && nodeIndex < nodesData.length - 1 ) {
1887 // If separateEndContainer exists move range there.
1888 // Otherwise try to move range to container created during splitting.
1889 // If this doesn't work - don't move range.
1890 newContainer = separateEndContainer ? endContainer : newContainer;
1891 newContainer && range.setEndAt( newContainer, CKEDITOR.POSITION_AFTER_START );
1892 splittingContainer = 0;
1893 }
1894
1895 // Collapse range after insertion to end.
1896 range.collapse();
1897 }
1898
1899 // Rule 9. Non-editable content should be selected as a whole.
1900 if ( isSingleNonEditableElement( nodesData ) ) {
1901 dontMoveCaret = true;
1902 node = nodesData[ 0 ].node;
1903 range.setStartAt( node, CKEDITOR.POSITION_BEFORE_START );
1904 range.setEndAt( node, CKEDITOR.POSITION_AFTER_END );
1905 }
1906
1907 that.dontMoveCaret = dontMoveCaret;
1908 that.bogusNeededBlocks = bogusNeededBlocks;
1909 }
1910
1911 function cleanupAfterInsertion( that ) {
1912 var range = that.range,
1913 node, testRange, movedIntoInline,
1914 bogusNeededBlocks = that.bogusNeededBlocks,
1915 // Create a bookmark to defend against the following range deconstructing operations.
1916 bm = range.createBookmark();
1917
1918 // Remove all elements that could be created while splitting nodes
1919 // with ranges at its start|end.
1920 // E.g. remove <div><p></p></div>
1921 // But not <div><p> </p></div>
1922 // And replace <div><p><span data="cke-bookmark"/></p></div> with found bookmark.
1923 while ( ( node = that.zombies.pop() ) ) {
1924 // Detached element.
1925 if ( !node.getParent() )
1926 continue;
1927
1928 testRange = range.clone();
1929 testRange.moveToElementEditStart( node );
1930 testRange.removeEmptyBlocksAtEnd();
1931 }
1932
1933 if ( bogusNeededBlocks ) {
1934 // Bring back all block bogus nodes.
1935 while ( ( node = bogusNeededBlocks.pop() ) ) {
1936 if ( CKEDITOR.env.needsBrFiller )
1937 node.appendBogus();
1938 else
1939 node.append( range.document.createText( '\u00a0' ) );
1940 }
1941 }
1942
1943 // Eventually merge identical inline elements.
1944 while ( ( node = that.mergeCandidates.pop() ) )
1945 node.mergeSiblings();
1946
1947 range.moveToBookmark( bm );
1948
1949 // Rule 3.
1950 // Shrink range to the BEFOREEND of previous innermost editable node in source order.
1951
1952 if ( !that.dontMoveCaret ) {
1953 node = getRangePrevious( range );
1954
1955 while ( node && checkIfElement( node ) && !node.is( DTD.$empty ) ) {
1956 if ( node.isBlockBoundary() )
1957 range.moveToPosition( node, CKEDITOR.POSITION_BEFORE_END );
1958 else {
1959 // Don't move into inline element (which ends with a text node)
1960 // found which contains white-space at its end.
1961 // If not - move range's end to the end of this element.
1962 if ( isInline( node ) && node.getHtml().match( /(\s|&nbsp;)$/g ) ) {
1963 movedIntoInline = null;
1964 break;
1965 }
1966
1967 movedIntoInline = range.clone();
1968 movedIntoInline.moveToPosition( node, CKEDITOR.POSITION_BEFORE_END );
1969 }
1970
1971 node = node.getLast( isNotEmpty );
1972 }
1973
1974 movedIntoInline && range.moveToRange( movedIntoInline );
1975 }
1976
1977 }
1978
1979 //
1980 // HELPERS ------------------------------------------------------------
1981 //
1982
1983 function checkIfElement( node ) {
1984 return node.type == CKEDITOR.NODE_ELEMENT;
1985 }
1986
1987 function extractNodesData( dataWrapper, that ) {
1988 var node, sibling, nodeName, allowed,
1989 nodesData = [],
1990 startContainer = that.range.startContainer,
1991 path = that.range.startPath(),
1992 allowedNames = DTD[ startContainer.getName() ],
1993 nodeIndex = 0,
1994 nodesList = dataWrapper.getChildren(),
1995 nodesCount = nodesList.count(),
1996 firstNotAllowed = -1,
1997 lastNotAllowed = -1,
1998 lineBreak = 0,
1999 blockSibling;
2000
2001 // Selection start within a list.
2002 var insideOfList = path.contains( DTD.$list );
2003
2004 for ( ; nodeIndex < nodesCount; ++nodeIndex ) {
2005 node = nodesList.getItem( nodeIndex );
2006
2007 if ( checkIfElement( node ) ) {
2008 nodeName = node.getName();
2009
2010 // Extract only the list items, when insertion happens
2011 // inside of a list, reads as rearrange list items. (#7957)
2012 if ( insideOfList && nodeName in CKEDITOR.dtd.$list ) {
2013 nodesData = nodesData.concat( extractNodesData( node, that ) );
2014 continue;
2015 }
2016
2017 allowed = !!allowedNames[ nodeName ];
2018
2019 // Mark <brs data-cke-eol="1"> at the beginning and at the end.
2020 if ( nodeName == 'br' && node.data( 'cke-eol' ) && ( !nodeIndex || nodeIndex == nodesCount - 1 ) ) {
2021 sibling = nodeIndex ? nodesData[ nodeIndex - 1 ].node : nodesList.getItem( nodeIndex + 1 );
2022
2023 // Line break has to have sibling which is not an <br>.
2024 lineBreak = sibling && ( !checkIfElement( sibling ) || !sibling.is( 'br' ) );
2025 // Line break has block element as a sibling.
2026 blockSibling = sibling && checkIfElement( sibling ) && DTD.$block[ sibling.getName() ];
2027 }
2028
2029 if ( firstNotAllowed == -1 && !allowed )
2030 firstNotAllowed = nodeIndex;
2031 if ( !allowed )
2032 lastNotAllowed = nodeIndex;
2033
2034 nodesData.push( {
2035 isElement: 1,
2036 isLineBreak: lineBreak,
2037 isBlock: node.isBlockBoundary(),
2038 hasBlockSibling: blockSibling,
2039 node: node,
2040 name: nodeName,
2041 allowed: allowed
2042 } );
2043
2044 lineBreak = 0;
2045 blockSibling = 0;
2046 } else {
2047 nodesData.push( { isElement: 0, node: node, allowed: 1 } );
2048 }
2049 }
2050
2051 // Mark first node that cannot be inserted directly into startContainer
2052 // and last node for which startContainer has to be split.
2053 if ( firstNotAllowed > -1 )
2054 nodesData[ firstNotAllowed ].firstNotAllowed = 1;
2055 if ( lastNotAllowed > -1 )
2056 nodesData[ lastNotAllowed ].lastNotAllowed = 1;
2057
2058 return nodesData;
2059 }
2060
2061 // TODO: Review content transformation rules on filtering element.
2062 function filterElement( element, parentName, isFirst, isLast ) {
2063 var nodes = filterElementInner( element, parentName ),
2064 nodes2 = [],
2065 nodesCount = nodes.length,
2066 nodeIndex = 0,
2067 node,
2068 afterSpace = 0,
2069 lastSpaceIndex = -1;
2070
2071 // Remove duplicated spaces and spaces at the:
2072 // * beginnig if filtered element isFirst (isFirst that's going to be inserted)
2073 // * end if filtered element isLast.
2074 for ( ; nodeIndex < nodesCount; nodeIndex++ ) {
2075 node = nodes[ nodeIndex ];
2076
2077 if ( node == ' ' ) {
2078 // Don't push doubled space and if it's leading space for insertion.
2079 if ( !afterSpace && !( isFirst && !nodeIndex ) ) {
2080 nodes2.push( new CKEDITOR.dom.text( ' ' ) );
2081 lastSpaceIndex = nodes2.length;
2082 }
2083 afterSpace = 1;
2084 } else {
2085 nodes2.push( node );
2086 afterSpace = 0;
2087 }
2088 }
2089
2090 // Remove trailing space.
2091 if ( isLast && lastSpaceIndex == nodes2.length )
2092 nodes2.pop();
2093
2094 return nodes2;
2095 }
2096
2097 function filterElementInner( element, parentName ) {
2098 var nodes = [],
2099 children = element.getChildren(),
2100 childrenCount = children.count(),
2101 child,
2102 childIndex = 0,
2103 allowedNames = DTD[ parentName ],
2104 surroundBySpaces = !element.is( DTD.$inline ) || element.is( 'br' );
2105
2106 if ( surroundBySpaces )
2107 nodes.push( ' ' );
2108
2109 for ( ; childIndex < childrenCount; childIndex++ ) {
2110 child = children.getItem( childIndex );
2111
2112 if ( checkIfElement( child ) && !child.is( allowedNames ) )
2113 nodes = nodes.concat( filterElementInner( child, parentName ) );
2114 else
2115 nodes.push( child );
2116 }
2117
2118 if ( surroundBySpaces )
2119 nodes.push( ' ' );
2120
2121 return nodes;
2122 }
2123
2124 function getRangePrevious( range ) {
2125 return checkIfElement( range.startContainer ) && range.startContainer.getChild( range.startOffset - 1 );
2126 }
2127
2128 function isInline( node ) {
2129 return node && checkIfElement( node ) && ( node.is( DTD.$removeEmpty ) || node.is( 'a' ) && !node.isBlockBoundary() );
2130 }
2131
2132 // Checks if only non-editable element is being inserted.
2133 function isSingleNonEditableElement( nodesData ) {
2134 if ( nodesData.length != 1 )
2135 return false;
2136
2137 var nodeData = nodesData[ 0 ];
2138
2139 return nodeData.isElement && ( nodeData.node.getAttribute( 'contenteditable' ) == 'false' );
2140 }
2141
2142 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 };
2143
2144 // See rule 5. in TCs.
2145 // Initial situation:
2146 // <ul><li>AA^</li></ul><ul><li>BB</li></ul>
2147 // We're looking for 2nd <ul>, comparing with 1st <ul> and merging.
2148 // We're not merging if caret is between these elements.
2149 function mergeAncestorElementsOfSelectionEnds( range, blockLimit, startPath, endPath ) {
2150 var walkerRange = range.clone(),
2151 walker, nextNode, previousNode;
2152
2153 walkerRange.setEndAt( blockLimit, CKEDITOR.POSITION_BEFORE_END );
2154 walker = new CKEDITOR.dom.walker( walkerRange );
2155
2156 if ( ( nextNode = walker.next() ) && // Find next source node
2157 checkIfElement( nextNode ) && // which is an element
2158 blockMergedTags[ nextNode.getName() ] && // that can be merged.
2159 ( previousNode = nextNode.getPrevious() ) && // Take previous one
2160 checkIfElement( previousNode ) && // which also has to be an element.
2161 !previousNode.getParent().equals( range.startContainer ) && // Fail if caret is on the same level.
2162 // This means that caret is between these nodes.
2163 startPath.contains( previousNode ) && // Elements path of start of selection has
2164 endPath.contains( nextNode ) && // to contain prevNode and vice versa.
2165 nextNode.isIdentical( previousNode ) // Check if elements are identical.
2166 ) {
2167 // Merge blocks and repeat.
2168 nextNode.moveChildren( previousNode );
2169 nextNode.remove();
2170 mergeAncestorElementsOfSelectionEnds( range, blockLimit, startPath, endPath );
2171 }
2172 }
2173
2174 // If last node that will be inserted is a block (but not a <br>)
2175 // and it will be inserted right before <br> remove this <br>.
2176 // Do the same for the first element that will be inserted and preceding <br>.
2177 function removeBrsAdjacentToPastedBlocks( nodesData, range ) {
2178 var succeedingNode = range.endContainer.getChild( range.endOffset ),
2179 precedingNode = range.endContainer.getChild( range.endOffset - 1 );
2180
2181 if ( succeedingNode )
2182 remove( succeedingNode, nodesData[ nodesData.length - 1 ] );
2183
2184 if ( precedingNode && remove( precedingNode, nodesData[ 0 ] ) ) {
2185 // If preceding <br> was removed - move range left.
2186 range.setEnd( range.endContainer, range.endOffset - 1 );
2187 range.collapse();
2188 }
2189
2190 function remove( maybeBr, maybeBlockData ) {
2191 if ( maybeBlockData.isBlock && maybeBlockData.isElement && !maybeBlockData.node.is( 'br' ) &&
2192 checkIfElement( maybeBr ) && maybeBr.is( 'br' ) ) {
2193 maybeBr.remove();
2194 return 1;
2195 }
2196 }
2197 }
2198
2199 // Return 1 if <br> should be skipped when inserting, 0 otherwise.
2200 function splitOnLineBreak( range, blockLimit, nodeData ) {
2201 var firstBlockAscendant, pos;
2202
2203 if ( nodeData.hasBlockSibling )
2204 return 1;
2205
2206 firstBlockAscendant = range.startContainer.getAscendant( DTD.$block, 1 );
2207 if ( !firstBlockAscendant || !firstBlockAscendant.is( { div: 1, p: 1 } ) )
2208 return 0;
2209
2210 pos = firstBlockAscendant.getPosition( blockLimit );
2211
2212 if ( pos == CKEDITOR.POSITION_IDENTICAL || pos == CKEDITOR.POSITION_CONTAINS )
2213 return 0;
2214
2215 var newContainer = range.splitElement( firstBlockAscendant );
2216 range.moveToPosition( newContainer, CKEDITOR.POSITION_AFTER_START );
2217
2218 return 1;
2219 }
2220
2221 var stripSingleBlockTags = { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1 },
2222 inlineButNotBr = CKEDITOR.tools.extend( {}, DTD.$inline );
2223 delete inlineButNotBr.br;
2224
2225 // Rule 7.
2226 function stripBlockTagIfSingleLine( dataWrapper ) {
2227 var block, children;
2228
2229 if ( dataWrapper.getChildCount() == 1 && // Only one node bein inserted.
2230 checkIfElement( block = dataWrapper.getFirst() ) && // And it's an element.
2231 block.is( stripSingleBlockTags ) && // That's <p> or <div> or header.
2232 !block.hasAttribute( 'contenteditable' ) // It's not a non-editable block or nested editable.
2233 ) {
2234 // Check children not containing block.
2235 children = block.getElementsByTag( '*' );
2236 for ( var i = 0, child, count = children.count(); i < count; i++ ) {
2237 child = children.getItem( i );
2238 if ( !child.is( inlineButNotBr ) )
2239 return;
2240 }
2241
2242 block.moveChildren( block.getParent( 1 ) );
2243 block.remove();
2244 }
2245 }
2246
2247 function wrapDataWithInlineStyles( data, that ) {
2248 var element = that.inlineStylesPeak,
2249 doc = element.getDocument(),
2250 wrapper = doc.createText( '{cke-peak}' ),
2251 limit = that.inlineStylesRoot.getParent();
2252
2253 while ( !element.equals( limit ) ) {
2254 wrapper = wrapper.appendTo( element.clone() );
2255 element = element.getParent();
2256 }
2257
2258 // Don't use String.replace because it fails in IE7 if special replacement
2259 // characters ($$, $&, etc.) are in data (#10367).
2260 return wrapper.getOuterHtml().split( '{cke-peak}' ).join( data );
2261 }
2262
2263 return insert;
2264 } )();
2265
2266 function afterInsert( editable ) {
2267 var editor = editable.editor;
2268
2269 // Scroll using selection, not ranges, to affect native pastes.
2270 editor.getSelection().scrollIntoView();
2271
2272 // Save snaps after the whole execution completed.
2273 // This's a workaround for make DOM modification's happened after
2274 // 'insertElement' to be included either, e.g. Form-based dialogs' 'commitContents'
2275 // call.
2276 setTimeout( function() {
2277 editor.fire( 'saveSnapshot' );
2278 }, 0 );
2279 }
2280
2281 // 1. Fixes a range which is a result of deleteContents() and is placed in an intermediate element (see dtd.$intermediate),
2282 // inside a table. A goal is to find a closest <td> or <th> element and when this fails, recreate the structure of the table.
2283 // 2. Fixes empty cells by appending bogus <br>s or deleting empty text nodes in IE<=8 case.
2284 fixTableAfterContentsDeletion = ( function() {
2285 // Creates an element walker which can only "go deeper". It won't
2286 // move out from any element. Therefore it can be used to find <td>x</td> in cases like:
2287 // <table><tbody><tr><td>x</td></tr></tbody>^<tfoot>...
2288 function getFixTableSelectionWalker( testRange ) {
2289 var walker = new CKEDITOR.dom.walker( testRange );
2290 walker.guard = function( node, isMovingOut ) {
2291 if ( isMovingOut )
2292 return false;
2293 if ( node.type == CKEDITOR.NODE_ELEMENT )
2294 return node.is( CKEDITOR.dtd.$tableContent );
2295 };
2296 walker.evaluator = function( node ) {
2297 return node.type == CKEDITOR.NODE_ELEMENT;
2298 };
2299
2300 return walker;
2301 }
2302
2303 function fixTableStructure( element, newElementName, appendToStart ) {
2304 var temp = element.getDocument().createElement( newElementName );
2305 element.append( temp, appendToStart );
2306 return temp;
2307 }
2308
2309 // Fix empty cells. This means:
2310 // * add bogus <br> if browser needs it
2311 // * remove empty text nodes on IE8, because it will crash (http://dev.ckeditor.com/ticket/11183#comment:8).
2312 function fixEmptyCells( cells ) {
2313 var i = cells.count(),
2314 cell;
2315
2316 for ( i; i-- > 0; ) {
2317 cell = cells.getItem( i );
2318
2319 if ( !CKEDITOR.tools.trim( cell.getHtml() ) ) {
2320 cell.appendBogus();
2321 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && cell.getChildCount() )
2322 cell.getFirst().remove();
2323 }
2324 }
2325 }
2326
2327 return function( range ) {
2328 var container = range.startContainer,
2329 table = container.getAscendant( 'table', 1 ),
2330 testRange,
2331 deeperSibling,
2332 appendToStart = false;
2333
2334 fixEmptyCells( table.getElementsByTag( 'td' ) );
2335 fixEmptyCells( table.getElementsByTag( 'th' ) );
2336
2337 // Look left.
2338 testRange = range.clone();
2339 testRange.setStart( container, 0 );
2340 deeperSibling = getFixTableSelectionWalker( testRange ).lastBackward();
2341
2342 // If left is empty, look right.
2343 if ( !deeperSibling ) {
2344 testRange = range.clone();
2345 testRange.setEndAt( container, CKEDITOR.POSITION_BEFORE_END );
2346 deeperSibling = getFixTableSelectionWalker( testRange ).lastForward();
2347 appendToStart = true;
2348 }
2349
2350 // If there's no deeper nested element in both direction - container is empty - we'll use it then.
2351 if ( !deeperSibling )
2352 deeperSibling = container;
2353
2354 // Fix structure...
2355
2356 // We found a table what means that it's empty - remove it completely.
2357 if ( deeperSibling.is( 'table' ) ) {
2358 range.setStartAt( deeperSibling, CKEDITOR.POSITION_BEFORE_START );
2359 range.collapse( true );
2360 deeperSibling.remove();
2361 return;
2362 }
2363
2364 // Found an empty txxx element - append tr.
2365 if ( deeperSibling.is( { tbody: 1, thead: 1, tfoot: 1 } ) )
2366 deeperSibling = fixTableStructure( deeperSibling, 'tr', appendToStart );
2367
2368 // Found an empty tr element - append td/th.
2369 if ( deeperSibling.is( 'tr' ) )
2370 deeperSibling = fixTableStructure( deeperSibling, deeperSibling.getParent().is( 'thead' ) ? 'th' : 'td', appendToStart );
2371
2372 // To avoid setting selection after bogus, remove it from the current cell.
2373 // We can safely do that, because we'll insert element into that cell.
2374 var bogus = deeperSibling.getBogus();
2375 if ( bogus )
2376 bogus.remove();
2377
2378 range.moveToPosition( deeperSibling, appendToStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
2379 };
2380 } )();
2381
2382 fixListAfterContentsDelete = ( function() {
2383 // Creates an element walker which operates only within lists.
2384 function getFixListSelectionWalker( testRange ) {
2385 var walker = new CKEDITOR.dom.walker( testRange );
2386 walker.guard = function( node, isMovingOut ) {
2387 if ( isMovingOut )
2388 return false;
2389 if ( node.type == CKEDITOR.NODE_ELEMENT )
2390 return node.is( CKEDITOR.dtd.$list ) || node.is( CKEDITOR.dtd.$listItem );
2391 };
2392 walker.evaluator = function( node ) {
2393 return node.type == CKEDITOR.NODE_ELEMENT && node.is( CKEDITOR.dtd.$listItem );
2394 };
2395
2396 return walker;
2397 }
2398
2399 return function( range ) {
2400 var container = range.startContainer,
2401 appendToStart = false,
2402 testRange,
2403 deeperSibling;
2404
2405 // Look left.
2406 testRange = range.clone();
2407 testRange.setStart( container, 0 );
2408 deeperSibling = getFixListSelectionWalker( testRange ).lastBackward();
2409
2410 // If left is empty, look right.
2411 if ( !deeperSibling ) {
2412 testRange = range.clone();
2413 testRange.setEndAt( container, CKEDITOR.POSITION_BEFORE_END );
2414 deeperSibling = getFixListSelectionWalker( testRange ).lastForward();
2415 appendToStart = true;
2416 }
2417
2418 // If there's no deeper nested element in both direction - container is empty - we'll use it then.
2419 if ( !deeperSibling )
2420 deeperSibling = container;
2421
2422 // We found a list what means that it's empty - remove it completely.
2423 if ( deeperSibling.is( CKEDITOR.dtd.$list ) ) {
2424 range.setStartAt( deeperSibling, CKEDITOR.POSITION_BEFORE_START );
2425 range.collapse( true );
2426 deeperSibling.remove();
2427 return;
2428 }
2429
2430 // To avoid setting selection after bogus, remove it from the target list item.
2431 // We can safely do that, because we'll insert element into that cell.
2432 var bogus = deeperSibling.getBogus();
2433 if ( bogus )
2434 bogus.remove();
2435
2436 range.moveToPosition( deeperSibling, appendToStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
2437 range.select();
2438 };
2439 } )();
2440
2441 function mergeBlocksCollapsedSelection( editor, range, backspace, startPath ) {
2442 var startBlock = startPath.block;
2443
2444 // Selection must be collapsed and to be anchored in a block.
2445 if ( !startBlock )
2446 return false;
2447
2448 // Exclude cases where, i.e. if pressed arrow key, selection
2449 // would move within the same block (merge inside a block).
2450 if ( !range[ backspace ? 'checkStartOfBlock' : 'checkEndOfBlock' ]() )
2451 return false;
2452
2453 // Make sure, there's an editable position to put selection,
2454 // which i.e. would be used if pressed arrow key, but abort
2455 // if such position exists but means a selected non-editable element.
2456 if ( !range.moveToClosestEditablePosition( startBlock, !backspace ) || !range.collapsed )
2457 return false;
2458
2459 // Handle special case, when block's sibling is a <hr>. Delete it and keep selection
2460 // in the same place (http://dev.ckeditor.com/ticket/11861#comment:9).
2461 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT ) {
2462 var touched = range.startContainer.getChild( range.startOffset - ( backspace ? 1 : 0 ) );
2463 if ( touched && touched.type == CKEDITOR.NODE_ELEMENT && touched.is( 'hr' ) ) {
2464 editor.fire( 'saveSnapshot' );
2465 touched.remove();
2466 return true;
2467 }
2468 }
2469
2470 var siblingBlock = range.startPath().block;
2471
2472 // Abort if an editable position exists, but either it's not
2473 // in a block or that block is the parent of the start block
2474 // (merging child into parent).
2475 if ( !siblingBlock || ( siblingBlock && siblingBlock.contains( startBlock ) ) )
2476 return;
2477
2478 editor.fire( 'saveSnapshot' );
2479
2480 // Remove bogus to avoid duplicated boguses.
2481 var bogus;
2482 if ( ( bogus = ( backspace ? siblingBlock : startBlock ).getBogus() ) )
2483 bogus.remove();
2484
2485 // Save selection. It will be restored.
2486 var selection = editor.getSelection(),
2487 bookmarks = selection.createBookmarks();
2488
2489 // Merge blocks.
2490 ( backspace ? startBlock : siblingBlock ).moveChildren( backspace ? siblingBlock : startBlock, false );
2491
2492 // Also merge children along with parents.
2493 startPath.lastElement.mergeSiblings();
2494
2495 // Cut off removable branch of the DOM tree.
2496 pruneEmptyDisjointAncestors( startBlock, siblingBlock, !backspace );
2497
2498 // Restore selection.
2499 selection.selectBookmarks( bookmarks );
2500
2501 return true;
2502 }
2503
2504 function mergeBlocksNonCollapsedSelection( editor, range, startPath ) {
2505 var startBlock = startPath.block,
2506 endPath = range.endPath(),
2507 endBlock = endPath.block;
2508
2509 // Selection must be anchored in two different blocks.
2510 if ( !startBlock || !endBlock || startBlock.equals( endBlock ) )
2511 return false;
2512
2513 editor.fire( 'saveSnapshot' );
2514
2515 // Remove bogus to avoid duplicated boguses.
2516 var bogus;
2517 if ( ( bogus = startBlock.getBogus() ) )
2518 bogus.remove();
2519
2520 // Changing end container to element from text node (#12503).
2521 range.enlarge( CKEDITOR.ENLARGE_INLINE );
2522
2523 // Delete range contents. Do NOT merge. Merging is weird.
2524 range.deleteContents();
2525
2526 // If something has left of the block to be merged, clean it up.
2527 // It may happen when merging with list items.
2528 if ( endBlock.getParent() ) {
2529 // Move children to the first block.
2530 endBlock.moveChildren( startBlock, false );
2531
2532 // ...and merge them if that's possible.
2533 startPath.lastElement.mergeSiblings();
2534
2535 // If expanded selection, things are always merged like with BACKSPACE.
2536 pruneEmptyDisjointAncestors( startBlock, endBlock, true );
2537 }
2538
2539 // Make sure the result selection is collapsed.
2540 range = editor.getSelection().getRanges()[ 0 ];
2541 range.collapse( 1 );
2542
2543 // Optimizing range containers from text nodes to elements (#12503).
2544 range.optimize();
2545 if ( range.startContainer.getHtml() === '' ) {
2546 range.startContainer.appendBogus();
2547 }
2548
2549 range.select();
2550
2551 return true;
2552 }
2553
2554 // Finds the innermost child of common parent, which,
2555 // if removed, removes nothing but the contents of the element.
2556 //
2557 // before: <div><p><strong>first</strong></p><p>second</p></div>
2558 // after: <div><p>second</p></div>
2559 //
2560 // before: <div><p>x<strong>first</strong></p><p>second</p></div>
2561 // after: <div><p>x</p><p>second</p></div>
2562 //
2563 // isPruneToEnd=true
2564 // before: <div><p><strong>first</strong></p><p>second</p></div>
2565 // after: <div><p><strong>first</strong></p></div>
2566 //
2567 // @param {CKEDITOR.dom.element} first
2568 // @param {CKEDITOR.dom.element} second
2569 // @param {Boolean} isPruneToEnd
2570 function pruneEmptyDisjointAncestors( first, second, isPruneToEnd ) {
2571 var commonParent = first.getCommonAncestor( second ),
2572 node = isPruneToEnd ? second : first,
2573 removableParent = node;
2574
2575 while ( ( node = node.getParent() ) && !commonParent.equals( node ) && node.getChildCount() == 1 )
2576 removableParent = node;
2577
2578 removableParent.remove();
2579 }
2580
2581 //
2582 // Helpers for editable.getHtmlFromRange.
2583 //
2584 getHtmlFromRangeHelpers = {
2585 eol: {
2586 detect: function( that, editable ) {
2587 var range = that.range,
2588 rangeStart = range.clone(),
2589 rangeEnd = range.clone(),
2590
2591 startPath = new CKEDITOR.dom.elementPath( range.startContainer, editable ),
2592 endPath = new CKEDITOR.dom.elementPath( range.endContainer, editable );
2593
2594 // Note: checkBoundaryOfElement will not work on original range as CKEDITOR.START|END
2595 // means that range start|end must be literally anchored at block start|end, e.g.
2596 //
2597 // <p>a{</p><p>}b</p>
2598 //
2599 // will return false for both paragraphs but two similar ranges
2600 //
2601 // <p>a{}</p><p>{}b</p>
2602 //
2603 // will return true if checked separately.
2604 rangeStart.collapse( 1 );
2605 rangeEnd.collapse();
2606
2607 if ( startPath.block && rangeStart.checkBoundaryOfElement( startPath.block, CKEDITOR.END ) ) {
2608 range.setStartAfter( startPath.block );
2609 that.prependEolBr = 1;
2610 }
2611
2612 if ( endPath.block && rangeEnd.checkBoundaryOfElement( endPath.block, CKEDITOR.START ) ) {
2613 range.setEndBefore( endPath.block );
2614 that.appendEolBr = 1;
2615 }
2616 },
2617
2618 fix: function( that, editable ) {
2619 var doc = editable.getDocument(),
2620 appended;
2621
2622 // Append <br data-cke-eol="1"> to the fragment.
2623 if ( that.appendEolBr ) {
2624 appended = this.createEolBr( doc );
2625 that.fragment.append( appended );
2626 }
2627
2628 // Prepend <br data-cke-eol="1"> to the fragment but avoid duplicates. Such
2629 // elements should never follow each other in DOM.
2630 if ( that.prependEolBr && ( !appended || appended.getPrevious() ) ) {
2631 that.fragment.append( this.createEolBr( doc ), 1 );
2632 }
2633 },
2634
2635 createEolBr: function( doc ) {
2636 return doc.createElement( 'br', {
2637 attributes: {
2638 'data-cke-eol': 1
2639 }
2640 } );
2641 }
2642 },
2643
2644 bogus: {
2645 exclude: function( that ) {
2646 var boundaryNodes = that.range.getBoundaryNodes(),
2647 startNode = boundaryNodes.startNode,
2648 endNode = boundaryNodes.endNode;
2649
2650 // If bogus is the last node in range but not the only node, exclude it.
2651 if ( endNode && isBogus( endNode ) && ( !startNode || !startNode.equals( endNode ) ) )
2652 that.range.setEndBefore( endNode );
2653 }
2654 },
2655
2656 tree: {
2657 rebuild: function( that, editable ) {
2658 var range = that.range,
2659 node = range.getCommonAncestor(),
2660
2661 // A path relative to the common ancestor.
2662 commonPath = new CKEDITOR.dom.elementPath( node, editable ),
2663 startPath = new CKEDITOR.dom.elementPath( range.startContainer, editable ),
2664 endPath = new CKEDITOR.dom.elementPath( range.endContainer, editable ),
2665 limit;
2666
2667 if ( node.type == CKEDITOR.NODE_TEXT )
2668 node = node.getParent();
2669
2670 // Fix DOM of partially enclosed tables
2671 // <table><tbody><tr><td>a{b</td><td>c}d</td></tr></tbody></table>
2672 // Full table is returned
2673 // <table><tbody><tr><td>b</td><td>c</td></tr></tbody></table>
2674 // instead of
2675 // <td>b</td><td>c</td>
2676 if ( commonPath.blockLimit.is( { tr: 1, table: 1 } ) ) {
2677 var tableParent = commonPath.contains( 'table' ).getParent();
2678
2679 limit = function( node ) {
2680 return !node.equals( tableParent );
2681 };
2682 }
2683
2684 // Fix DOM in the following case
2685 // <ol><li>a{b<ul><li>c}d</li></ul></li></ol>
2686 // Full list is returned
2687 // <ol><li>b<ul><li>c</li></ul></li></ol>
2688 // instead of
2689 // b<ul><li>c</li></ul>
2690 else if ( commonPath.block && commonPath.block.is( CKEDITOR.dtd.$listItem ) ) {
2691 var startList = startPath.contains( CKEDITOR.dtd.$list ),
2692 endList = endPath.contains( CKEDITOR.dtd.$list );
2693
2694 if ( !startList.equals( endList ) ) {
2695 var listParent = commonPath.contains( CKEDITOR.dtd.$list ).getParent();
2696
2697 limit = function( node ) {
2698 return !node.equals( listParent );
2699 };
2700 }
2701 }
2702
2703 // If not defined, use generic limit function.
2704 if ( !limit ) {
2705 limit = function( node ) {
2706 return !node.equals( commonPath.block ) && !node.equals( commonPath.blockLimit );
2707 };
2708 }
2709
2710 this.rebuildFragment( that, editable, node, limit );
2711 },
2712
2713 rebuildFragment: function( that, editable, node, checkLimit ) {
2714 var clone;
2715
2716 while ( node && !node.equals( editable ) && checkLimit( node ) ) {
2717 // Don't clone children. Preserve element ids.
2718 clone = node.clone( 0, 1 );
2719 that.fragment.appendTo( clone );
2720 that.fragment = clone;
2721
2722 node = node.getParent();
2723 }
2724 }
2725 },
2726
2727 cell: {
2728 // Handle range anchored in table row with a single cell enclosed:
2729 // <table><tbody><tr>[<td>a</td>]</tr></tbody></table>
2730 // becomes
2731 // <table><tbody><tr><td>{a}</td></tr></tbody></table>
2732 shrink: function( that ) {
2733 var range = that.range,
2734 startContainer = range.startContainer,
2735 endContainer = range.endContainer,
2736 startOffset = range.startOffset,
2737 endOffset = range.endOffset;
2738
2739 if ( startContainer.type == CKEDITOR.NODE_ELEMENT && startContainer.equals( endContainer ) && startContainer.is( 'tr' ) && ++startOffset == endOffset ) {
2740 range.shrink( CKEDITOR.SHRINK_TEXT );
2741 }
2742 }
2743 }
2744 };
2745
2746 //
2747 // Helpers for editable.extractHtmlFromRange.
2748 //
2749 extractHtmlFromRangeHelpers = ( function() {
2750 function optimizeBookmarkNode( node, toStart ) {
2751 var parent = node.getParent();
2752
2753 if ( parent.is( CKEDITOR.dtd.$inline ) )
2754 node[ toStart ? 'insertBefore' : 'insertAfter' ]( parent );
2755 }
2756
2757 function mergeElements( merged, startBookmark, endBookmark ) {
2758 optimizeBookmarkNode( startBookmark );
2759 optimizeBookmarkNode( endBookmark, 1 );
2760
2761 var next;
2762 while ( ( next = endBookmark.getNext() ) ) {
2763 next.insertAfter( startBookmark );
2764
2765 // Update startBookmark after insertion to avoid the reversal of nodes (#13449).
2766 startBookmark = next;
2767 }
2768
2769 if ( isEmpty( merged ) )
2770 merged.remove();
2771 }
2772
2773 function getPath( startElement, root ) {
2774 return new CKEDITOR.dom.elementPath( startElement, root );
2775 }
2776
2777 // Creates a range from a bookmark without removing the bookmark.
2778 function createRangeFromBookmark( root, bookmark ) {
2779 var range = new CKEDITOR.dom.range( root );
2780 range.setStartAfter( bookmark.startNode );
2781 range.setEndBefore( bookmark.endNode );
2782 return range;
2783 }
2784
2785 var list = {
2786 detectMerge: function( that, editable ) {
2787 var range = createRangeFromBookmark( editable, that.bookmark ),
2788 startPath = range.startPath(),
2789 endPath = range.endPath(),
2790
2791 startList = startPath.contains( CKEDITOR.dtd.$list ),
2792 endList = endPath.contains( CKEDITOR.dtd.$list );
2793
2794 that.mergeList =
2795 // Both lists must exist
2796 startList && endList &&
2797 // ...and be of the same type
2798 // startList.getName() == endList.getName() &&
2799 // ...and share the same parent (same level in the tree)
2800 startList.getParent().equals( endList.getParent() ) &&
2801 // ...and must be different.
2802 !startList.equals( endList );
2803
2804 that.mergeListItems =
2805 startPath.block && endPath.block &&
2806 // Both containers must be list items
2807 startPath.block.is( CKEDITOR.dtd.$listItem ) && endPath.block.is( CKEDITOR.dtd.$listItem );
2808
2809 // Create merge bookmark.
2810 if ( that.mergeList || that.mergeListItems ) {
2811 var rangeClone = range.clone();
2812
2813 rangeClone.setStartBefore( that.bookmark.startNode );
2814 rangeClone.setEndAfter( that.bookmark.endNode );
2815
2816 that.mergeListBookmark = rangeClone.createBookmark();
2817 }
2818 },
2819
2820 merge: function( that, editable ) {
2821 if ( !that.mergeListBookmark )
2822 return;
2823
2824 var startNode = that.mergeListBookmark.startNode,
2825 endNode = that.mergeListBookmark.endNode,
2826
2827 startPath = getPath( startNode, editable ),
2828 endPath = getPath( endNode, editable );
2829
2830 if ( that.mergeList ) {
2831 var firstList = startPath.contains( CKEDITOR.dtd.$list ),
2832 secondList = endPath.contains( CKEDITOR.dtd.$list );
2833
2834 if ( !firstList.equals( secondList ) ) {
2835 secondList.moveChildren( firstList );
2836 secondList.remove();
2837 }
2838 }
2839
2840 if ( that.mergeListItems ) {
2841 var firstListItem = startPath.contains( CKEDITOR.dtd.$listItem ),
2842 secondListItem = endPath.contains( CKEDITOR.dtd.$listItem );
2843
2844 if ( !firstListItem.equals( secondListItem ) ) {
2845 mergeElements( secondListItem, startNode, endNode );
2846 }
2847 }
2848
2849 // Remove bookmark nodes.
2850 startNode.remove();
2851 endNode.remove();
2852 }
2853 };
2854
2855 var block = {
2856 // Detects whether blocks should be merged once contents are extracted.
2857 detectMerge: function( that, editable ) {
2858 // Don't merge blocks if lists or tables are already involved.
2859 if ( that.tableContentsRanges || that.mergeListBookmark )
2860 return;
2861
2862 var rangeClone = new CKEDITOR.dom.range( editable );
2863
2864 rangeClone.setStartBefore( that.bookmark.startNode );
2865 rangeClone.setEndAfter( that.bookmark.endNode );
2866
2867 that.mergeBlockBookmark = rangeClone.createBookmark();
2868 },
2869
2870 merge: function( that, editable ) {
2871 if ( !that.mergeBlockBookmark || that.purgeTableBookmark )
2872 return;
2873
2874 var startNode = that.mergeBlockBookmark.startNode,
2875 endNode = that.mergeBlockBookmark.endNode,
2876
2877 startPath = getPath( startNode, editable ),
2878 endPath = getPath( endNode, editable ),
2879
2880 firstBlock = startPath.block,
2881 secondBlock = endPath.block;
2882
2883 if ( firstBlock && secondBlock && !firstBlock.equals( secondBlock ) ) {
2884 mergeElements( secondBlock, startNode, endNode );
2885 }
2886
2887 // Remove bookmark nodes.
2888 startNode.remove();
2889 endNode.remove();
2890 }
2891 };
2892
2893 var table = ( function() {
2894 var tableEditable = { td: 1, th: 1, caption: 1 };
2895
2896 // Returns an array of ranges which should be entirely extracted.
2897 //
2898 // <table><tr>[<td>xx</td><td>y}y</td></tr></table>
2899 // will find:
2900 // <table><tr><td>[xx]</td><td>[y}y</td></tr></table>
2901 function findTableContentsRanges( range ) {
2902 // Leaving the below for debugging purposes.
2903 //
2904 // console.log( 'findTableContentsRanges' );
2905 // console.log( bender.tools.range.getWithHtml( range.root, range ) );
2906
2907 var contentsRanges = [],
2908 editableRange,
2909 walker = new CKEDITOR.dom.walker( range ),
2910 startCell = range.startPath().contains( tableEditable ),
2911 endCell = range.endPath().contains( tableEditable ),
2912 database = {};
2913
2914 walker.guard = function( node, leaving ) {
2915 // Guard may be executed on some node boundaries multiple times,
2916 // what results in creating more than one range for each selected cell. (#12964)
2917 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
2918 var key = 'visited_' + ( leaving ? 'out' : 'in' );
2919 if ( node.getCustomData( key ) ) {
2920 return;
2921 }
2922
2923 CKEDITOR.dom.element.setMarker( database, node, key, 1 );
2924 }
2925
2926 // Handle partial selection in a cell in which the range starts:
2927 // <td><p>x{xx</p></td>...
2928 // will store:
2929 // <td><p>x{xx</p>]</td>
2930 if ( leaving && startCell && node.equals( startCell ) ) {
2931 editableRange = range.clone();
2932 editableRange.setEndAt( startCell, CKEDITOR.POSITION_BEFORE_END );
2933 contentsRanges.push( editableRange );
2934 return;
2935 }
2936
2937 // Handle partial selection in a cell in which the range ends.
2938 if ( !leaving && endCell && node.equals( endCell ) ) {
2939 editableRange = range.clone();
2940 editableRange.setStartAt( endCell, CKEDITOR.POSITION_AFTER_START );
2941 contentsRanges.push( editableRange );
2942 return;
2943 }
2944
2945 // Handle all other cells visited by the walker.
2946 // We need to check whether the cell is disjoint with
2947 // the start and end cells to correctly handle case like:
2948 // <td>x{x</td><td><table>..<td>y}y</td>..</table></td>
2949 // without the check the second cell's content would be entirely removed.
2950 if ( !leaving && checkRemoveCellContents( node ) ) {
2951 editableRange = range.clone();
2952 editableRange.selectNodeContents( node );
2953 contentsRanges.push( editableRange );
2954 }
2955 };
2956
2957 walker.lastForward();
2958
2959 // Clear all markers so next extraction will not be affected by this one.
2960 CKEDITOR.dom.element.clearAllMarkers( database );
2961
2962 return contentsRanges;
2963
2964 function checkRemoveCellContents( node ) {
2965 return (
2966 // Must be a cell.
2967 node.type == CKEDITOR.NODE_ELEMENT && node.is( tableEditable ) &&
2968 // Must be disjoint with the range's startCell if exists.
2969 ( !startCell || checkDisjointNodes( node, startCell ) ) &&
2970 // Must be disjoint with the range's endCell if exists.
2971 ( !endCell || checkDisjointNodes( node, endCell ) )
2972 );
2973 }
2974 }
2975
2976 // Returns a normalized common ancestor of a range.
2977 // If the real common ancestor is located somewhere in between a table and a td/th/caption,
2978 // then the table will be returned.
2979 function getNormalizedAncestor( range ) {
2980 var common = range.getCommonAncestor();
2981
2982 if ( common.is( CKEDITOR.dtd.$tableContent ) && !common.is( tableEditable ) ) {
2983 common = common.getAscendant( 'table', true );
2984 }
2985
2986 return common;
2987 }
2988
2989 // Check whether node1 and node2 are disjoint, so are:
2990 // * not identical,
2991 // * not contained in each other.
2992 function checkDisjointNodes( node1, node2 ) {
2993 var disallowedPositions = CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_IS_CONTAINED,
2994 pos = node1.getPosition( node2 );
2995
2996 // Baaah... IDENTICAL is 0, so we can't simplify this ;/.
2997 return pos === CKEDITOR.POSITION_IDENTICAL ?
2998 false :
2999 ( ( pos & disallowedPositions ) === 0 );
3000 }
3001
3002 return {
3003 // Detects whether to purge entire list.
3004 detectPurge: function( that ) {
3005 var range = that.range,
3006 walkerRange = range.clone();
3007
3008 walkerRange.enlarge( CKEDITOR.ENLARGE_ELEMENT );
3009
3010 var walker = new CKEDITOR.dom.walker( walkerRange ),
3011 editablesCount = 0;
3012
3013 // Count the number of table editables in the range. If there's more than one,
3014 // table MAY be removed completely (it's a cross-cell range). Otherwise, only
3015 // the contents of the cell are usually removed.
3016 walker.evaluator = function( node ) {
3017 if ( node.type == CKEDITOR.NODE_ELEMENT && node.is( tableEditable ) ) {
3018 ++editablesCount;
3019 }
3020 };
3021
3022 walker.checkForward();
3023
3024 if ( editablesCount > 1 ) {
3025 var startTable = range.startPath().contains( 'table' ),
3026 endTable = range.endPath().contains( 'table' );
3027
3028 if ( startTable && endTable && range.checkBoundaryOfElement( startTable, CKEDITOR.START ) && range.checkBoundaryOfElement( endTable, CKEDITOR.END ) ) {
3029 var rangeClone = that.range.clone();
3030
3031 rangeClone.setStartBefore( startTable );
3032 rangeClone.setEndAfter( endTable );
3033
3034 that.purgeTableBookmark = rangeClone.createBookmark();
3035 }
3036 }
3037 },
3038
3039 // The magic.
3040 //
3041 // This method tries to discover whether the range starts or ends somewhere in a table
3042 // (it is not interested whether the range contains a table, because in such case
3043 // the extractContents() methods does the job correctly).
3044 // If the range meets these criteria, then the method tries to discover and store the following:
3045 //
3046 // * that.tableSurroundingRange - a part of the range which is located outside of any table which
3047 // will be touched (note: when range is located in a single cell it does not touch the table).
3048 // This range can be placed at:
3049 // * at the beginning: <p>he{re</p><table>..]..</table>
3050 // * in the middle: <table>..[..</table><p>here</p><table>..]..</table>
3051 // * at the end: <table>..[..</table><p>he}re</p>
3052 // * that.tableContentsRanges - an array of ranges with contents of td/th/caption that should be removed.
3053 // This assures that calling extractContents() does not change the structure of the table(s).
3054 detectRanges: function( that, editable ) {
3055 var range = createRangeFromBookmark( editable, that.bookmark ),
3056 surroundingRange = range.clone(),
3057 leftRange,
3058 rightRange,
3059
3060 // Find a common ancestor and normalize it (so the following paths contain tables).
3061 commonAncestor = getNormalizedAncestor( range ),
3062
3063 // Create paths using the normalized ancestor, so tables beyond the context
3064 // of the input range are not found.
3065 startPath = new CKEDITOR.dom.elementPath( range.startContainer, commonAncestor ),
3066 endPath = new CKEDITOR.dom.elementPath( range.endContainer, commonAncestor ),
3067
3068 startTable = startPath.contains( 'table' ),
3069 endTable = endPath.contains( 'table' ),
3070
3071 tableContentsRanges;
3072
3073 // Nothing to do here - the range doesn't touch any table or
3074 // it contains a table, but that table is fully selected so it will be simply fully removed
3075 // by the normal algorithm.
3076 if ( !startTable && !endTable ) {
3077 return;
3078 }
3079
3080 // Handle two disjoint tables case:
3081 // <table>..[..</table><p>ab</p><table>..]..</table>
3082 // is handled as (respectively: findTableContents( left ), surroundingRange, findTableContents( right )):
3083 // <table>..[..</table>][<p>ab</p>][<table>..]..</table>
3084 // Check that tables are disjoint to exclude a case when start equals end or one is contained
3085 // in the other.
3086 if ( startTable && endTable && checkDisjointNodes( startTable, endTable ) ) {
3087 that.tableSurroundingRange = surroundingRange;
3088 surroundingRange.setStartAt( startTable, CKEDITOR.POSITION_AFTER_END );
3089 surroundingRange.setEndAt( endTable, CKEDITOR.POSITION_BEFORE_START );
3090
3091 leftRange = range.clone();
3092 leftRange.setEndAt( startTable, CKEDITOR.POSITION_AFTER_END );
3093
3094 rightRange = range.clone();
3095 rightRange.setStartAt( endTable, CKEDITOR.POSITION_BEFORE_START );
3096
3097 tableContentsRanges = findTableContentsRanges( leftRange ).concat( findTableContentsRanges( rightRange ) );
3098 }
3099 // Divide the initial range into two parts:
3100 // * range which contains the part containing the table,
3101 // * surroundingRange which contains the part outside the table.
3102 //
3103 // The surroundingRange exists only if one of the range ends is
3104 // located outside the table.
3105 //
3106 // <p>a{b</p><table>..]..</table><p>cd</p>
3107 // becomes (respectively: surroundingRange, range):
3108 // <p>a{b</p>][<table>..]..</table><p>cd</p>
3109 else if ( !startTable ) {
3110 that.tableSurroundingRange = surroundingRange;
3111 surroundingRange.setEndAt( endTable, CKEDITOR.POSITION_BEFORE_START );
3112
3113 range.setStartAt( endTable, CKEDITOR.POSITION_AFTER_START );
3114 }
3115 // <p>ab</p><table>..[..</table><p>c}d</p>
3116 // becomes (respectively range, surroundingRange):
3117 // <p>ab</p><table>..[..</table>][<p>c}d</p>
3118 else if ( !endTable ) {
3119 that.tableSurroundingRange = surroundingRange;
3120 surroundingRange.setStartAt( startTable, CKEDITOR.POSITION_AFTER_END );
3121
3122 range.setEndAt( startTable, CKEDITOR.POSITION_AFTER_END );
3123 }
3124
3125 // Use already calculated or calculate for the remaining range.
3126 that.tableContentsRanges = tableContentsRanges ? tableContentsRanges : findTableContentsRanges( range );
3127
3128 // Leaving the below for debugging purposes.
3129 //
3130 // if ( that.tableSurroundingRange ) {
3131 // console.log( 'tableSurroundingRange' );
3132 // console.log( bender.tools.range.getWithHtml( that.tableSurroundingRange.root, that.tableSurroundingRange ) );
3133 // }
3134 //
3135 // console.log( 'tableContentsRanges' );
3136 // that.tableContentsRanges.forEach( function( range ) {
3137 // console.log( bender.tools.range.getWithHtml( range.root, range ) );
3138 // } );
3139 },
3140
3141 deleteRanges: function( that ) {
3142 var range;
3143
3144 // Delete table cell contents.
3145 while ( ( range = that.tableContentsRanges.pop() ) ) {
3146 range.extractContents();
3147
3148 if ( isEmpty( range.startContainer ) )
3149 range.startContainer.appendBogus();
3150 }
3151
3152 // Finally delete surroundings of the table.
3153 if ( that.tableSurroundingRange ) {
3154 that.tableSurroundingRange.extractContents();
3155 }
3156 },
3157
3158 purge: function( that ) {
3159 if ( !that.purgeTableBookmark )
3160 return;
3161
3162 var doc = that.doc,
3163 range = that.range,
3164 rangeClone = range.clone(),
3165 // How about different enter modes?
3166 block = doc.createElement( 'p' );
3167
3168 block.insertBefore( that.purgeTableBookmark.startNode );
3169
3170 rangeClone.moveToBookmark( that.purgeTableBookmark );
3171 rangeClone.deleteContents();
3172
3173 that.range.moveToPosition( block, CKEDITOR.POSITION_AFTER_START );
3174 }
3175 };
3176 } )();
3177
3178 return {
3179 list: list,
3180 block: block,
3181 table: table,
3182
3183 // Detects whether use "mergeThen" argument in range.extractContents().
3184 detectExtractMerge: function( that ) {
3185 // Don't merge if playing with lists.
3186 return !(
3187 that.range.startPath().contains( CKEDITOR.dtd.$listItem ) &&
3188 that.range.endPath().contains( CKEDITOR.dtd.$listItem )
3189 );
3190 },
3191
3192 fixUneditableRangePosition: function( range ) {
3193 if ( !range.startContainer.getDtd()[ '#' ] ) {
3194 range.moveToClosestEditablePosition( null, true );
3195 }
3196 },
3197
3198 // Perform auto paragraphing if needed.
3199 autoParagraph: function( editor, range ) {
3200 var path = range.startPath(),
3201 fixBlock;
3202
3203 if ( shouldAutoParagraph( editor, path.block, path.blockLimit ) && ( fixBlock = autoParagraphTag( editor ) ) ) {
3204 fixBlock = range.document.createElement( fixBlock );
3205 fixBlock.appendBogus();
3206 range.insertNode( fixBlock );
3207 range.moveToPosition( fixBlock, CKEDITOR.POSITION_AFTER_START );
3208 }
3209 }
3210 };
3211 } )();
3212
3213} )();
3214
3215/**
3216 * Whether the editor must output an empty value (`''`) if its content only consists
3217 * of an empty paragraph.
3218 *
3219 * config.ignoreEmptyParagraph = false;
3220 *
3221 * @cfg {Boolean} [ignoreEmptyParagraph=true]
3222 * @member CKEDITOR.config
3223 */
3224
3225/**
3226 * Event fired by the editor in order to get accessibility help label.
3227 * The event is responded to by a component which provides accessibility
3228 * help (i.e. the `a11yhelp` plugin) hence the editor is notified whether
3229 * accessibility help is available.
3230 *
3231 * Providing info:
3232 *
3233 * editor.on( 'ariaEditorHelpLabel', function( evt ) {
3234 * evt.data.label = editor.lang.common.editorHelp;
3235 * } );
3236 *
3237 * Getting label:
3238 *
3239 * var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
3240 *
3241 * @since 4.4.3
3242 * @event ariaEditorHelpLabel
3243 * @param {String} label The label to be used.
3244 * @member CKEDITOR.editor
3245 */
3246
3247/**
3248 * Event fired when the user double-clicks in the editable area.
3249 * The event allows to open a dialog window for a clicked element in a convenient way:
3250 *
3251 * editor.on( 'doubleclick', function( evt ) {
3252 * var element = evt.data.element;
3253 *
3254 * if ( element.is( 'table' ) )
3255 * evt.data.dialog = 'tableProperties';
3256 * } );
3257 *
3258 * **Note:** To handle double-click on a widget use {@link CKEDITOR.plugins.widget#doubleclick}.
3259 *
3260 * @event doubleclick
3261 * @param data
3262 * @param {CKEDITOR.dom.element} data.element The double-clicked element.
3263 * @param {String} data.dialog The dialog window to be opened. If set by the listener,
3264 * the specified dialog window will be opened.
3265 * @member CKEDITOR.editor
3266 */
diff --git a/sources/core/editor.js b/sources/core/editor.js
new file mode 100644
index 0000000..8dfce7f
--- /dev/null
+++ b/sources/core/editor.js
@@ -0,0 +1,2039 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.editor} class 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 // Create DocumentFragment from specified ranges. For now it handles only tables in Firefox
658 // and returns DocumentFragment from the 1. range for other cases. (#13884)
659 function createDocumentFragmentFromRanges( ranges, editable ) {
660 var docFragment = new CKEDITOR.dom.documentFragment(),
661 tableClone,
662 currentRow,
663 currentRowClone;
664
665 for ( var i = 0; i < ranges.length; i++ ) {
666 var range = ranges[ i ],
667 container = range.startContainer;
668
669 if ( container.getName && container.getName() == 'tr' ) {
670 if ( !tableClone ) {
671 tableClone = container.getAscendant( 'table' ).clone();
672 tableClone.append( container.getAscendant( 'tbody' ).clone() );
673 docFragment.append( tableClone );
674 tableClone = tableClone.findOne( 'tbody' );
675 }
676
677 if ( !( currentRow && currentRow.equals( container ) ) ) {
678 currentRow = container;
679 currentRowClone = container.clone();
680 tableClone.append( currentRowClone );
681 }
682
683 currentRowClone.append( range.cloneContents() );
684 } else {
685 // If there was something else copied with table,
686 // append it to DocumentFragment.
687 docFragment.append( range.cloneContents() );
688 }
689 }
690
691 if ( !tableClone ) {
692 return editable.getHtmlFromRange( ranges[ 0 ] );
693 }
694
695 return docFragment;
696 }
697
698 CKEDITOR.tools.extend( CKEDITOR.editor.prototype, {
699 /**
700 * Adds a command definition to the editor instance. Commands added with
701 * this function can be executed later with the <code>{@link #execCommand}</code> method.
702 *
703 * editorInstance.addCommand( 'sample', {
704 * exec: function( editor ) {
705 * alert( 'Executing a command for the editor name "' + editor.name + '"!' );
706 * }
707 * } );
708 *
709 * @param {String} commandName The indentifier name of the command.
710 * @param {CKEDITOR.commandDefinition} commandDefinition The command definition.
711 */
712 addCommand: function( commandName, commandDefinition ) {
713 commandDefinition.name = commandName.toLowerCase();
714 var cmd = new CKEDITOR.command( this, commandDefinition );
715
716 // Update command when mode is set.
717 // This guarantees that commands added before first editor#mode
718 // aren't immediately updated, but waits for editor#mode and that
719 // commands added later are immediately refreshed, even when added
720 // before instanceReady. #10103, #10249
721 if ( this.mode )
722 updateCommand( this, cmd );
723
724 return this.commands[ commandName ] = cmd;
725 },
726
727 /**
728 * Attaches the editor to a form to call {@link #updateElement} before form submission.
729 * This method is called by both creators ({@link CKEDITOR#replace replace} and
730 * {@link CKEDITOR#inline inline}), so there is no reason to call it manually.
731 *
732 * @private
733 */
734 _attachToForm: function() {
735 var editor = this,
736 element = editor.element,
737 form = new CKEDITOR.dom.element( element.$.form );
738
739 // If are replacing a textarea, we must
740 if ( element.is( 'textarea' ) ) {
741 if ( form ) {
742 form.on( 'submit', onSubmit );
743
744 // Check if there is no element/elements input with name == "submit".
745 // If they exists they will overwrite form submit function (form.$.submit).
746 // If form.$.submit is overwritten we can not do anything with it.
747 if ( isFunction( form.$.submit ) ) {
748 // Setup the submit function because it doesn't fire the
749 // "submit" event.
750 form.$.submit = CKEDITOR.tools.override( form.$.submit, function( originalSubmit ) {
751 return function() {
752 onSubmit();
753
754 // For IE, the DOM submit function is not a
755 // function, so we need third check.
756 if ( originalSubmit.apply )
757 originalSubmit.apply( this );
758 else
759 originalSubmit();
760 };
761 } );
762 }
763
764 // Remove 'submit' events registered on form element before destroying.(#3988)
765 editor.on( 'destroy', function() {
766 form.removeListener( 'submit', onSubmit );
767 } );
768 }
769 }
770
771 function onSubmit( evt ) {
772 editor.updateElement();
773
774 // #8031 If textarea had required attribute and editor is empty fire 'required' event and if
775 // it was cancelled, prevent submitting the form.
776 if ( editor._.required && !element.getValue() && editor.fire( 'required' ) === false ) {
777 // When user press save button event (evt) is undefined (see save plugin).
778 // This method works because it throws error so originalSubmit won't be called.
779 // Also this error won't be shown because it will be caught in save plugin.
780 evt.data.preventDefault();
781 }
782 }
783
784 function isFunction( f ) {
785 // For IE8 typeof fun == object so we cannot use it.
786 return !!( f && f.call && f.apply );
787 }
788 },
789
790 /**
791 * Destroys the editor instance, releasing all resources used by it.
792 * If the editor replaced an element, the element will be recovered.
793 *
794 * alert( CKEDITOR.instances.editor1 ); // e.g. object
795 * CKEDITOR.instances.editor1.destroy();
796 * alert( CKEDITOR.instances.editor1 ); // undefined
797 *
798 * @param {Boolean} [noUpdate] If the instance is replacing a DOM
799 * element, this parameter indicates whether or not to update the
800 * element with the instance content.
801 */
802 destroy: function( noUpdate ) {
803 this.fire( 'beforeDestroy' );
804
805 !noUpdate && updateEditorElement.call( this );
806
807 this.editable( null );
808
809 if ( this.filter ) {
810 this.filter.destroy();
811 delete this.filter;
812 }
813
814 delete this.activeFilter;
815
816 this.status = 'destroyed';
817
818 this.fire( 'destroy' );
819
820 // Plug off all listeners to prevent any further events firing.
821 this.removeAllListeners();
822
823 CKEDITOR.remove( this );
824 CKEDITOR.fire( 'instanceDestroyed', null, this );
825 },
826
827 /**
828 * Returns an {@link CKEDITOR.dom.elementPath element path} for the selection in the editor.
829 *
830 * @param {CKEDITOR.dom.node} [startNode] From which the path should start,
831 * if not specified defaults to editor selection's
832 * start element yielded by {@link CKEDITOR.dom.selection#getStartElement}.
833 * @returns {CKEDITOR.dom.elementPath}
834 */
835 elementPath: function( startNode ) {
836 if ( !startNode ) {
837 var sel = this.getSelection();
838 if ( !sel )
839 return null;
840
841 startNode = sel.getStartElement();
842 }
843
844 return startNode ? new CKEDITOR.dom.elementPath( startNode, this.editable() ) : null;
845 },
846
847 /**
848 * Shortcut to create a {@link CKEDITOR.dom.range} instance from the editable element.
849 *
850 * @returns {CKEDITOR.dom.range} The DOM range created if the editable has presented.
851 * @see CKEDITOR.dom.range
852 */
853 createRange: function() {
854 var editable = this.editable();
855 return editable ? new CKEDITOR.dom.range( editable ) : null;
856 },
857
858 /**
859 * Executes a command associated with the editor.
860 *
861 * editorInstance.execCommand( 'bold' );
862 *
863 * @param {String} commandName The indentifier name of the command.
864 * @param {Object} [data] The data to be passed to the command.
865 * @returns {Boolean} `true` if the command was executed
866 * successfully, otherwise `false`.
867 * @see CKEDITOR.editor#addCommand
868 */
869 execCommand: function( commandName, data ) {
870 var command = this.getCommand( commandName );
871
872 var eventData = {
873 name: commandName,
874 commandData: data,
875 command: command
876 };
877
878 if ( command && command.state != CKEDITOR.TRISTATE_DISABLED ) {
879 if ( this.fire( 'beforeCommandExec', eventData ) !== false ) {
880 eventData.returnValue = command.exec( eventData.commandData );
881
882 // Fire the 'afterCommandExec' immediately if command is synchronous.
883 if ( !command.async && this.fire( 'afterCommandExec', eventData ) !== false )
884 return eventData.returnValue;
885 }
886 }
887
888 // throw 'Unknown command name "' + commandName + '"';
889 return false;
890 },
891
892 /**
893 * Gets one of the registered commands. Note that after registering a
894 * command definition with {@link #addCommand}, it is
895 * transformed internally into an instance of
896 * {@link CKEDITOR.command}, which will then be returned by this function.
897 *
898 * @param {String} commandName The name of the command to be returned.
899 * This is the same name that is used to register the command with `addCommand`.
900 * @returns {CKEDITOR.command} The command object identified by the provided name.
901 */
902 getCommand: function( commandName ) {
903 return this.commands[ commandName ];
904 },
905
906 /**
907 * Gets the editor data. The data will be in "raw" format. It is the same
908 * data that is posted by the editor.
909 *
910 * if ( CKEDITOR.instances.editor1.getData() == '' )
911 * alert( 'There is no data available.' );
912 *
913 * @param {Boolean} internal If set to `true`, it will prevent firing the
914 * {@link CKEDITOR.editor#beforeGetData} and {@link CKEDITOR.editor#event-getData} events, so
915 * the real content of the editor will not be read and cached data will be returned. The method will work
916 * much faster, but this may result in the editor returning the data that is not up to date. This parameter
917 * should thus only be set to `true` when you are certain that the cached data is up to date.
918 *
919 * @returns {String} The editor data.
920 */
921 getData: function( internal ) {
922 !internal && this.fire( 'beforeGetData' );
923
924 var eventData = this._.data;
925
926 if ( typeof eventData != 'string' ) {
927 var element = this.element;
928 if ( element && this.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE )
929 eventData = element.is( 'textarea' ) ? element.getValue() : element.getHtml();
930 else
931 eventData = '';
932 }
933
934 eventData = { dataValue: eventData };
935
936 // Fire "getData" so data manipulation may happen.
937 !internal && this.fire( 'getData', eventData );
938
939 return eventData.dataValue;
940 },
941
942 /**
943 * Gets the "raw data" currently available in the editor. This is a
944 * fast method which returns the data as is, without processing, so it is
945 * not recommended to use it on resulting pages. Instead it can be used
946 * combined with the {@link #method-loadSnapshot} method in order
947 * to automatically save the editor data from time to time
948 * while the user is using the editor, to avoid data loss, without risking
949 * performance issues.
950 *
951 * alert( editor.getSnapshot() );
952 *
953 * See also:
954 *
955 * * {@link CKEDITOR.editor#method-getData}.
956 *
957 * @returns {String} Editor "raw data".
958 */
959 getSnapshot: function() {
960 var data = this.fire( 'getSnapshot' );
961
962 if ( typeof data != 'string' ) {
963 var element = this.element;
964
965 if ( element && this.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) {
966 data = element.is( 'textarea' ) ? element.getValue() : element.getHtml();
967 }
968 else {
969 // If we don't have a proper element, set data to an empty string,
970 // as this method is expected to return a string. (#13385)
971 data = '';
972 }
973 }
974
975 return data;
976 },
977
978 /**
979 * Loads "raw data" into the editor. The data is loaded with processing
980 * straight to the editing area. It should not be used as a way to load
981 * any kind of data, but instead in combination with
982 * {@link #method-getSnapshot}-produced data.
983 *
984 * var data = editor.getSnapshot();
985 * editor.loadSnapshot( data );
986 *
987 * @see CKEDITOR.editor#setData
988 */
989 loadSnapshot: function( snapshot ) {
990 this.fire( 'loadSnapshot', snapshot );
991 },
992
993 /**
994 * Sets the editor data. The data must be provided in the "raw" format (HTML).
995 *
996 * Note that this method is asynchronous. The `callback` parameter must
997 * be used if interaction with the editor is needed after setting the data.
998 *
999 * CKEDITOR.instances.editor1.setData( '<p>This is the editor data.</p>' );
1000 *
1001 * CKEDITOR.instances.editor1.setData( '<p>Some other editor data.</p>', {
1002 * callback: function() {
1003 * this.checkDirty(); // true
1004 * }
1005 * } );
1006 *
1007 * Note: In **CKEditor 4.4.2** the signature of this method has changed. All arguments
1008 * except `data` were wrapped into the `options` object. However, backward compatibility
1009 * was preserved and it is still possible to use the `data, callback, internal` arguments.
1010 *
1011 *
1012 * @param {String} data The HTML code to replace current editor content.
1013 * @param {Object} [options]
1014 * @param {Boolean} [options.internal=false] Whether to suppress any event firing when copying data internally inside the editor.
1015 * @param {Function} [options.callback] Function to be called after `setData` is completed (on {@link #dataReady}).
1016 * @param {Boolean} [options.noSnapshot=false] If set to `true`, it will prevent recording an undo snapshot.
1017 * Introduced in CKEditor 4.4.2.
1018 */
1019 setData: function( data, options, internal ) {
1020 var fireSnapshot = true,
1021 // Backward compatibility.
1022 callback = options,
1023 eventData;
1024
1025 if ( options && typeof options == 'object' ) {
1026 internal = options.internal;
1027 callback = options.callback;
1028 fireSnapshot = !options.noSnapshot;
1029 }
1030
1031 if ( !internal && fireSnapshot )
1032 this.fire( 'saveSnapshot' );
1033
1034 if ( callback || !internal ) {
1035 this.once( 'dataReady', function( evt ) {
1036 if ( !internal && fireSnapshot )
1037 this.fire( 'saveSnapshot' );
1038
1039 if ( callback )
1040 callback.call( evt.editor );
1041 } );
1042 }
1043
1044 // Fire "setData" so data manipulation may happen.
1045 eventData = { dataValue: data };
1046 !internal && this.fire( 'setData', eventData );
1047
1048 this._.data = eventData.dataValue;
1049
1050 !internal && this.fire( 'afterSetData', eventData );
1051 },
1052
1053 /**
1054 * Puts or restores the editor into the read-only state. When in read-only,
1055 * the user is not able to change the editor content, but can still use
1056 * some editor features. This function sets the {@link #property-readOnly}
1057 * property of the editor, firing the {@link #event-readOnly} event.
1058 *
1059 * **Note:** The current editing area will be reloaded.
1060 *
1061 * @since 3.6
1062 * @param {Boolean} [isReadOnly] Indicates that the editor must go
1063 * read-only (`true`, default) or be restored and made editable (`false`).
1064 */
1065 setReadOnly: function( isReadOnly ) {
1066 isReadOnly = ( isReadOnly == null ) || isReadOnly;
1067
1068 if ( this.readOnly != isReadOnly ) {
1069 this.readOnly = isReadOnly;
1070
1071 // Block or release BACKSPACE key according to current read-only
1072 // state to prevent browser's history navigation (#9761).
1073 this.keystrokeHandler.blockedKeystrokes[ 8 ] = +isReadOnly;
1074
1075 this.editable().setReadOnly( isReadOnly );
1076
1077 // Fire the readOnly event so the editor features can update
1078 // their state accordingly.
1079 this.fire( 'readOnly' );
1080 }
1081 },
1082
1083 /**
1084 * Inserts HTML code into the currently selected position in the editor in WYSIWYG mode.
1085 *
1086 * Example:
1087 *
1088 * CKEDITOR.instances.editor1.insertHtml( '<p>This is a new paragraph.</p>' );
1089 *
1090 * Fires the {@link #event-insertHtml} and {@link #event-afterInsertHtml} events. The HTML is inserted
1091 * in the {@link #event-insertHtml} event's listener with a default priority (10) so you can add listeners with
1092 * lower or higher priorities in order to execute some code before or after the HTML is inserted.
1093 *
1094 * @param {String} html HTML code to be inserted into the editor.
1095 * @param {String} [mode='html'] The mode in which the HTML code will be inserted. One of the following:
1096 *
1097 * * `'html'` &ndash; The inserted content will completely override the styles at the selected position.
1098 * * `'unfiltered_html'` &ndash; Like `'html'` but the content is not filtered with {@link CKEDITOR.filter}.
1099 * * `'text'` &ndash; The inserted content will inherit the styles applied in
1100 * the selected position. This mode should be used when inserting "htmlified" plain text
1101 * (HTML without inline styles and styling elements like `<b>`, `<strong>`, `<span style="...">`).
1102 *
1103 * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range
1104 * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case).
1105 * Introduced in CKEditor 4.5.
1106 */
1107 insertHtml: function( html, mode, range ) {
1108 this.fire( 'insertHtml', { dataValue: html, mode: mode, range: range } );
1109 },
1110
1111 /**
1112 * Inserts text content into the currently selected position in the
1113 * editor in WYSIWYG mode. The styles of the selected element will be applied to the inserted text.
1114 * Spaces around the text will be left untouched.
1115 *
1116 * CKEDITOR.instances.editor1.insertText( ' line1 \n\n line2' );
1117 *
1118 * Fires the {@link #event-insertText} and {@link #event-afterInsertHtml} events. The text is inserted
1119 * in the {@link #event-insertText} event's listener with a default priority (10) so you can add listeners with
1120 * lower or higher priorities in order to execute some code before or after the text is inserted.
1121 *
1122 * @since 3.5
1123 * @param {String} text Text to be inserted into the editor.
1124 */
1125 insertText: function( text ) {
1126 this.fire( 'insertText', text );
1127 },
1128
1129 /**
1130 * Inserts an element into the currently selected position in the editor in WYSIWYG mode.
1131 *
1132 * var element = CKEDITOR.dom.element.createFromHtml( '<img src="hello.png" border="0" title="Hello" />' );
1133 * CKEDITOR.instances.editor1.insertElement( element );
1134 *
1135 * Fires the {@link #event-insertElement} event. The element is inserted in the listener with a default priority (10),
1136 * so you can add listeners with lower or higher priorities in order to execute some code before or after
1137 * the element is inserted.
1138 *
1139 * @param {CKEDITOR.dom.element} element The element to be inserted into the editor.
1140 */
1141 insertElement: function( element ) {
1142 this.fire( 'insertElement', element );
1143 },
1144
1145 /**
1146 * Gets the selected HTML (it is returned as a {@link CKEDITOR.dom.documentFragment document fragment}
1147 * or a string). This method is designed to work as the user would expect the copy functionality to work.
1148 * For instance, if the following selection was made:
1149 *
1150 * <p>a<b>b{c}d</b>e</p>
1151 *
1152 * The following HTML will be returned:
1153 *
1154 * <b>c</b>
1155 *
1156 * As you can see, the information about the bold formatting was preserved, even though the selection was
1157 * anchored inside the `<b>` element.
1158 *
1159 * See also:
1160 *
1161 * * the {@link #extractSelectedHtml} method,
1162 * * the {@link CKEDITOR.editable#getHtmlFromRange} method.
1163 *
1164 * @since 4.5
1165 * @param {Boolean} [toString] If `true`, then stringified HTML will be returned.
1166 * @returns {CKEDITOR.dom.documentFragment/String}
1167 */
1168 getSelectedHtml: function( toString ) {
1169 var editable = this.editable(),
1170 selection = this.getSelection(),
1171 ranges = selection && selection.getRanges();
1172
1173 if ( !editable || !ranges || ranges.length === 0 ) {
1174 return null;
1175 }
1176
1177 var docFragment = createDocumentFragmentFromRanges( ranges, editable );
1178
1179 return toString ? docFragment.getHtml() : docFragment;
1180 },
1181
1182 /**
1183 * Gets the selected HTML (it is returned as a {@link CKEDITOR.dom.documentFragment document fragment}
1184 * or a string) and removes the selected part of the DOM. This method is designed to work as the user would
1185 * expect the cut and delete functionalities to work.
1186 *
1187 * See also:
1188 *
1189 * * the {@link #getSelectedHtml} method,
1190 * * the {@link CKEDITOR.editable#extractHtmlFromRange} method.
1191 *
1192 * @since 4.5
1193 * @param {Boolean} [toString] If `true`, then stringified HTML will be returned.
1194 * @param {Boolean} [removeEmptyBlock=false] Default `false` means that the function will keep an empty block (if the
1195 * entire content was removed) or it will create it (if a block element was removed) and set the selection in that block.
1196 * If `true`, the empty block will be removed or not created. In this case the function will not handle the selection.
1197 * @returns {CKEDITOR.dom.documentFragment/String/null}
1198 */
1199 extractSelectedHtml: function( toString, removeEmptyBlock ) {
1200 var editable = this.editable(),
1201 ranges = this.getSelection().getRanges();
1202
1203 if ( !editable || ranges.length === 0 ) {
1204 return null;
1205 }
1206
1207 var range = ranges[ 0 ],
1208 docFragment = editable.extractHtmlFromRange( range, removeEmptyBlock );
1209
1210 if ( !removeEmptyBlock ) {
1211 this.getSelection().selectRanges( [ range ] );
1212 }
1213
1214 return toString ? docFragment.getHtml() : docFragment;
1215 },
1216
1217 /**
1218 * Moves the selection focus to the editing area space in the editor.
1219 */
1220 focus: function() {
1221 this.fire( 'beforeFocus' );
1222 },
1223
1224 /**
1225 * Checks whether the current editor content contains changes when
1226 * compared to the content loaded into the editor at startup, or to
1227 * the content available in the editor when {@link #resetDirty}
1228 * was called.
1229 *
1230 * function beforeUnload( evt ) {
1231 * if ( CKEDITOR.instances.editor1.checkDirty() )
1232 * return evt.returnValue = "You will lose the changes made in the editor.";
1233 * }
1234 *
1235 * if ( window.addEventListener )
1236 * window.addEventListener( 'beforeunload', beforeUnload, false );
1237 * else
1238 * window.attachEvent( 'onbeforeunload', beforeUnload );
1239 *
1240 * @returns {Boolean} `true` if the content contains changes.
1241 */
1242 checkDirty: function() {
1243 return this.status == 'ready' && this._.previousValue !== this.getSnapshot();
1244 },
1245
1246 /**
1247 * Resets the "dirty state" of the editor so subsequent calls to
1248 * {@link #checkDirty} will return `false` if the user will not
1249 * have made further changes to the content.
1250 *
1251 * alert( editor.checkDirty() ); // e.g. true
1252 * editor.resetDirty();
1253 * alert( editor.checkDirty() ); // false
1254 */
1255 resetDirty: function() {
1256 this._.previousValue = this.getSnapshot();
1257 },
1258
1259 /**
1260 * Updates the `<textarea>` element that was replaced by the editor with
1261 * the current data available in the editor.
1262 *
1263 * **Note:** This method will only affect those editor instances created
1264 * with the {@link CKEDITOR#ELEMENT_MODE_REPLACE} element mode or inline instances
1265 * bound to `<textarea>` elements.
1266 *
1267 * CKEDITOR.instances.editor1.updateElement();
1268 * alert( document.getElementById( 'editor1' ).value ); // The current editor data.
1269 *
1270 * @see CKEDITOR.editor#element
1271 */
1272 updateElement: function() {
1273 return updateEditorElement.call( this );
1274 },
1275
1276 /**
1277 * Assigns keystrokes associated with editor commands.
1278 *
1279 * editor.setKeystroke( CKEDITOR.CTRL + 115, 'save' ); // Assigned Ctrl+S to the "save" command.
1280 * editor.setKeystroke( CKEDITOR.CTRL + 115, false ); // Disabled Ctrl+S keystroke assignment.
1281 * editor.setKeystroke( [
1282 * [ CKEDITOR.ALT + 122, false ],
1283 * [ CKEDITOR.CTRL + 121, 'link' ],
1284 * [ CKEDITOR.SHIFT + 120, 'bold' ]
1285 * ] );
1286 *
1287 * This method may be used in the following cases:
1288 *
1289 * * By plugins (like `link` or `basicstyles`) to set their keystrokes when plugins are being loaded.
1290 * * During the runtime to modify existing keystrokes.
1291 *
1292 * The editor handles keystroke configuration in the following order:
1293 *
1294 * 1. Plugins use this method to define default keystrokes.
1295 * 2. Editor extends default keystrokes with {@link CKEDITOR.config#keystrokes}.
1296 * 3. Editor blocks keystrokes defined in {@link CKEDITOR.config#blockedKeystrokes}.
1297 *
1298 * You can then set new keystrokes using this method during the runtime.
1299 *
1300 * @since 4.0
1301 * @param {Number/Array} keystroke A keystroke or an array of keystroke definitions.
1302 * @param {String/Boolean} [behavior] A command to be executed on the keystroke.
1303 */
1304 setKeystroke: function() {
1305 var keystrokes = this.keystrokeHandler.keystrokes,
1306 newKeystrokes = CKEDITOR.tools.isArray( arguments[ 0 ] ) ? arguments[ 0 ] : [ [].slice.call( arguments, 0 ) ],
1307 keystroke, behavior;
1308
1309 for ( var i = newKeystrokes.length; i--; ) {
1310 keystroke = newKeystrokes[ i ];
1311 behavior = 0;
1312
1313 // It may be a pair of: [ key, command ]
1314 if ( CKEDITOR.tools.isArray( keystroke ) ) {
1315 behavior = keystroke[ 1 ];
1316 keystroke = keystroke[ 0 ];
1317 }
1318
1319 if ( behavior )
1320 keystrokes[ keystroke ] = behavior;
1321 else
1322 delete keystrokes[ keystroke ];
1323 }
1324 },
1325
1326 /**
1327 * Returns the keystroke that is assigned to a specified {@link CKEDITOR.command}. If no keystroke is assigned,
1328 * it returns null.
1329 *
1330 * @since 4.6.0
1331 * @param {CKEDITOR.command} command
1332 * @returns {Number} The keystroke assigned to the provided command or null if there is no keystroke.
1333 */
1334 getCommandKeystroke: function( command ) {
1335 var commandName = command.name,
1336 keystrokes = this.keystrokeHandler.keystrokes,
1337 key;
1338
1339 // Some commands have a fake keystroke - for example CUT/COPY/PASTE commands are handled natively.
1340 if ( command.fakeKeystroke ) {
1341 return command.fakeKeystroke;
1342 }
1343
1344 for ( key in keystrokes ) {
1345 if ( keystrokes.hasOwnProperty( key ) && keystrokes[ key ] == commandName ) {
1346 return key;
1347 }
1348 }
1349
1350 return null;
1351 },
1352
1353 /**
1354 * Shorthand for {@link CKEDITOR.filter#addFeature}.
1355 *
1356 * @since 4.1
1357 * @param {CKEDITOR.feature} feature See {@link CKEDITOR.filter#addFeature}.
1358 * @returns {Boolean} See {@link CKEDITOR.filter#addFeature}.
1359 */
1360 addFeature: function( feature ) {
1361 return this.filter.addFeature( feature );
1362 },
1363
1364 /**
1365 * Sets the active filter ({@link #activeFilter}). Fires the {@link #activeFilterChange} event.
1366 *
1367 * // Set active filter which allows only 4 elements.
1368 * // Buttons like Bold, Italic will be disabled.
1369 * var filter = new CKEDITOR.filter( 'p strong em br' );
1370 * editor.setActiveFilter( filter );
1371 *
1372 * Setting a new filter will also change the {@link #setActiveEnterMode active Enter modes} to the first values
1373 * allowed by the new filter (see {@link CKEDITOR.filter#getAllowedEnterMode}).
1374 *
1375 * @since 4.3
1376 * @param {CKEDITOR.filter} filter Filter instance or a falsy value (e.g. `null`) to reset to the default one.
1377 */
1378 setActiveFilter: function( filter ) {
1379 if ( !filter )
1380 filter = this.filter;
1381
1382 if ( this.activeFilter !== filter ) {
1383 this.activeFilter = filter;
1384 this.fire( 'activeFilterChange' );
1385
1386 // Reset active filter to the main one - it resets enter modes, too.
1387 if ( filter === this.filter )
1388 this.setActiveEnterMode( null, null );
1389 else
1390 this.setActiveEnterMode(
1391 filter.getAllowedEnterMode( this.enterMode ),
1392 filter.getAllowedEnterMode( this.shiftEnterMode, true )
1393 );
1394 }
1395 },
1396
1397 /**
1398 * Sets the active Enter modes: ({@link #enterMode} and {@link #shiftEnterMode}).
1399 * Fires the {@link #activeEnterModeChange} event.
1400 *
1401 * Prior to CKEditor 4.3 Enter modes were static and it was enough to check {@link CKEDITOR.config#enterMode}
1402 * and {@link CKEDITOR.config#shiftEnterMode} when implementing a feature which should depend on the Enter modes.
1403 * Since CKEditor 4.3 these options are source of initial:
1404 *
1405 * * static {@link #enterMode} and {@link #shiftEnterMode} values,
1406 * * dynamic {@link #activeEnterMode} and {@link #activeShiftEnterMode} values.
1407 *
1408 * However, the dynamic Enter modes can be changed during runtime by using this method, to reflect the selection context.
1409 * For example, if selection is moved to the {@link CKEDITOR.plugins.widget widget}'s nested editable which
1410 * is a {@link #blockless blockless one}, then the active Enter modes should be changed to {@link CKEDITOR#ENTER_BR}
1411 * (in this case [Widget System](#!/guide/dev_widgets) takes care of that).
1412 *
1413 * **Note:** This method should not be used to configure the editor &ndash; use {@link CKEDITOR.config#enterMode} and
1414 * {@link CKEDITOR.config#shiftEnterMode} instead. This method should only be used to dynamically change
1415 * Enter modes during runtime based on selection changes.
1416 * Keep in mind that changed Enter mode may be overwritten by another plugin/feature when it decided that
1417 * the changed context requires this.
1418 *
1419 * **Note:** In case of blockless editor (inline editor based on an element which cannot contain block elements
1420 * &mdash; see {@link CKEDITOR.editor#blockless}) only {@link CKEDITOR#ENTER_BR} is a valid Enter mode. Therefore
1421 * this method will not allow to set other values.
1422 *
1423 * **Note:** Changing the {@link #activeFilter active filter} may cause the Enter mode to change if default Enter modes
1424 * are not allowed by the new filter.
1425 *
1426 * @since 4.3
1427 * @param {Number} enterMode One of {@link CKEDITOR#ENTER_P}, {@link CKEDITOR#ENTER_DIV}, {@link CKEDITOR#ENTER_BR}.
1428 * Pass falsy value (e.g. `null`) to reset the Enter mode to the default value ({@link #enterMode} and/or {@link #shiftEnterMode}).
1429 * @param {Number} shiftEnterMode See the `enterMode` argument.
1430 */
1431 setActiveEnterMode: function( enterMode, shiftEnterMode ) {
1432 // Validate passed modes or use default ones (validated on init).
1433 enterMode = enterMode ? validateEnterMode( this, enterMode ) : this.enterMode;
1434 shiftEnterMode = shiftEnterMode ? validateEnterMode( this, shiftEnterMode ) : this.shiftEnterMode;
1435
1436 if ( this.activeEnterMode != enterMode || this.activeShiftEnterMode != shiftEnterMode ) {
1437 this.activeEnterMode = enterMode;
1438 this.activeShiftEnterMode = shiftEnterMode;
1439 this.fire( 'activeEnterModeChange' );
1440 }
1441 },
1442
1443 /**
1444 * Shows a notification to the user.
1445 *
1446 * If the [Notification](http://ckeditor.com/addons/notification) plugin is not enabled, this function shows
1447 * a normal alert with the given `message`. The `type` and `progressOrDuration` parameters are supported
1448 * only by the Notification plugin.
1449 *
1450 * If the Notification plugin is enabled, this method creates and shows a new notification.
1451 * By default the notification is shown over the editor content, in the viewport if it is possible.
1452 *
1453 * See {@link CKEDITOR.plugins.notification}.
1454 *
1455 * @since 4.5
1456 * @member CKEDITOR.editor
1457 * @param {String} message The message displayed in the notification.
1458 * @param {String} [type='info'] The type of the notification. Can be `'info'`, `'warning'`, `'success'` or `'progress'`.
1459 * @param {Number} [progressOrDuration] If the type is `progress`, the third parameter may be a progress from `0` to `1`
1460 * (defaults to `0`). Otherwise the third parameter may be a notification duration denoting after how many milliseconds
1461 * the notification should be closed automatically. `0` means that the notification will not close automatically and the user
1462 * needs to close it manually. See {@link CKEDITOR.plugins.notification#duration}.
1463 * Note that `warning` notifications will not be closed automatically.
1464 * @returns {CKEDITOR.plugins.notification} Created and shown notification.
1465 */
1466 showNotification: function( message ) {
1467 alert( message ); // jshint ignore:line
1468 }
1469 } );
1470} )();
1471
1472/**
1473 * The editor has no associated element.
1474 *
1475 * @readonly
1476 * @property {Number} [=0]
1477 * @member CKEDITOR
1478 */
1479CKEDITOR.ELEMENT_MODE_NONE = 0;
1480
1481/**
1482 * The element is to be replaced by the editor instance.
1483 *
1484 * @readonly
1485 * @property {Number} [=1]
1486 * @member CKEDITOR
1487 */
1488CKEDITOR.ELEMENT_MODE_REPLACE = 1;
1489
1490/**
1491 * The editor is to be created inside the element.
1492 *
1493 * @readonly
1494 * @property {Number} [=2]
1495 * @member CKEDITOR
1496 */
1497CKEDITOR.ELEMENT_MODE_APPENDTO = 2;
1498
1499/**
1500 * The editor is to be attached to the element, using it as the editing block.
1501 *
1502 * @readonly
1503 * @property {Number} [=3]
1504 * @member CKEDITOR
1505 */
1506CKEDITOR.ELEMENT_MODE_INLINE = 3;
1507
1508/**
1509 * Whether to escape HTML when the editor updates the original input element.
1510 *
1511 * config.htmlEncodeOutput = true;
1512 *
1513 * @since 3.1
1514 * @cfg {Boolean} [htmlEncodeOutput=false]
1515 * @member CKEDITOR.config
1516 */
1517
1518/**
1519 * If `true`, makes the editor start in read-only state. Otherwise, it will check
1520 * if the linked `<textarea>` element has the `disabled` attribute.
1521 *
1522 * Read more in the [documentation](#!/guide/dev_readonly)
1523 * and see the [SDK sample](http://sdk.ckeditor.com/samples/readonly.html).
1524 *
1525 * config.readOnly = true;
1526 *
1527 * @since 3.6
1528 * @cfg {Boolean} [readOnly=false]
1529 * @member CKEDITOR.config
1530 * @see CKEDITOR.editor#setReadOnly
1531 */
1532
1533/**
1534 * Whether an editable element should have focus when the editor is loading for the first time.
1535 *
1536 * config.startupFocus = true;
1537 *
1538 * @cfg {Boolean} [startupFocus=false]
1539 * @member CKEDITOR.config
1540 */
1541
1542 /**
1543 * Customizes the {@link CKEDITOR.editor#title human-readable title} of this editor. This title is displayed in
1544 * tooltips and impacts various [accessibility aspects](#!/guide/dev_a11y-section-announcing-the-editor-on-the-page),
1545 * e.g. it is commonly used by screen readers for distinguishing editor instances and for navigation.
1546 * Accepted values are a string or `false`.
1547 *
1548 * **Note:** When `config.title` is set globally, the same value will be applied to all editor instances
1549 * loaded with this config. This may adversely affect accessibility as screen reader users will be unable
1550 * to distinguish particular editor instances and navigate between them.
1551 *
1552 * **Note:** Setting `config.title = false` may also impair accessibility in a similar way.
1553 *
1554 * **Note:** Please do not confuse this property with {@link CKEDITOR.editor#name}
1555 * which identifies the instance in the {@link CKEDITOR#instances} literal.
1556 *
1557 * // Sets the title to 'My WYSIWYG editor.'. The original title of the element (if it exists)
1558 * // will be restored once the editor instance is destroyed.
1559 * config.title = 'My WYSIWYG editor.';
1560 *
1561 * // Do not touch the title. If the element already has a title, it remains unchanged.
1562 * // Also if no `title` attribute exists, nothing new will be added.
1563 * config.title = false;
1564 *
1565 * See also:
1566 *
1567 * * CKEDITOR.editor#name
1568 * * CKEDITOR.editor#title
1569 *
1570 * @since 4.2
1571 * @cfg {String/Boolean} [title=based on editor.name]
1572 * @member CKEDITOR.config
1573 */
1574
1575/**
1576 * Sets listeners on editor events.
1577 *
1578 * **Note:** This property can only be set in the `config` object passed directly
1579 * to {@link CKEDITOR#replace}, {@link CKEDITOR#inline}, and other creators.
1580 *
1581 * CKEDITOR.replace( 'editor1', {
1582 * on: {
1583 * instanceReady: function() {
1584 * alert( this.name ); // 'editor1'
1585 * },
1586 *
1587 * key: function() {
1588 * // ...
1589 * }
1590 * }
1591 * } );
1592 *
1593 * @cfg {Object} on
1594 * @member CKEDITOR.config
1595 */
1596
1597/**
1598 * The outermost element in the DOM tree in which the editable element resides. It is provided
1599 * by a specific editor creator after the editor UI is created and is not intended to
1600 * be modified.
1601 *
1602 * var editor = CKEDITOR.instances.editor1;
1603 * alert( editor.container.getName() ); // 'span'
1604 *
1605 * @readonly
1606 * @property {CKEDITOR.dom.element} container
1607 */
1608
1609/**
1610 * The document that stores the editor content.
1611 *
1612 * * For the classic (`iframe`-based) editor it is equal to the document inside the
1613 * `iframe` containing the editable element.
1614 * * For the inline editor it is equal to {@link CKEDITOR#document}.
1615 *
1616 * The document object is available after the {@link #contentDom} event is fired
1617 * and may be invalidated when the {@link #contentDomUnload} event is fired
1618 * (classic editor only).
1619 *
1620 * editor.on( 'contentDom', function() {
1621 * console.log( editor.document );
1622 * } );
1623 *
1624 * @readonly
1625 * @property {CKEDITOR.dom.document} document
1626 */
1627
1628/**
1629 * The window instance related to the {@link #document} property.
1630 *
1631 * It is always equal to the `editor.document.getWindow()`.
1632 *
1633 * See the {@link #document} property documentation.
1634 *
1635 * @readonly
1636 * @property {CKEDITOR.dom.window} window
1637 */
1638
1639/**
1640 * The main filter instance used for input data filtering, data
1641 * transformations, and activation of features.
1642 *
1643 * It points to a {@link CKEDITOR.filter} instance set up based on
1644 * editor configuration.
1645 *
1646 * @since 4.1
1647 * @readonly
1648 * @property {CKEDITOR.filter} filter
1649 */
1650
1651/**
1652 * The active filter instance which should be used in the current context (location selection).
1653 * This instance will be used to make a decision which commands, buttons and other
1654 * {@link CKEDITOR.feature features} can be enabled.
1655 *
1656 * By default it equals the {@link #filter} and it can be changed by the {@link #setActiveFilter} method.
1657 *
1658 * editor.on( 'activeFilterChange', function() {
1659 * if ( editor.activeFilter.check( 'cite' ) )
1660 * // Do something when <cite> was enabled - e.g. enable a button.
1661 * else
1662 * // Otherwise do something else.
1663 * } );
1664 *
1665 * See also the {@link #setActiveEnterMode} method for an explanation of dynamic settings.
1666 *
1667 * @since 4.3
1668 * @readonly
1669 * @property {CKEDITOR.filter} activeFilter
1670 */
1671
1672/**
1673 * The main (static) Enter mode which is a validated version of the {@link CKEDITOR.config#enterMode} setting.
1674 * Currently only one rule exists &mdash; {@link #blockless blockless editors} may have
1675 * Enter modes set only to {@link CKEDITOR#ENTER_BR}.
1676 *
1677 * @since 4.3
1678 * @readonly
1679 * @property {Number} enterMode
1680 */
1681
1682/**
1683 * See the {@link #enterMode} property.
1684 *
1685 * @since 4.3
1686 * @readonly
1687 * @property {Number} shiftEnterMode
1688 */
1689
1690/**
1691 * The dynamic Enter mode which should be used in the current context (selection location).
1692 * By default it equals the {@link #enterMode} and it can be changed by the {@link #setActiveEnterMode} method.
1693 *
1694 * See also the {@link #setActiveEnterMode} method for an explanation of dynamic settings.
1695 *
1696 * @since 4.3
1697 * @readonly
1698 * @property {Number} activeEnterMode
1699 */
1700
1701/**
1702 * See the {@link #activeEnterMode} property.
1703 *
1704 * @since 4.3
1705 * @readonly
1706 * @property {Number} activeShiftEnterMode
1707 */
1708
1709/**
1710 * Event fired by the {@link #setActiveFilter} method when the {@link #activeFilter} is changed.
1711 *
1712 * @since 4.3
1713 * @event activeFilterChange
1714 */
1715
1716/**
1717 * Event fired by the {@link #setActiveEnterMode} method when any of the active Enter modes is changed.
1718 * See also the {@link #activeEnterMode} and {@link #activeShiftEnterMode} properties.
1719 *
1720 * @since 4.3
1721 * @event activeEnterModeChange
1722 */
1723
1724/**
1725 * Event fired when a CKEDITOR instance is created, but still before initializing it.
1726 * To interact with a fully initialized instance, use the
1727 * {@link CKEDITOR#instanceReady} event instead.
1728 *
1729 * @event instanceCreated
1730 * @member CKEDITOR
1731 * @param {CKEDITOR.editor} editor The editor instance that has been created.
1732 */
1733
1734/**
1735 * Event fired when CKEDITOR instance's components (configuration, languages and plugins) are fully
1736 * loaded and initialized. However, the editor will be fully ready for interaction
1737 * on {@link CKEDITOR#instanceReady}.
1738 *
1739 * @event instanceLoaded
1740 * @member CKEDITOR
1741 * @param {CKEDITOR.editor} editor This editor instance that has been loaded.
1742 */
1743
1744/**
1745 * Event fired when a CKEDITOR instance is destroyed.
1746 *
1747 * @event instanceDestroyed
1748 * @member CKEDITOR
1749 * @param {CKEDITOR.editor} editor The editor instance that has been destroyed.
1750 */
1751
1752/**
1753 * Event fired when a CKEDITOR instance is created, fully initialized and ready for interaction.
1754 *
1755 * @event instanceReady
1756 * @member CKEDITOR
1757 * @param {CKEDITOR.editor} editor The editor instance that has been created.
1758 */
1759
1760/**
1761 * Event fired when the language is loaded into the editor instance.
1762 *
1763 * @since 3.6.1
1764 * @event langLoaded
1765 * @param {CKEDITOR.editor} editor This editor instance.
1766 */
1767
1768/**
1769 * Event fired when all plugins are loaded and initialized into the editor instance.
1770 *
1771 * @event pluginsLoaded
1772 * @param {CKEDITOR.editor} editor This editor instance.
1773 */
1774
1775/**
1776 * Event fired when the styles set is loaded. During the editor initialization
1777 * phase the {@link #getStylesSet} method returns only styles that
1778 * are already loaded, which may not include e.g. styles parsed
1779 * by the `stylesheetparser` plugin. Thus, to be notified when all
1780 * styles are ready, you can listen on this event.
1781 *
1782 * @since 4.1
1783 * @event stylesSet
1784 * @param {CKEDITOR.editor} editor This editor instance.
1785 * @param {Array} styles An array of styles definitions.
1786 */
1787
1788/**
1789 * Event fired before the command execution when {@link #execCommand} is called.
1790 *
1791 * @event beforeCommandExec
1792 * @param {CKEDITOR.editor} editor This editor instance.
1793 * @param data
1794 * @param {String} data.name The command name.
1795 * @param {Object} data.commandData The data to be sent to the command. This
1796 * can be manipulated by the event listener.
1797 * @param {CKEDITOR.command} data.command The command itself.
1798 */
1799
1800/**
1801 * Event fired after the command execution when {@link #execCommand} is called.
1802 *
1803 * @event afterCommandExec
1804 * @param {CKEDITOR.editor} editor This editor instance.
1805 * @param data
1806 * @param {String} data.name The command name.
1807 * @param {Object} data.commandData The data sent to the command.
1808 * @param {CKEDITOR.command} data.command The command itself.
1809 * @param {Object} data.returnValue The value returned by the command execution.
1810 */
1811
1812/**
1813 * Event fired when a custom configuration file is loaded, before the final
1814 * configuration initialization.
1815 *
1816 * Custom configuration files can be loaded thorugh the
1817 * {@link CKEDITOR.config#customConfig} setting. Several files can be loaded
1818 * by changing this setting.
1819 *
1820 * @event customConfigLoaded
1821 * @param {CKEDITOR.editor} editor This editor instance.
1822 */
1823
1824/**
1825 * Event fired once the editor configuration is ready (loaded and processed).
1826 *
1827 * @event configLoaded
1828 * @param {CKEDITOR.editor} editor This editor instance.
1829 */
1830
1831/**
1832 * Event fired when this editor instance is destroyed. The editor at this
1833 * point is not usable and this event should be used to perform the clean-up
1834 * in any plugin.
1835 *
1836 * @event destroy
1837 * @param {CKEDITOR.editor} editor This editor instance.
1838 */
1839
1840/**
1841 * Event fired when the {@link #method-destroy} method is called,
1842 * but before destroying the editor.
1843 *
1844 * @event beforeDestroy
1845 * @param {CKEDITOR.editor} editor This editor instance.
1846 */
1847
1848/**
1849 * Internal event to get the current data.
1850 *
1851 * @event beforeGetData
1852 * @param {CKEDITOR.editor} editor This editor instance.
1853 */
1854
1855/**
1856 * Internal event to perform the {@link #method-getSnapshot} call.
1857 *
1858 * @event getSnapshot
1859 * @param {CKEDITOR.editor} editor This editor instance.
1860 */
1861
1862/**
1863 * Internal event to perform the {@link #method-loadSnapshot} call.
1864 *
1865 * @event loadSnapshot
1866 * @param {CKEDITOR.editor} editor This editor instance.
1867 * @param {String} data The data that will be used.
1868 */
1869
1870/**
1871 * Event fired before the {@link #method-getData} call returns, allowing for additional manipulation.
1872 *
1873 * @event getData
1874 * @param {CKEDITOR.editor} editor This editor instance.
1875 * @param data
1876 * @param {String} data.dataValue The data that will be returned.
1877 */
1878
1879/**
1880 * Event fired before the {@link #method-setData} call is executed, allowing for additional manipulation.
1881 *
1882 * @event setData
1883 * @param {CKEDITOR.editor} editor This editor instance.
1884 * @param data
1885 * @param {String} data.dataValue The data that will be used.
1886 */
1887
1888/**
1889 * Event fired at the end of the {@link #method-setData} call execution. Usually it is better to use the
1890 * {@link #dataReady} event.
1891 *
1892 * @event afterSetData
1893 * @param {CKEDITOR.editor} editor This editor instance.
1894 * @param data
1895 * @param {String} data.dataValue The data that has been set.
1896 */
1897
1898/**
1899 * Event fired as an indicator of the editor data loading. It may be the result of
1900 * calling {@link #method-setData} explicitly or an internal
1901 * editor function, like the editor editing mode switching (move to Source and back).
1902 *
1903 * @event dataReady
1904 * @param {CKEDITOR.editor} editor This editor instance.
1905 */
1906
1907/**
1908 * Event fired when the CKEDITOR instance is completely created, fully initialized
1909 * and ready for interaction.
1910 *
1911 * @event instanceReady
1912 * @param {CKEDITOR.editor} editor This editor instance.
1913 */
1914
1915/**
1916 * Event fired when editor components (configuration, languages and plugins) are fully
1917 * loaded and initialized. However, the editor will be fully ready to for interaction
1918 * on {@link #instanceReady}.
1919 *
1920 * @event loaded
1921 * @param {CKEDITOR.editor} editor This editor instance.
1922 */
1923
1924/**
1925 * Event fired by the {@link #method-insertHtml} method. See the method documentation for more information
1926 * about how this event can be used.
1927 *
1928 * @event insertHtml
1929 * @param {CKEDITOR.editor} editor This editor instance.
1930 * @param data
1931 * @param {String} data.mode The mode in which the data is inserted (see {@link #method-insertHtml}).
1932 * @param {String} data.dataValue The HTML code to insert.
1933 * @param {CKEDITOR.dom.range} [data.range] See {@link #method-insertHtml}'s `range` parameter.
1934 */
1935
1936/**
1937 * Event fired by the {@link #method-insertText} method. See the method documentation for more information
1938 * about how this event can be used.
1939 *
1940 * @event insertText
1941 * @param {CKEDITOR.editor} editor This editor instance.
1942 * @param {String} data The text to insert.
1943 */
1944
1945/**
1946 * Event fired by the {@link #method-insertElement} method. See the method documentation for more information
1947 * about how this event can be used.
1948 *
1949 * @event insertElement
1950 * @param {CKEDITOR.editor} editor This editor instance.
1951 * @param {CKEDITOR.dom.element} data The element to insert.
1952 */
1953
1954/**
1955 * Event fired after data insertion using the {@link #method-insertHtml}, {@link CKEDITOR.editable#insertHtml},
1956 * or {@link CKEDITOR.editable#insertHtmlIntoRange} methods.
1957 *
1958 * @since 4.5
1959 * @event afterInsertHtml
1960 * @param data
1961 * @param {CKEDITOR.dom.range} [data.intoRange] If set, the HTML was not inserted into the current selection, but into
1962 * the specified range. This property is set if the {@link CKEDITOR.editable#insertHtmlIntoRange} method was used,
1963 * but not if for the {@link CKEDITOR.editable#insertHtml} method.
1964 */
1965
1966/**
1967 * Event fired after the {@link #property-readOnly} property changes.
1968 *
1969 * @since 3.6
1970 * @event readOnly
1971 * @param {CKEDITOR.editor} editor This editor instance.
1972 */
1973
1974/**
1975 * Event fired when a UI template is added to the editor instance. It makes
1976 * it possible to bring customizations to the template source.
1977 *
1978 * @event template
1979 * @param {CKEDITOR.editor} editor This editor instance.
1980 * @param data
1981 * @param {String} data.name The template name.
1982 * @param {String} data.source The source data for this template.
1983 */
1984
1985/**
1986 * Event fired when the editor content (its DOM structure) is ready.
1987 * It is similar to the native `DOMContentLoaded` event, but it applies to
1988 * the editor content. It is also the first event fired after
1989 * the {@link CKEDITOR.editable} is initialized.
1990 *
1991 * This event is particularly important for classic (`iframe`-based)
1992 * editor, because on editor initialization and every time the data are set
1993 * (by {@link CKEDITOR.editor#method-setData}) content DOM structure
1994 * is rebuilt. Thus, e.g. you need to attach DOM event listeners
1995 * on editable one more time.
1996 *
1997 * For inline editor this event is fired only once &mdash; when the
1998 * editor is initialized for the first time. This is because setting
1999 * editor content does not cause editable destruction and creation.
2000 *
2001 * The {@link #contentDom} event goes along with {@link #contentDomUnload}
2002 * which is fired before the content DOM structure is destroyed. This is the
2003 * right moment to detach content DOM event listener. Otherwise
2004 * browsers like IE or Opera may throw exceptions when accessing
2005 * elements from the detached document.
2006 *
2007 * **Note:** {@link CKEDITOR.editable#attachListener} is a convenient
2008 * way to attach listeners that will be detached on {@link #contentDomUnload}.
2009 *
2010 * editor.on( 'contentDom', function() {
2011 * var editable = editor.editable();
2012 *
2013 * editable.attachListener( editable, 'click', function() {
2014 * console.log( 'The editable was clicked.' );
2015 * });
2016 * });
2017 *
2018 * @event contentDom
2019 * @param {CKEDITOR.editor} editor This editor instance.
2020 */
2021
2022/**
2023 * Event fired before the content DOM structure is destroyed.
2024 * See {@link #contentDom} documentation for more details.
2025 *
2026 * @event contentDomUnload
2027 * @param {CKEDITOR.editor} editor This editor instance.
2028 */
2029
2030/**
2031 * Event fired when the content DOM changes and some of the references as well as
2032 * the native DOM event listeners could be lost.
2033 * This event is useful when it is important to keep track of references
2034 * to elements in the editable content from code.
2035 *
2036 * @since 4.3
2037 * @event contentDomInvalidated
2038 * @param {CKEDITOR.editor} editor This editor instance.
2039 */
diff --git a/sources/core/editor_basic.js b/sources/core/editor_basic.js
new file mode 100644
index 0000000..b7ab577
--- /dev/null
+++ b/sources/core/editor_basic.js
@@ -0,0 +1,36 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
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 0000000..43b608a
--- /dev/null
+++ b/sources/core/env.js
@@ -0,0 +1,361 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 0000000..89444b5
--- /dev/null
+++ b/sources/core/event.js
@@ -0,0 +1,389 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 0000000..ea62ac9
--- /dev/null
+++ b/sources/core/eventInfo.js
@@ -0,0 +1,115 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the "virtual" {@link CKEDITOR.eventInfo} class, which
8 * contains the defintions of the event object passed to event listeners.
9 * This file is for documentation purposes only.
10 */
11
12/**
13 * Virtual class that illustrates the features of the event object to be
14 * passed to event listeners by a {@link CKEDITOR.event} based object.
15 *
16 * This class is not really part of the API.
17 *
18 * @class CKEDITOR.eventInfo
19 * @abstract
20 */
21
22/**
23 * The event name.
24 *
25 * someObject.on( 'someEvent', function( event ) {
26 * alert( event.name ); // 'someEvent'
27 * } );
28 * someObject.fire( 'someEvent' );
29 *
30 * @property {String} name
31 */
32
33/**
34 * The object that publishes (sends) the event.
35 *
36 * someObject.on( 'someEvent', function( event ) {
37 * alert( event.sender == someObject ); // true
38 * } );
39 * someObject.fire( 'someEvent' );
40 *
41 * @property sender
42 */
43
44/**
45 * The editor instance that holds the sender. May be the same as sender. May be
46 * null if the sender is not part of an editor instance, like a component
47 * running in standalone mode.
48 *
49 * myButton.on( 'someEvent', function( event ) {
50 * alert( event.editor == myEditor ); // true
51 * } );
52 * myButton.fire( 'someEvent', null, myEditor );
53 *
54 * @property {CKEDITOR.editor} editor
55 */
56
57/**
58 * Any kind of additional data. Its format and usage is event dependent.
59 *
60 * someObject.on( 'someEvent', function( event ) {
61 * alert( event.data ); // 'Example'
62 * } );
63 * someObject.fire( 'someEvent', 'Example' );
64 *
65 * @property data
66 */
67
68/**
69 * Any extra data appended during the listener registration.
70 *
71 * someObject.on( 'someEvent', function( event ) {
72 * alert( event.listenerData ); // 'Example'
73 * }, null, 'Example' );
74 *
75 * @property listenerData
76 */
77
78/**
79 * Indicates that no further listeners are to be called.
80 *
81 * someObject.on( 'someEvent', function( event ) {
82 * event.stop();
83 * } );
84 * someObject.on( 'someEvent', function( event ) {
85 * // This one will not be called.
86 * } );
87 * alert( someObject.fire( 'someEvent' ) ); // true
88 *
89 * @method stop
90 */
91
92/**
93 * Indicates that the event is to be cancelled (if cancelable).
94 *
95 * someObject.on( 'someEvent', function( event ) {
96 * event.cancel();
97 * } );
98 * someObject.on( 'someEvent', function( event ) {
99 * // This one will not be called.
100 * } );
101 * alert( someObject.fire( 'someEvent' ) ); // false
102 *
103 * @method cancel
104 */
105
106/**
107 * Removes the current listener.
108 *
109 * someObject.on( 'someEvent', function( event ) {
110 * event.removeListener();
111 * // Now this function won't be called again by 'someEvent'.
112 * } );
113 *
114 * @method removeListener
115 */
diff --git a/sources/core/filter.js b/sources/core/filter.js
new file mode 100644
index 0000000..db68530
--- /dev/null
+++ b/sources/core/filter.js
@@ -0,0 +1,2540 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 '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 && !CKEDITOR.tools.isEmpty( styles ) ) {
1252 styles = copy( styles );
1253 attrs.style = CKEDITOR.tools.writeCssText( styles, true );
1254 } else {
1255 styles = {};
1256 }
1257
1258 return {
1259 name: styleDef.element,
1260 attributes: attrs,
1261 classes: attrs[ 'class' ] ? attrs[ 'class' ].split( /\s+/ ) : [],
1262 styles: styles,
1263 children: []
1264 };
1265 }
1266
1267 // Mock hash based on string.
1268 // 'a,b,c' => { a: 'cke-test', b: 'cke-test', c: 'cke-test' }
1269 // Used to mock styles and attributes objects.
1270 function mockHash( str ) {
1271 // It may be a null or empty string.
1272 if ( !str )
1273 return {};
1274
1275 var keys = str.split( /\s*,\s*/ ).sort(),
1276 obj = {};
1277
1278 while ( keys.length )
1279 obj[ keys.shift() ] = TEST_VALUE;
1280
1281 return obj;
1282 }
1283
1284 // Extract properties names from the object
1285 // and replace those containing wildcards with regexps.
1286 // Note: there's a room for performance improvement. Array of mixed types
1287 // breaks JIT-compiler optiomization what may invalidate compilation of pretty a lot of code.
1288 //
1289 // @returns An array of strings and regexps.
1290 function optimizeRequiredProperties( requiredProperties ) {
1291 var arr = [];
1292 for ( var propertyName in requiredProperties ) {
1293 if ( propertyName.indexOf( '*' ) > -1 )
1294 arr.push( new RegExp( '^' + propertyName.replace( /\*/g, '.*' ) + '$' ) );
1295 else
1296 arr.push( propertyName );
1297 }
1298 return arr;
1299 }
1300
1301 var validators = { styles: 1, attributes: 1, classes: 1 },
1302 validatorsRequired = {
1303 styles: 'requiredStyles',
1304 attributes: 'requiredAttributes',
1305 classes: 'requiredClasses'
1306 };
1307
1308 // Optimize a rule by replacing validators with functions
1309 // and rewriting requiredXXX validators to arrays.
1310 function optimizeRule( rule ) {
1311 var validatorName,
1312 requiredProperties,
1313 i;
1314
1315 for ( validatorName in validators )
1316 rule[ validatorName ] = validatorFunction( rule[ validatorName ] );
1317
1318 var nothingRequired = true;
1319 for ( i in validatorsRequired ) {
1320 validatorName = validatorsRequired[ i ];
1321 requiredProperties = optimizeRequiredProperties( rule[ validatorName ] );
1322 // Don't set anything if there are no required properties. This will allow to
1323 // save some memory by GCing all empty arrays (requiredProperties).
1324 if ( requiredProperties.length ) {
1325 rule[ validatorName ] = requiredProperties;
1326 nothingRequired = false;
1327 }
1328 }
1329
1330 rule.nothingRequired = nothingRequired;
1331 rule.noProperties = !( rule.attributes || rule.classes || rule.styles );
1332 }
1333
1334 // Add optimized version of rule to optimizedRules object.
1335 function optimizeRules( optimizedRules, rules ) {
1336 var elementsRules = optimizedRules.elements,
1337 genericRules = optimizedRules.generic,
1338 i, l, rule, element, priority;
1339
1340 for ( i = 0, l = rules.length; i < l; ++i ) {
1341 // Shallow copy. Do not modify original rule.
1342 rule = copy( rules[ i ] );
1343 priority = rule.classes === true || rule.styles === true || rule.attributes === true;
1344 optimizeRule( rule );
1345
1346 // E.g. "*(xxx)[xxx]" - it's a generic rule that
1347 // validates properties only.
1348 // Or '$1': { match: function() {...} }
1349 if ( rule.elements === true || rule.elements === null ) {
1350 // Add priority rules at the beginning.
1351 genericRules[ priority ? 'unshift' : 'push' ]( rule );
1352 }
1353 // If elements list was explicitly defined,
1354 // add this rule for every defined element.
1355 else {
1356 // We don't need elements validator for this kind of rule.
1357 var elements = rule.elements;
1358 delete rule.elements;
1359
1360 for ( element in elements ) {
1361 if ( !elementsRules[ element ] )
1362 elementsRules[ element ] = [ rule ];
1363 else
1364 elementsRules[ element ][ priority ? 'unshift' : 'push' ]( rule );
1365 }
1366 }
1367 }
1368 }
1369
1370 // < elements >< styles, attributes and classes >< separator >
1371 var rulePattern = /^([a-z0-9\-*\s]+)((?:\s*\{[!\w\-,\s\*]+\}\s*|\s*\[[!\w\-,\s\*]+\]\s*|\s*\([!\w\-,\s\*]+\)\s*){0,3})(?:;\s*|$)/i,
1372 groupsPatterns = {
1373 styles: /{([^}]+)}/,
1374 attrs: /\[([^\]]+)\]/,
1375 classes: /\(([^\)]+)\)/
1376 };
1377
1378 function parseRulesString( input ) {
1379 var match,
1380 props, styles, attrs, classes,
1381 rules = {},
1382 groupNum = 1;
1383
1384 input = trim( input );
1385
1386 while ( ( match = input.match( rulePattern ) ) ) {
1387 if ( ( props = match[ 2 ] ) ) {
1388 styles = parseProperties( props, 'styles' );
1389 attrs = parseProperties( props, 'attrs' );
1390 classes = parseProperties( props, 'classes' );
1391 } else {
1392 styles = attrs = classes = null;
1393 }
1394
1395 // Add as an unnamed rule, because there can be two rules
1396 // for one elements set defined in string format.
1397 rules[ '$' + groupNum++ ] = {
1398 elements: match[ 1 ],
1399 classes: classes,
1400 styles: styles,
1401 attributes: attrs
1402 };
1403
1404 // Move to the next group.
1405 input = input.slice( match[ 0 ].length );
1406 }
1407
1408 return rules;
1409 }
1410
1411 // Extract specified properties group (styles, attrs, classes) from
1412 // what stands after the elements list in string format of allowedContent.
1413 function parseProperties( properties, groupName ) {
1414 var group = properties.match( groupsPatterns[ groupName ] );
1415 return group ? trim( group[ 1 ] ) : null;
1416 }
1417
1418 function populateProperties( element ) {
1419 // Backup styles and classes, because they may be removed by DACRs.
1420 // We'll need them in updateElement().
1421 var styles = element.styleBackup = element.attributes.style,
1422 classes = element.classBackup = element.attributes[ 'class' ];
1423
1424 // Parse classes and styles if that hasn't been done before.
1425 if ( !element.styles )
1426 element.styles = CKEDITOR.tools.parseCssText( styles || '', 1 );
1427 if ( !element.classes )
1428 element.classes = classes ? classes.split( /\s+/ ) : [];
1429 }
1430
1431 // Filter element protected with a comment.
1432 // Returns true if protected content is ok, false otherwise.
1433 function processProtectedElement( that, comment, protectedRegexs, filterOpts ) {
1434 var source = decodeURIComponent( comment.value.replace( /^\{cke_protected\}/, '' ) ),
1435 protectedFrag,
1436 toBeRemoved = [],
1437 node, i, match;
1438
1439 // Protected element's and protected source's comments look exactly the same.
1440 // Check if what we have isn't a protected source instead of protected script/noscript.
1441 if ( protectedRegexs ) {
1442 for ( i = 0; i < protectedRegexs.length; ++i ) {
1443 if ( ( match = source.match( protectedRegexs[ i ] ) ) &&
1444 match[ 0 ].length == source.length // Check whether this pattern matches entire source
1445 // to avoid '<script>alert("<? 1 ?>")</script>' matching
1446 // the PHP's protectedSource regexp.
1447 )
1448 return true;
1449 }
1450 }
1451
1452 protectedFrag = CKEDITOR.htmlParser.fragment.fromHtml( source );
1453
1454 if ( protectedFrag.children.length == 1 && ( node = protectedFrag.children[ 0 ] ).type == CKEDITOR.NODE_ELEMENT )
1455 processElement( that, node, toBeRemoved, filterOpts );
1456
1457 // If protected element has been marked to be removed, return 'false' - comment was rejected.
1458 return !toBeRemoved.length;
1459 }
1460
1461 var unprotectElementsNamesRegexp = /^cke:(object|embed|param)$/,
1462 protectElementsNamesRegexp = /^(object|embed|param)$/;
1463
1464 // The actual function which filters, transforms and does other funny things with an element.
1465 //
1466 // @param {CKEDITOR.filter} that Context.
1467 // @param {CKEDITOR.htmlParser.element} element The element to be processed.
1468 // @param {Array} toBeRemoved Array into which elements rejected by the filter will be pushed.
1469 // @param {Boolean} [opts.doFilter] Whether element should be filtered.
1470 // @param {Boolean} [opts.doTransform] Whether transformations should be applied.
1471 // @param {Boolean} [opts.doCallbacks] Whether to execute element callbacks.
1472 // @param {Boolean} [opts.toHtml] Set to true if filter used together with htmlDP#toHtml
1473 // @param {Boolean} [opts.skipRequired] Whether element's required properties shouldn't be verified.
1474 // @param {Boolean} [opts.skipFinalValidation] Whether to not perform final element validation (a,img).
1475 // @returns {Number} Possible flags:
1476 // * FILTER_ELEMENT_MODIFIED,
1477 // * FILTER_SKIP_TREE.
1478 function processElement( that, element, toBeRemoved, opts ) {
1479 var status,
1480 retVal = 0,
1481 callbacksRetVal;
1482
1483 // Unprotect elements names previously protected by htmlDataProcessor
1484 // (see protectElementNames and protectSelfClosingElements functions).
1485 // Note: body, title, etc. are not protected by htmlDataP (or are protected and then unprotected).
1486 if ( opts.toHtml )
1487 element.name = element.name.replace( unprotectElementsNamesRegexp, '$1' );
1488
1489 // Execute element callbacks and return if one of them returned any value.
1490 if ( opts.doCallbacks && that.elementCallbacks ) {
1491 // For now we only support here FILTER_SKIP_TREE, so we can early return if retVal is truly value.
1492 if ( ( callbacksRetVal = executeElementCallbacks( element, that.elementCallbacks ) ) )
1493 return callbacksRetVal;
1494 }
1495
1496 // If transformations are set apply all groups.
1497 if ( opts.doTransform )
1498 transformElement( that, element );
1499
1500 if ( opts.doFilter ) {
1501 // Apply all filters.
1502 status = filterElement( that, element, opts );
1503
1504 // Handle early return from filterElement.
1505 if ( !status ) {
1506 toBeRemoved.push( element );
1507 return FILTER_ELEMENT_MODIFIED;
1508 }
1509
1510 // Finally, if after running all filter rules it still hasn't been allowed - remove it.
1511 if ( !status.valid ) {
1512 toBeRemoved.push( element );
1513 return FILTER_ELEMENT_MODIFIED;
1514 }
1515
1516 // Update element's attributes based on status of filtering.
1517 if ( updateElement( element, status ) )
1518 retVal = FILTER_ELEMENT_MODIFIED;
1519
1520 if ( !opts.skipFinalValidation && !validateElement( element ) ) {
1521 toBeRemoved.push( element );
1522 return FILTER_ELEMENT_MODIFIED;
1523 }
1524 }
1525
1526 // Protect previously unprotected elements.
1527 if ( opts.toHtml )
1528 element.name = element.name.replace( protectElementsNamesRegexp, 'cke:$1' );
1529
1530 return retVal;
1531 }
1532
1533 // Returns a regexp object which can be used to test if a property
1534 // matches one of wildcard validators.
1535 function regexifyPropertiesWithWildcards( validators ) {
1536 var patterns = [],
1537 i;
1538
1539 for ( i in validators ) {
1540 if ( i.indexOf( '*' ) > -1 )
1541 patterns.push( i.replace( /\*/g, '.*' ) );
1542 }
1543
1544 if ( patterns.length )
1545 return new RegExp( '^(?:' + patterns.join( '|' ) + ')$' );
1546 else
1547 return null;
1548 }
1549
1550 // Standardize a rule by converting all validators to hashes.
1551 function standardizeRule( rule ) {
1552 rule.elements = convertValidatorToHash( rule.elements, /\s+/ ) || null;
1553 rule.propertiesOnly = rule.propertiesOnly || ( rule.elements === true );
1554
1555 var delim = /\s*,\s*/,
1556 i;
1557
1558 for ( i in validators ) {
1559 rule[ i ] = convertValidatorToHash( rule[ i ], delim ) || null;
1560 rule[ validatorsRequired[ i ] ] = extractRequired( convertValidatorToHash(
1561 rule[ validatorsRequired[ i ] ], delim ), rule[ i ] ) || null;
1562 }
1563
1564 rule.match = rule.match || null;
1565 }
1566
1567 // Does the element transformation by applying registered
1568 // transformation rules.
1569 function transformElement( that, element ) {
1570 var transformations = that._.transformations[ element.name ],
1571 i;
1572
1573 if ( !transformations )
1574 return;
1575
1576 populateProperties( element );
1577
1578 for ( i = 0; i < transformations.length; ++i )
1579 applyTransformationsGroup( that, element, transformations[ i ] );
1580
1581 // Do not count on updateElement() which is called in processElement, because it:
1582 // * may not be called,
1583 // * may skip some properties when all are marked as valid.
1584 updateAttributes( element );
1585 }
1586
1587 // Copy element's styles and classes back to attributes array.
1588 function updateAttributes( element ) {
1589 var attrs = element.attributes,
1590 styles;
1591
1592 // Will be recreated later if any of styles/classes exists.
1593 delete attrs.style;
1594 delete attrs[ 'class' ];
1595
1596 if ( ( styles = CKEDITOR.tools.writeCssText( element.styles, true ) ) )
1597 attrs.style = styles;
1598
1599 if ( element.classes.length )
1600 attrs[ 'class' ] = element.classes.sort().join( ' ' );
1601 }
1602
1603 // Update element object based on status of filtering.
1604 // @returns Whether element was modified.
1605 function updateElement( element, status ) {
1606 var validAttrs = status.validAttributes,
1607 validStyles = status.validStyles,
1608 validClasses = status.validClasses,
1609 attrs = element.attributes,
1610 styles = element.styles,
1611 classes = element.classes,
1612 origClasses = element.classBackup,
1613 origStyles = element.styleBackup,
1614 name, origName, i,
1615 stylesArr = [],
1616 classesArr = [],
1617 internalAttr = /^data-cke-/,
1618 isModified = false;
1619
1620 // Will be recreated later if any of styles/classes were passed.
1621 delete attrs.style;
1622 delete attrs[ 'class' ];
1623 // Clean up.
1624 delete element.classBackup;
1625 delete element.styleBackup;
1626
1627 if ( !status.allAttributes ) {
1628 for ( name in attrs ) {
1629 // If not valid and not internal attribute delete it.
1630 if ( !validAttrs[ name ] ) {
1631 // Allow all internal attibutes...
1632 if ( internalAttr.test( name ) ) {
1633 // ... unless this is a saved attribute and the original one isn't allowed.
1634 if ( name != ( origName = name.replace( /^data-cke-saved-/, '' ) ) &&
1635 !validAttrs[ origName ]
1636 ) {
1637 delete attrs[ name ];
1638 isModified = true;
1639 }
1640 } else {
1641 delete attrs[ name ];
1642 isModified = true;
1643 }
1644 }
1645
1646 }
1647 }
1648
1649 if ( !status.allStyles || status.hadInvalidStyle ) {
1650 for ( name in styles ) {
1651 // We check status.allStyles because when there was a '*' ACR and some
1652 // DACR we have now both properties true - status.allStyles and status.hadInvalidStyle.
1653 // However unlike in the case when we only have '*' ACR, in which we can just copy original
1654 // styles, in this case we must copy only those styles which were not removed by DACRs.
1655 if ( status.allStyles || validStyles[ name ] )
1656 stylesArr.push( name + ':' + styles[ name ] );
1657 else
1658 isModified = true;
1659 }
1660 if ( stylesArr.length )
1661 attrs.style = stylesArr.sort().join( '; ' );
1662 }
1663 else if ( origStyles ) {
1664 attrs.style = origStyles;
1665 }
1666
1667 if ( !status.allClasses || status.hadInvalidClass ) {
1668 for ( i = 0; i < classes.length; ++i ) {
1669 // See comment for styles.
1670 if ( status.allClasses || validClasses[ classes[ i ] ] )
1671 classesArr.push( classes[ i ] );
1672 }
1673 if ( classesArr.length )
1674 attrs[ 'class' ] = classesArr.sort().join( ' ' );
1675
1676 if ( origClasses && classesArr.length < origClasses.split( /\s+/ ).length )
1677 isModified = true;
1678 }
1679 else if ( origClasses ) {
1680 attrs[ 'class' ] = origClasses;
1681 }
1682
1683 return isModified;
1684 }
1685
1686 function validateElement( element ) {
1687 switch ( element.name ) {
1688 case 'a':
1689 // Code borrowed from htmlDataProcessor, so ACF does the same clean up.
1690 if ( !( element.children.length || element.attributes.name || element.attributes.id ) )
1691 return false;
1692 break;
1693 case 'img':
1694 if ( !element.attributes.src )
1695 return false;
1696 break;
1697 }
1698
1699 return true;
1700 }
1701
1702 function validatorFunction( validator ) {
1703 if ( !validator )
1704 return false;
1705 if ( validator === true )
1706 return true;
1707
1708 // Note: We don't need to remove properties with wildcards from the validator object.
1709 // E.g. data-* is actually an edge case of /^data-.*$/, so when it's accepted
1710 // by `value in validator` it's ok.
1711 var regexp = regexifyPropertiesWithWildcards( validator );
1712
1713 return function( value ) {
1714 return value in validator || ( regexp && value.match( regexp ) );
1715 };
1716 }
1717
1718 //
1719 // REMOVE ELEMENT ---------------------------------------------------------
1720 //
1721
1722 // Check whether all children will be valid in new context.
1723 // Note: it doesn't verify if text node is valid, because
1724 // new parent should accept them.
1725 function checkChildren( children, newParentName ) {
1726 var allowed = DTD[ newParentName ];
1727
1728 for ( var i = 0, l = children.length, child; i < l; ++i ) {
1729 child = children[ i ];
1730 if ( child.type == CKEDITOR.NODE_ELEMENT && !allowed[ child.name ] )
1731 return false;
1732 }
1733
1734 return true;
1735 }
1736
1737 function createBr() {
1738 return new CKEDITOR.htmlParser.element( 'br' );
1739 }
1740
1741 // Whether this is an inline element or text.
1742 function inlineNode( node ) {
1743 return node.type == CKEDITOR.NODE_TEXT ||
1744 node.type == CKEDITOR.NODE_ELEMENT && DTD.$inline[ node.name ];
1745 }
1746
1747 function isBrOrBlock( node ) {
1748 return node.type == CKEDITOR.NODE_ELEMENT &&
1749 ( node.name == 'br' || DTD.$block[ node.name ] );
1750 }
1751
1752 // Try to remove element in the best possible way.
1753 //
1754 // @param {Array} toBeChecked After executing this function
1755 // this array will contain elements that should be checked
1756 // because they were marked as potentially:
1757 // * in wrong context (e.g. li in body),
1758 // * empty elements from $removeEmpty,
1759 // * incorrect img/a/other element validated by validateElement().
1760 function removeElement( element, enterTag, toBeChecked ) {
1761 var name = element.name;
1762
1763 if ( DTD.$empty[ name ] || !element.children.length ) {
1764 // Special case - hr in br mode should be replaced with br, not removed.
1765 if ( name == 'hr' && enterTag == 'br' )
1766 element.replaceWith( createBr() );
1767 else {
1768 // Parent might become an empty inline specified in $removeEmpty or empty a[href].
1769 if ( element.parent )
1770 toBeChecked.push( { check: 'it', el: element.parent } );
1771
1772 element.remove();
1773 }
1774 } else if ( DTD.$block[ name ] || name == 'tr' ) {
1775 if ( enterTag == 'br' )
1776 stripBlockBr( element, toBeChecked );
1777 else
1778 stripBlock( element, enterTag, toBeChecked );
1779 }
1780 // Special case - elements that may contain CDATA should be removed completely.
1781 else if ( name in { style: 1, script: 1 } )
1782 element.remove();
1783 // The rest of inline elements. May also be the last resort
1784 // for some special elements.
1785 else {
1786 // Parent might become an empty inline specified in $removeEmpty or empty a[href].
1787 if ( element.parent )
1788 toBeChecked.push( { check: 'it', el: element.parent } );
1789 element.replaceWithChildren();
1790 }
1791 }
1792
1793 // Strip element block, but leave its content.
1794 // Works in 'div' and 'p' enter modes.
1795 function stripBlock( element, enterTag, toBeChecked ) {
1796 var children = element.children;
1797
1798 // First, check if element's children may be wrapped with <p/div>.
1799 // Ignore that <p/div> may not be allowed in element.parent.
1800 // This will be fixed when removing parent or by toBeChecked rule.
1801 if ( checkChildren( children, enterTag ) ) {
1802 element.name = enterTag;
1803 element.attributes = {};
1804 // Check if this p/div was put in correct context.
1805 // If not - strip parent.
1806 toBeChecked.push( { check: 'parent-down', el: element } );
1807 return;
1808 }
1809
1810 var parent = element.parent,
1811 shouldAutoP = parent.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT || parent.name == 'body',
1812 i, child, p, parentDtd;
1813
1814 for ( i = children.length; i > 0; ) {
1815 child = children[ --i ];
1816
1817 // If parent requires auto paragraphing and child is inline node,
1818 // insert this child into newly created paragraph.
1819 if ( shouldAutoP && inlineNode( child ) ) {
1820 if ( !p ) {
1821 p = new CKEDITOR.htmlParser.element( enterTag );
1822 p.insertAfter( element );
1823
1824 // Check if this p/div was put in correct context.
1825 // If not - strip parent.
1826 toBeChecked.push( { check: 'parent-down', el: p } );
1827 }
1828 p.add( child, 0 );
1829 }
1830 // Child which doesn't need to be auto paragraphed.
1831 else {
1832 p = null;
1833 parentDtd = DTD[ parent.name ] || DTD.span;
1834
1835 child.insertAfter( element );
1836 // If inserted into invalid context, mark it and check
1837 // after removing all elements.
1838 if ( parent.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT &&
1839 child.type == CKEDITOR.NODE_ELEMENT &&
1840 !parentDtd[ child.name ]
1841 )
1842 toBeChecked.push( { check: 'el-up', el: child } );
1843 }
1844 }
1845
1846 // All children have been moved to element's parent, so remove it.
1847 element.remove();
1848 }
1849
1850 // Prepend/append block with <br> if isn't
1851 // already prepended/appended with <br> or block and
1852 // isn't first/last child of its parent.
1853 // Then replace element with its children.
1854 // <p>a</p><p>b</p> => <p>a</p><br>b => a<br>b
1855 function stripBlockBr( element ) {
1856 var br;
1857
1858 if ( element.previous && !isBrOrBlock( element.previous ) ) {
1859 br = createBr();
1860 br.insertBefore( element );
1861 }
1862
1863 if ( element.next && !isBrOrBlock( element.next ) ) {
1864 br = createBr();
1865 br.insertAfter( element );
1866 }
1867
1868 element.replaceWithChildren();
1869 }
1870
1871 //
1872 // TRANSFORMATIONS --------------------------------------------------------
1873 //
1874 var transformationsTools;
1875
1876 // Apply given transformations group to the element.
1877 function applyTransformationsGroup( filter, element, group ) {
1878 var i, rule;
1879
1880 for ( i = 0; i < group.length; ++i ) {
1881 rule = group[ i ];
1882
1883 // Test with #check or #left only if it's set.
1884 // Do not apply transformations because that creates infinite loop.
1885 if ( ( !rule.check || filter.check( rule.check, false ) ) &&
1886 ( !rule.left || rule.left( element ) ) ) {
1887 rule.right( element, transformationsTools );
1888 return; // Only first matching rule in a group is executed.
1889 }
1890 }
1891 }
1892
1893 // Check whether element matches CKEDITOR.style.
1894 // The element can be a "superset" of style,
1895 // e.g. it may have more classes, but need to have
1896 // at least those defined in style.
1897 function elementMatchesStyle( element, style ) {
1898 var def = style.getDefinition(),
1899 defAttrs = def.attributes,
1900 defStyles = def.styles,
1901 attrName, styleName,
1902 classes, classPattern, cl;
1903
1904 if ( element.name != def.element )
1905 return false;
1906
1907 for ( attrName in defAttrs ) {
1908 if ( attrName == 'class' ) {
1909 classes = defAttrs[ attrName ].split( /\s+/ );
1910 classPattern = element.classes.join( '|' );
1911 while ( ( cl = classes.pop() ) ) {
1912 if ( classPattern.indexOf( cl ) == -1 )
1913 return false;
1914 }
1915 } else {
1916 if ( element.attributes[ attrName ] != defAttrs[ attrName ] )
1917 return false;
1918 }
1919 }
1920
1921 for ( styleName in defStyles ) {
1922 if ( element.styles[ styleName ] != defStyles[ styleName ] )
1923 return false;
1924 }
1925
1926 return true;
1927 }
1928
1929 // Return transformation group for content form.
1930 // One content form makes one transformation rule in one group.
1931 function getContentFormTransformationGroup( form, preferredForm ) {
1932 var element, left;
1933
1934 if ( typeof form == 'string' )
1935 element = form;
1936 else if ( form instanceof CKEDITOR.style )
1937 left = form;
1938 else {
1939 element = form[ 0 ];
1940 left = form[ 1 ];
1941 }
1942
1943 return [ {
1944 element: element,
1945 left: left,
1946 right: function( el, tools ) {
1947 tools.transform( el, preferredForm );
1948 }
1949 } ];
1950 }
1951
1952 // Obtain element's name from transformation rule.
1953 // It will be defined by #element, or #check or #left (styleDef.element).
1954 function getElementNameForTransformation( rule, check ) {
1955 if ( rule.element )
1956 return rule.element;
1957 if ( check )
1958 return check.match( /^([a-z0-9]+)/i )[ 0 ];
1959 return rule.left.getDefinition().element;
1960 }
1961
1962 function getMatchStyleFn( style ) {
1963 return function( el ) {
1964 return elementMatchesStyle( el, style );
1965 };
1966 }
1967
1968 function getTransformationFn( toolName ) {
1969 return function( el, tools ) {
1970 tools[ toolName ]( el );
1971 };
1972 }
1973
1974 function optimizeTransformationsGroup( rules ) {
1975 var groupName, i, rule,
1976 check, left, right,
1977 optimizedRules = [];
1978
1979 for ( i = 0; i < rules.length; ++i ) {
1980 rule = rules[ i ];
1981
1982 if ( typeof rule == 'string' ) {
1983 rule = rule.split( /\s*:\s*/ );
1984 check = rule[ 0 ];
1985 left = null;
1986 right = rule[ 1 ];
1987 } else {
1988 check = rule.check;
1989 left = rule.left;
1990 right = rule.right;
1991 }
1992
1993 // Extract element name.
1994 if ( !groupName )
1995 groupName = getElementNameForTransformation( rule, check );
1996
1997 if ( left instanceof CKEDITOR.style )
1998 left = getMatchStyleFn( left );
1999
2000 optimizedRules.push( {
2001 // It doesn't make sense to test against name rule (e.g. 'table'), so don't save it.
2002 check: check == groupName ? null : check,
2003
2004 left: left,
2005
2006 // Handle shorthand format. E.g.: 'table[width]:sizeToAttribute'.
2007 right: typeof right == 'string' ? getTransformationFn( right ) : right
2008 } );
2009 }
2010
2011 return {
2012 name: groupName,
2013 rules: optimizedRules
2014 };
2015 }
2016
2017 /**
2018 * Singleton containing tools useful for transformation rules.
2019 *
2020 * @class CKEDITOR.filter.transformationsTools
2021 * @singleton
2022 */
2023 transformationsTools = CKEDITOR.filter.transformationsTools = {
2024 /**
2025 * Converts `width` and `height` attributes to styles.
2026 *
2027 * @param {CKEDITOR.htmlParser.element} element
2028 */
2029 sizeToStyle: function( element ) {
2030 this.lengthToStyle( element, 'width' );
2031 this.lengthToStyle( element, 'height' );
2032 },
2033
2034 /**
2035 * Converts `width` and `height` styles to attributes.
2036 *
2037 * @param {CKEDITOR.htmlParser.element} element
2038 */
2039 sizeToAttribute: function( element ) {
2040 this.lengthToAttribute( element, 'width' );
2041 this.lengthToAttribute( element, 'height' );
2042 },
2043
2044 /**
2045 * Converts length in the `attrName` attribute to a valid CSS length (like `width` or `height`).
2046 *
2047 * @param {CKEDITOR.htmlParser.element} element
2048 * @param {String} attrName Name of the attribute that will be converted.
2049 * @param {String} [styleName=attrName] Name of the style into which the attribute will be converted.
2050 */
2051 lengthToStyle: function( element, attrName, styleName ) {
2052 styleName = styleName || attrName;
2053
2054 if ( !( styleName in element.styles ) ) {
2055 var value = element.attributes[ attrName ];
2056
2057 if ( value ) {
2058 if ( ( /^\d+$/ ).test( value ) )
2059 value += 'px';
2060
2061 element.styles[ styleName ] = value;
2062 }
2063 }
2064
2065 delete element.attributes[ attrName ];
2066 },
2067
2068 /**
2069 * Converts length in the `styleName` style to a valid length attribute (like `width` or `height`).
2070 *
2071 * @param {CKEDITOR.htmlParser.element} element
2072 * @param {String} styleName The name of the style that will be converted.
2073 * @param {String} [attrName=styleName] The name of the attribute into which the style will be converted.
2074 */
2075 lengthToAttribute: function( element, styleName, attrName ) {
2076 attrName = attrName || styleName;
2077
2078 if ( !( attrName in element.attributes ) ) {
2079 var value = element.styles[ styleName ],
2080 match = value && value.match( /^(\d+)(?:\.\d*)?px$/ );
2081
2082 if ( match )
2083 element.attributes[ attrName ] = match[ 1 ];
2084 // Pass the TEST_VALUE used by filter#check when mocking element.
2085 else if ( value == TEST_VALUE )
2086 element.attributes[ attrName ] = TEST_VALUE;
2087 }
2088
2089 delete element.styles[ styleName ];
2090 },
2091
2092 /**
2093 * Converts the `align` attribute to the `float` style if not set. The attribute
2094 * is always removed.
2095 *
2096 * @param {CKEDITOR.htmlParser.element} element
2097 */
2098 alignmentToStyle: function( element ) {
2099 if ( !( 'float' in element.styles ) ) {
2100 var value = element.attributes.align;
2101
2102 if ( value == 'left' || value == 'right' )
2103 element.styles[ 'float' ] = value; // Uh... GCC doesn't like the 'float' prop name.
2104 }
2105
2106 delete element.attributes.align;
2107 },
2108
2109 /**
2110 * Converts the `float` style to the `align` attribute if not set.
2111 * The style is always removed.
2112 *
2113 * @param {CKEDITOR.htmlParser.element} element
2114 */
2115 alignmentToAttribute: function( element ) {
2116 if ( !( 'align' in element.attributes ) ) {
2117 var value = element.styles[ 'float' ];
2118
2119 if ( value == 'left' || value == 'right' )
2120 element.attributes.align = value;
2121 }
2122
2123 delete element.styles[ 'float' ]; // Uh... GCC doesn't like the 'float' prop name.
2124 },
2125
2126 /**
2127 * Converts the shorthand form of the `border` style to seperate styles.
2128 *
2129 * @param {CKEDITOR.htmlParser.element} element
2130 */
2131 splitBorderShorthand: function( element ) {
2132 if ( !element.styles.border ) {
2133 return;
2134 }
2135
2136 var widths = element.styles.border.match( /([\.\d]+\w+)/g ) || [ '0px' ];
2137 switch ( widths.length ) {
2138 case 1:
2139 element.styles[ 'border-width' ] = widths[0];
2140 break;
2141 case 2:
2142 mapStyles( [ 0, 1, 0, 1 ] );
2143 break;
2144 case 3:
2145 mapStyles( [ 0, 1, 2, 1 ] );
2146 break;
2147 case 4:
2148 mapStyles( [ 0, 1, 2, 3 ] );
2149 break;
2150 }
2151
2152 element.styles[ 'border-style' ] = element.styles[ 'border-style' ] ||
2153 ( element.styles.border.match( /(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset|initial|inherit)/ ) || [] )[ 0 ];
2154 if ( !element.styles[ 'border-style' ] )
2155 delete element.styles[ 'border-style' ];
2156
2157 delete element.styles.border;
2158
2159 function mapStyles( map ) {
2160 element.styles['border-top-width'] = widths[ map[0] ];
2161 element.styles['border-right-width'] = widths[ map[1] ];
2162 element.styles['border-bottom-width'] = widths[ map[2] ];
2163 element.styles['border-left-width'] = widths[ map[3] ];
2164 }
2165 },
2166
2167 listTypeToStyle: function( element ) {
2168 if ( element.attributes.type ) {
2169 switch ( element.attributes.type ) {
2170 case 'a':
2171 element.styles[ 'list-style-type' ] = 'lower-alpha';
2172 break;
2173 case 'A':
2174 element.styles[ 'list-style-type' ] = 'upper-alpha';
2175 break;
2176 case 'i':
2177 element.styles[ 'list-style-type' ] = 'lower-roman';
2178 break;
2179 case 'I':
2180 element.styles[ 'list-style-type' ] = 'upper-roman';
2181 break;
2182 case '1':
2183 element.styles[ 'list-style-type' ] = 'decimal';
2184 break;
2185 default:
2186 element.styles[ 'list-style-type' ] = element.attributes.type;
2187 }
2188 }
2189 },
2190
2191 /**
2192 * Converts the shorthand form of the `margin` style to seperate styles.
2193 *
2194 * @param {CKEDITOR.htmlParser.element} element
2195 */
2196 splitMarginShorthand: function( element ) {
2197 if ( !element.styles.margin ) {
2198 return;
2199 }
2200
2201 var widths = element.styles.margin.match( /(\-?[\.\d]+\w+)/g ) || [ '0px' ];
2202 switch ( widths.length ) {
2203 case 1:
2204 element.styles.margin = widths[0];
2205 break;
2206 case 2:
2207 mapStyles( [ 0, 1, 0, 1 ] );
2208 break;
2209 case 3:
2210 mapStyles( [ 0, 1, 2, 1 ] );
2211 break;
2212 case 4:
2213 mapStyles( [ 0, 1, 2, 3 ] );
2214 break;
2215 }
2216
2217 delete element.styles.margin;
2218
2219 function mapStyles( map ) {
2220 element.styles['margin-top'] = widths[ map[0] ];
2221 element.styles['margin-right'] = widths[ map[1] ];
2222 element.styles['margin-bottom'] = widths[ map[2] ];
2223 element.styles['margin-left'] = widths[ map[3] ];
2224 }
2225 },
2226
2227 /**
2228 * Checks whether an element matches a given {@link CKEDITOR.style}.
2229 * The element can be a "superset" of a style, e.g. it may have
2230 * more classes, but needs to have at least those defined in the style.
2231 *
2232 * @param {CKEDITOR.htmlParser.element} element
2233 * @param {CKEDITOR.style} style
2234 */
2235 matchesStyle: elementMatchesStyle,
2236
2237 /**
2238 * Transforms an element to a given form.
2239 *
2240 * Form may be a:
2241 *
2242 * * {@link CKEDITOR.style},
2243 * * string &ndash; the new name of the element.
2244 *
2245 * @param {CKEDITOR.htmlParser.element} el
2246 * @param {CKEDITOR.style/String} form
2247 */
2248 transform: function( el, form ) {
2249 if ( typeof form == 'string' )
2250 el.name = form;
2251 // Form is an instance of CKEDITOR.style.
2252 else {
2253 var def = form.getDefinition(),
2254 defStyles = def.styles,
2255 defAttrs = def.attributes,
2256 attrName, styleName,
2257 existingClassesPattern, defClasses, cl;
2258
2259 el.name = def.element;
2260
2261 for ( attrName in defAttrs ) {
2262 if ( attrName == 'class' ) {
2263 existingClassesPattern = el.classes.join( '|' );
2264 defClasses = defAttrs[ attrName ].split( /\s+/ );
2265
2266 while ( ( cl = defClasses.pop() ) ) {
2267 if ( existingClassesPattern.indexOf( cl ) == -1 )
2268 el.classes.push( cl );
2269 }
2270 } else {
2271 el.attributes[ attrName ] = defAttrs[ attrName ];
2272 }
2273
2274 }
2275
2276 for ( styleName in defStyles ) {
2277 el.styles[ styleName ] = defStyles[ styleName ];
2278 }
2279 }
2280 }
2281 };
2282
2283} )();
2284
2285/**
2286 * Allowed content rules. This setting is used when
2287 * instantiating {@link CKEDITOR.editor#filter}.
2288 *
2289 * The following values are accepted:
2290 *
2291 * * {@link CKEDITOR.filter.allowedContentRules} &ndash; defined rules will be added
2292 * to the {@link CKEDITOR.editor#filter}.
2293 * * `true` &ndash; will disable the filter (data will not be filtered,
2294 * all features will be activated).
2295 * * default &ndash; the filter will be configured by loaded features
2296 * (toolbar items, commands, etc.).
2297 *
2298 * In all cases filter configuration may be extended by
2299 * {@link CKEDITOR.config#extraAllowedContent}. This option may be especially
2300 * useful when you want to use the default `allowedContent` value
2301 * along with some additional rules.
2302 *
2303 * CKEDITOR.replace( 'textarea_id', {
2304 * allowedContent: 'p b i; a[!href]',
2305 * on: {
2306 * instanceReady: function( evt ) {
2307 * var editor = evt.editor;
2308 *
2309 * editor.filter.check( 'h1' ); // -> false
2310 * editor.setData( '<h1><i>Foo</i></h1><p class="left"><span>Bar</span> <a href="http://foo.bar">foo</a></p>' );
2311 * // Editor contents will be:
2312 * '<p><i>Foo</i></p><p>Bar <a href="http://foo.bar">foo</a></p>'
2313 * }
2314 * }
2315 * } );
2316 *
2317 * It is also possible to disallow some already allowed content. It is especially
2318 * useful when you want to "trim down" the content allowed by default by
2319 * editor features. To do that, use the {@link #disallowedContent} option.
2320 *
2321 * Read more in the [documentation](#!/guide/dev_acf)
2322 * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html).
2323 *
2324 * @since 4.1
2325 * @cfg {CKEDITOR.filter.allowedContentRules/Boolean} [allowedContent=null]
2326 * @member CKEDITOR.config
2327 */
2328
2329/**
2330 * This option makes it possible to set additional allowed
2331 * content rules for {@link CKEDITOR.editor#filter}.
2332 *
2333 * It is especially useful in combination with the default
2334 * {@link CKEDITOR.config#allowedContent} value:
2335 *
2336 * CKEDITOR.replace( 'textarea_id', {
2337 * plugins: 'wysiwygarea,toolbar,format',
2338 * extraAllowedContent: 'b i',
2339 * on: {
2340 * instanceReady: function( evt ) {
2341 * var editor = evt.editor;
2342 *
2343 * editor.filter.check( 'h1' ); // -> true (thanks to Format combo)
2344 * editor.filter.check( 'b' ); // -> true (thanks to extraAllowedContent)
2345 * editor.setData( '<h1><i>Foo</i></h1><p class="left"><b>Bar</b> <a href="http://foo.bar">foo</a></p>' );
2346 * // Editor contents will be:
2347 * '<h1><i>Foo</i></h1><p><b>Bar</b> foo</p>'
2348 * }
2349 * }
2350 * } );
2351 *
2352 * Read more in the [documentation](#!/guide/dev_acf-section-automatic-mode-and-allow-additional-tags%2Fproperties)
2353 * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html).
2354 * See also {@link CKEDITOR.config#allowedContent} for more details.
2355 *
2356 * @since 4.1
2357 * @cfg {Object/String} extraAllowedContent
2358 * @member CKEDITOR.config
2359 */
2360
2361/**
2362 * Disallowed content rules. They have precedence over {@link #allowedContent allowed content rules}.
2363 * Read more in the [Disallowed Content guide](#!/guide/dev_disallowed_content).
2364 *
2365 * Read more in the [documentation](#!/guide/dev_acf-section-automatic-mode-but-disallow-certain-tags%2Fproperties)
2366 * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html).
2367 * See also {@link CKEDITOR.config#allowedContent} and {@link CKEDITOR.config#extraAllowedContent}.
2368 *
2369 * @since 4.4
2370 * @cfg {CKEDITOR.filter.disallowedContentRules} disallowedContent
2371 * @member CKEDITOR.config
2372 */
2373
2374/**
2375 * This event is fired when {@link CKEDITOR.filter} has stripped some
2376 * content from the data that was loaded (e.g. by {@link CKEDITOR.editor#method-setData}
2377 * method or in the source mode) or inserted (e.g. when pasting or using the
2378 * {@link CKEDITOR.editor#method-insertHtml} method).
2379 *
2380 * This event is useful when testing whether the {@link CKEDITOR.config#allowedContent}
2381 * setting is sufficient and correct for a system that is migrating to CKEditor 4.1
2382 * (where the [Advanced Content Filter](#!/guide/dev_advanced_content_filter) was introduced).
2383 *
2384 * @since 4.1
2385 * @event dataFiltered
2386 * @member CKEDITOR.editor
2387 * @param {CKEDITOR.editor} editor This editor instance.
2388 */
2389
2390/**
2391 * Virtual class which is the [Allowed Content Rules](#!/guide/dev_allowed_content_rules) formats type.
2392 *
2393 * Possible formats are:
2394 *
2395 * * the [string format](#!/guide/dev_allowed_content_rules-section-2),
2396 * * the [object format](#!/guide/dev_allowed_content_rules-section-3),
2397 * * a {@link CKEDITOR.style} instance &ndash; used mainly for integrating plugins with Advanced Content Filter,
2398 * * an array of the above formats.
2399 *
2400 * @since 4.1
2401 * @class CKEDITOR.filter.allowedContentRules
2402 * @abstract
2403 */
2404
2405/**
2406 * Virtual class representing the {@link CKEDITOR.filter#disallow} argument and a type of
2407 * the {@link CKEDITOR.config#disallowedContent} option.
2408 *
2409 * This is a simplified version of the {@link CKEDITOR.filter.allowedContentRules} type.
2410 * Only the string format and object format are accepted. Required properties
2411 * are not allowed in this format.
2412 *
2413 * Read more in the [Disallowed Content guide](#!/guide/dev_disallowed_content).
2414 *
2415 * @since 4.4
2416 * @class CKEDITOR.filter.disallowedContentRules
2417 * @abstract
2418 */
2419
2420/**
2421 * Virtual class representing {@link CKEDITOR.filter#check} argument.
2422 *
2423 * This is a simplified version of the {@link CKEDITOR.filter.allowedContentRules} type.
2424 * It may contain only one element and its styles, classes, and attributes. Only the
2425 * string format and a {@link CKEDITOR.style} instances are accepted. Required properties
2426 * are not allowed in this format.
2427 *
2428 * Example:
2429 *
2430 * 'img[src,alt](foo)' // Correct rule.
2431 * 'ol, ul(!foo)' // Incorrect rule. Multiple elements and required
2432 * // properties are not supported.
2433 *
2434 * @since 4.1
2435 * @class CKEDITOR.filter.contentRule
2436 * @abstract
2437 */
2438
2439/**
2440 * Interface that may be automatically implemented by any
2441 * instance of any class which has at least the `name` property and
2442 * can be meant as an editor feature.
2443 *
2444 * For example:
2445 *
2446 * * "Bold" command, button, and keystroke &ndash; it does not mean exactly
2447 * `<strong>` or `<b>` but just the ability to create bold text.
2448 * * "Format" drop-down list &ndash; it also does not imply any HTML tag.
2449 * * "Link" command, button, and keystroke.
2450 * * "Image" command, button, and dialog window.
2451 *
2452 * Thus most often a feature is an instance of one of the following classes:
2453 *
2454 * * {@link CKEDITOR.command}
2455 * * {@link CKEDITOR.ui.button}
2456 * * {@link CKEDITOR.ui.richCombo}
2457 *
2458 * None of them have a `name` property explicitly defined, but
2459 * it is set by {@link CKEDITOR.editor#addCommand} and {@link CKEDITOR.ui#add}.
2460 *
2461 * During editor initialization all features that the editor should activate
2462 * should be passed to {@link CKEDITOR.editor#addFeature} (shorthand for {@link CKEDITOR.filter#addFeature}).
2463 *
2464 * This method checks if a feature can be activated (see {@link #requiredContent}) and if yes,
2465 * then it registers allowed content rules required by this feature (see {@link #allowedContent}) along
2466 * with two kinds of transformations: {@link #contentForms} and {@link #contentTransformations}.
2467 *
2468 * By default all buttons that are included in [toolbar layout configuration](#!/guide/dev_toolbar)
2469 * are checked and registered with {@link CKEDITOR.editor#addFeature}, all styles available in the
2470 * 'Format' and 'Styles' drop-down lists are checked and registered too and so on.
2471 *
2472 * @since 4.1
2473 * @class CKEDITOR.feature
2474 * @abstract
2475 */
2476
2477/**
2478 * HTML code that can be generated by this feature.
2479 *
2480 * For example a basic image feature (image button displaying the image dialog window)
2481 * may allow `'img[!src,alt,width,height]'`.
2482 *
2483 * During the feature activation this value is passed to {@link CKEDITOR.filter#allow}.
2484 *
2485 * @property {CKEDITOR.filter.allowedContentRules} [allowedContent=null]
2486 */
2487
2488/**
2489 * Minimal HTML code that this feature must be allowed to
2490 * generate in order to work.
2491 *
2492 * For example a basic image feature (image button displaying the image dialog window)
2493 * needs `'img[src,alt]'` in order to be activated.
2494 *
2495 * During the feature validation this value is passed to {@link CKEDITOR.filter#check}.
2496 *
2497 * If this value is not provided, a feature will be always activated.
2498 *
2499 * @property {CKEDITOR.filter.contentRule} [requiredContent=null]
2500 */
2501
2502/**
2503 * The name of the feature.
2504 *
2505 * It is used for example to identify which {@link CKEDITOR.filter#allowedContent}
2506 * rule was added for which feature.
2507 *
2508 * @property {String} name
2509 */
2510
2511/**
2512 * Feature content forms to be registered in the {@link CKEDITOR.editor#filter}
2513 * during the feature activation.
2514 *
2515 * See {@link CKEDITOR.filter#addContentForms} for more details.
2516 *
2517 * @property [contentForms=null]
2518 */
2519
2520/**
2521 * Transformations (usually for content generated by this feature, but not necessarily)
2522 * that will be registered in the {@link CKEDITOR.editor#filter} during the feature activation.
2523 *
2524 * See {@link CKEDITOR.filter#addTransformations} for more details.
2525 *
2526 * @property [contentTransformations=null]
2527 */
2528
2529/**
2530 * Returns a feature that this feature needs to register.
2531 *
2532 * In some cases, during activation, one feature may need to register
2533 * another feature. For example a {@link CKEDITOR.ui.button} often registers
2534 * a related command. See {@link CKEDITOR.ui.button#toFeature}.
2535 *
2536 * This method is executed when a feature is passed to the {@link CKEDITOR.editor#addFeature}.
2537 *
2538 * @method toFeature
2539 * @returns {CKEDITOR.feature}
2540 */
diff --git a/sources/core/focusmanager.js b/sources/core/focusmanager.js
new file mode 100644
index 0000000..6fc9969
--- /dev/null
+++ b/sources/core/focusmanager.js
@@ -0,0 +1,281 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 var editor = this._.editor;
155
156 if ( this.hasFocus ) {
157 this.hasFocus = false;
158
159 // Blink browsers leave selection in `[contenteditable=true]`
160 // when it's blurred and it's neccessary to remove it manually for inline editor. (#13446)
161 if ( CKEDITOR.env.chrome && editor.editable().isInline() ) {
162 editor.window.$.getSelection().removeAllRanges();
163 }
164
165 var ct = this._.editor.container;
166 ct && ct.removeClass( 'cke_focus' );
167 this._.editor.fire( 'blur' );
168 }
169 }
170
171 if ( this._.timer )
172 clearTimeout( this._.timer );
173
174 var delay = CKEDITOR.focusManager._.blurDelay;
175 if ( noDelay || !delay )
176 doBlur.call( this );
177 else {
178 this._.timer = CKEDITOR.tools.setTimeout( function() {
179 delete this._.timer;
180 doBlur.call( this );
181 }, delay, this );
182 }
183 },
184
185 /**
186 * Registers a UI DOM element to the focus manager, which will make the focus manager "hasFocus"
187 * once the input focus is relieved on the element.
188 * This method is designed to be used by plugins to expand the jurisdiction of the editor focus.
189 *
190 * @param {CKEDITOR.dom.element} element The container (topmost) element of one UI part.
191 * @param {Boolean} isCapture If specified, {@link CKEDITOR.event#useCapture} will be used when listening to the focus event.
192 * @member CKEDITOR.focusManager
193 */
194 add: function( element, isCapture ) {
195 var fm = element.getCustomData( SLOT_NAME );
196 if ( !fm || fm != this ) {
197 // If this element is already taken by another instance, dismiss it first.
198 fm && fm.remove( element );
199
200 var focusEvent = 'focus',
201 blurEvent = 'blur';
202
203 // Bypass the element's internal DOM focus change.
204 if ( isCapture ) {
205
206 // Use "focusin/focusout" events instead of capture phase in IEs,
207 // which fires synchronously.
208 if ( CKEDITOR.env.ie ) {
209 focusEvent = 'focusin';
210 blurEvent = 'focusout';
211 } else {
212 CKEDITOR.event.useCapture = 1;
213 }
214 }
215
216 var listeners = {
217 blur: function() {
218 if ( element.equals( this.currentActive ) )
219 this.blur();
220 },
221 focus: function() {
222 this.focus( element );
223 }
224 };
225
226 element.on( focusEvent, listeners.focus, this );
227 element.on( blurEvent, listeners.blur, this );
228
229 if ( isCapture )
230 CKEDITOR.event.useCapture = 0;
231
232 element.setCustomData( SLOT_NAME, this );
233 element.setCustomData( SLOT_NAME_LISTENERS, listeners );
234 }
235 },
236
237 /**
238 * Dismisses an element from the focus manager delegations added by {@link #add}.
239 *
240 * @param {CKEDITOR.dom.element} element The element to be removed from the focus manager.
241 * @member CKEDITOR.focusManager
242 */
243 remove: function( element ) {
244 element.removeCustomData( SLOT_NAME );
245 var listeners = element.removeCustomData( SLOT_NAME_LISTENERS );
246 element.removeListener( 'blur', listeners.blur );
247 element.removeListener( 'focus', listeners.focus );
248 }
249
250 };
251
252} )();
253
254/**
255 * Fired when the editor instance receives the input focus.
256 *
257 * editor.on( 'focus', function( e ) {
258 * alert( 'The editor named ' + e.editor.name + ' is now focused' );
259 * } );
260 *
261 * @event focus
262 * @member CKEDITOR.editor
263 * @param {CKEDITOR.editor} editor The editor instance.
264 */
265
266/**
267 * Fired when the editor instance loses the input focus.
268 *
269 * **Note:** This event will **NOT** be triggered when focus is moved internally, e.g. from
270 * an editable to another part of the editor UI like a dialog window.
271 * If you are interested only in the focus state of the editable, listen to the `focus`
272 * and `blur` events of the {@link CKEDITOR.editable} instead.
273 *
274 * editor.on( 'blur', function( e ) {
275 * alert( 'The editor named ' + e.editor.name + ' lost the focus' );
276 * } );
277 *
278 * @event blur
279 * @member CKEDITOR.editor
280 * @param {CKEDITOR.editor} editor The editor instance.
281 */
diff --git a/sources/core/htmldataprocessor.js b/sources/core/htmldataprocessor.js
new file mode 100644
index 0000000..56764be
--- /dev/null
+++ b/sources/core/htmldataprocessor.js
@@ -0,0 +1,1036 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /**
8 * Represents 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 0000000..8c30992
--- /dev/null
+++ b/sources/core/htmlparser.js
@@ -0,0 +1,205 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * 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 0000000..529fbf1
--- /dev/null
+++ b/sources/core/htmlparser/basicwriter.js
@@ -0,0 +1,152 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * 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 0000000..be8c5cf
--- /dev/null
+++ b/sources/core/htmlparser/cdata.js
@@ -0,0 +1,48 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 'use strict';
7
8( function() {
9
10 /**
11 * A lightweight representation of HTML CDATA.
12 *
13 * @class
14 * @extends CKEDITOR.htmlParser.node
15 * @constructor Creates a cdata class instance.
16 * @param {String} value The CDATA section value.
17 */
18 CKEDITOR.htmlParser.cdata = function( value ) {
19 /**
20 * The CDATA value.
21 *
22 * @property {String}
23 */
24 this.value = value;
25 };
26
27 CKEDITOR.htmlParser.cdata.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
28 /**
29 * CDATA has the same type as {@link CKEDITOR.htmlParser.text} This is
30 * a constant value set to {@link CKEDITOR#NODE_TEXT}.
31 *
32 * @readonly
33 * @property {Number} [=CKEDITOR.NODE_TEXT]
34 */
35 type: CKEDITOR.NODE_TEXT,
36
37 filter: function() {},
38
39 /**
40 * Writes the CDATA with no special manipulations.
41 *
42 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
43 */
44 writeHtml: function( writer ) {
45 writer.write( this.value );
46 }
47 } );
48} )();
diff --git a/sources/core/htmlparser/comment.js b/sources/core/htmlparser/comment.js
new file mode 100644
index 0000000..14a38f3
--- /dev/null
+++ b/sources/core/htmlparser/comment.js
@@ -0,0 +1,80 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 'use strict';
7
8/**
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 0000000..0ed750e
--- /dev/null
+++ b/sources/core/htmlparser/element.js
@@ -0,0 +1,568 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8/**
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 the 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 the 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 * Searches through the current node children to find nodes matching the `criteria`.
447 *
448 * @param {String/Function} criteria Tag name or evaluator function.
449 * @param {Boolean} [recursive=false]
450 * @returns {CKEDITOR.htmlParser.node[]}
451 */
452 find: function( criteria, recursive ) {
453 if ( recursive === undefined ) {
454 recursive = false;
455 }
456
457 var ret = [],
458 i;
459
460 for ( i = 0; i < this.children.length; i++ ) {
461 var curChild = this.children[ i ];
462
463 if ( typeof criteria == 'function' && criteria( curChild ) ) {
464 ret.push( curChild );
465 } else if ( typeof criteria == 'string' && curChild.name === criteria ) {
466 ret.push( curChild );
467 }
468
469 if ( recursive && curChild.find ) {
470 ret = ret.concat( curChild.find( criteria, recursive ) );
471 }
472 }
473
474 return ret;
475 },
476
477 /**
478 * Adds a class name to the list of classes.
479 *
480 * @since 4.4
481 * @param {String} className The class name to be added.
482 */
483 addClass: function( className ) {
484 if ( this.hasClass( className ) )
485 return;
486
487 var c = this.attributes[ 'class' ] || '';
488
489 this.attributes[ 'class' ] = c + ( c ? ' ' : '' ) + className;
490 },
491
492 /**
493 * Removes a class name from the list of classes.
494 *
495 * @since 4.3
496 * @param {String} className The class name to be removed.
497 */
498 removeClass: function( className ) {
499 var classes = this.attributes[ 'class' ];
500
501 if ( !classes )
502 return;
503
504 // We can safely assume that className won't break regexp.
505 // http://stackoverflow.com/questions/448981/what-characters-are-valid-in-css-class-names
506 classes = CKEDITOR.tools.trim( classes.replace( new RegExp( '(?:\\s+|^)' + className + '(?:\\s+|$)' ), ' ' ) );
507
508 if ( classes )
509 this.attributes[ 'class' ] = classes;
510 else
511 delete this.attributes[ 'class' ];
512 },
513
514 /**
515 * Checkes whether this element has a class name.
516 *
517 * @since 4.3
518 * @param {String} className The class name to be checked.
519 * @returns {Boolean} Whether this element has a `className`.
520 */
521 hasClass: function( className ) {
522 var classes = this.attributes[ 'class' ];
523
524 if ( !classes )
525 return false;
526
527 return ( new RegExp( '(?:^|\\s)' + className + '(?=\\s|$)' ) ).test( classes );
528 },
529
530 getFilterContext: function( ctx ) {
531 var changes = [];
532
533 if ( !ctx ) {
534 ctx = {
535 off: false,
536 nonEditable: false,
537 nestedEditable: false
538 };
539 }
540
541 if ( !ctx.off && this.attributes[ 'data-cke-processor' ] == 'off' )
542 changes.push( 'off', true );
543
544 if ( !ctx.nonEditable && this.attributes.contenteditable == 'false' )
545 changes.push( 'nonEditable', true );
546 // A context to be given nestedEditable must be nonEditable first (by inheritance) (#11372, #11698).
547 // Special case: #11504 - filter starts on <body contenteditable=true>,
548 // so ctx.nonEditable has not been yet set to true.
549 else if ( ctx.nonEditable && !ctx.nestedEditable && this.attributes.contenteditable == 'true' )
550 changes.push( 'nestedEditable', true );
551
552 if ( changes.length ) {
553 ctx = CKEDITOR.tools.copy( ctx );
554 for ( var i = 0; i < changes.length; i += 2 )
555 ctx[ changes[ i ] ] = changes[ i + 1 ];
556 }
557
558 return ctx;
559 }
560 }, true );
561
562 function nameCondition( condition ) {
563 return function( el ) {
564 return el.type == CKEDITOR.NODE_ELEMENT &&
565 ( typeof condition == 'string' ? el.name == condition : el.name in condition );
566 };
567 }
568} )();
diff --git a/sources/core/htmlparser/filter.js b/sources/core/htmlparser/filter.js
new file mode 100644
index 0000000..db6b91c
--- /dev/null
+++ b/sources/core/htmlparser/filter.js
@@ -0,0 +1,407 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8( function() {
9 /**
10 * Filter is a configurable tool for transforming and filtering {@link CKEDITOR.htmlParser.node nodes}.
11 * It is mainly used during data processing phase which is done not on real DOM nodes,
12 * but on their simplified form represented by {@link CKEDITOR.htmlParser.node} class and its subclasses.
13 *
14 * var filter = new CKEDITOR.htmlParser.filter( {
15 * text: function( value ) {
16 * return '@' + value + '@';
17 * },
18 * elements: {
19 * p: function( element ) {
20 * element.attributes.foo = '1';
21 * }
22 * }
23 * } );
24 *
25 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p>Foo<b>bar!</b></p>' ),
26 * writer = new CKEDITOR.htmlParser.basicWriter();
27 * filter.applyTo( fragment );
28 * fragment.writeHtml( writer );
29 * writer.getHtml(); // '<p foo="1">@Foo@<b>@bar!@</b></p>'
30 *
31 * @class
32 */
33 CKEDITOR.htmlParser.filter = CKEDITOR.tools.createClass( {
34 /**
35 * @constructor Creates a filter class instance.
36 * @param {CKEDITOR.htmlParser.filterRulesDefinition} [rules]
37 */
38 $: function( rules ) {
39 /**
40 * ID of filter instance, which is used to mark elements
41 * to which this filter has been already applied.
42 *
43 * @property {Number} id
44 * @readonly
45 */
46 this.id = CKEDITOR.tools.getNextNumber();
47
48 /**
49 * Rules for element names.
50 *
51 * @property {CKEDITOR.htmlParser.filterRulesGroup}
52 * @readonly
53 */
54 this.elementNameRules = new filterRulesGroup();
55
56 /**
57 * Rules for attribute names.
58 *
59 * @property {CKEDITOR.htmlParser.filterRulesGroup}
60 * @readonly
61 */
62 this.attributeNameRules = new filterRulesGroup();
63
64 /**
65 * Hash of elementName => {@link CKEDITOR.htmlParser.filterRulesGroup rules for elements}.
66 *
67 * @readonly
68 */
69 this.elementsRules = {};
70
71 /**
72 * Hash of attributeName => {@link CKEDITOR.htmlParser.filterRulesGroup rules for attributes}.
73 *
74 * @readonly
75 */
76 this.attributesRules = {};
77
78 /**
79 * Rules for text nodes.
80 *
81 * @property {CKEDITOR.htmlParser.filterRulesGroup}
82 * @readonly
83 */
84 this.textRules = new filterRulesGroup();
85
86 /**
87 * Rules for comment nodes.
88 *
89 * @property {CKEDITOR.htmlParser.filterRulesGroup}
90 * @readonly
91 */
92 this.commentRules = new filterRulesGroup();
93
94 /**
95 * Rules for a root node.
96 *
97 * @property {CKEDITOR.htmlParser.filterRulesGroup}
98 * @readonly
99 */
100 this.rootRules = new filterRulesGroup();
101
102 if ( rules )
103 this.addRules( rules, 10 );
104 },
105
106 proto: {
107 /**
108 * Add rules to this filter.
109 *
110 * @param {CKEDITOR.htmlParser.filterRulesDefinition} rules Object containing filter rules.
111 * @param {Object/Number} [options] Object containing rules' options or a priority
112 * (for a backward compatibility with CKEditor versions up to 4.2.x).
113 * @param {Number} [options.priority=10] The priority of a rule.
114 * @param {Boolean} [options.applyToAll=false] Whether to apply rule to non-editable
115 * elements and their descendants too.
116 */
117 addRules: function( rules, options ) {
118 var priority;
119
120 // Backward compatibility.
121 if ( typeof options == 'number' )
122 priority = options;
123 // New version - try reading from options.
124 else if ( options && ( 'priority' in options ) )
125 priority = options.priority;
126
127 // Defaults.
128 if ( typeof priority != 'number' )
129 priority = 10;
130 if ( typeof options != 'object' )
131 options = {};
132
133 // Add the elementNames.
134 if ( rules.elementNames )
135 this.elementNameRules.addMany( rules.elementNames, priority, options );
136
137 // Add the attributeNames.
138 if ( rules.attributeNames )
139 this.attributeNameRules.addMany( rules.attributeNames, priority, options );
140
141 // Add the elements.
142 if ( rules.elements )
143 addNamedRules( this.elementsRules, rules.elements, priority, options );
144
145 // Add the attributes.
146 if ( rules.attributes )
147 addNamedRules( this.attributesRules, rules.attributes, priority, options );
148
149 // Add the text.
150 if ( rules.text )
151 this.textRules.add( rules.text, priority, options );
152
153 // Add the comment.
154 if ( rules.comment )
155 this.commentRules.add( rules.comment, priority, options );
156
157 // Add root node rules.
158 if ( rules.root )
159 this.rootRules.add( rules.root, priority, options );
160 },
161
162 /**
163 * Apply this filter to given node.
164 *
165 * @param {CKEDITOR.htmlParser.node} node The node to be filtered.
166 */
167 applyTo: function( node ) {
168 node.filter( this );
169 },
170
171 onElementName: function( context, name ) {
172 return this.elementNameRules.execOnName( context, name );
173 },
174
175 onAttributeName: function( context, name ) {
176 return this.attributeNameRules.execOnName( context, name );
177 },
178
179 onText: function( context, text, node ) {
180 return this.textRules.exec( context, text, node );
181 },
182
183 onComment: function( context, commentText, comment ) {
184 return this.commentRules.exec( context, commentText, comment );
185 },
186
187 onRoot: function( context, element ) {
188 return this.rootRules.exec( context, element );
189 },
190
191 onElement: function( context, element ) {
192 // We must apply filters set to the specific element name as
193 // well as those set to the generic ^/$ name. So, add both to an
194 // array and process them in a small loop.
195 var rulesGroups = [ this.elementsRules[ '^' ], this.elementsRules[ element.name ], this.elementsRules.$ ],
196 rulesGroup, ret;
197
198 for ( var i = 0; i < 3; i++ ) {
199 rulesGroup = rulesGroups[ i ];
200 if ( rulesGroup ) {
201 ret = rulesGroup.exec( context, element, this );
202
203 if ( ret === false )
204 return null;
205
206 if ( ret && ret != element )
207 return this.onNode( context, ret );
208
209 // The non-root element has been dismissed by one of the filters.
210 if ( element.parent && !element.name )
211 break;
212 }
213 }
214
215 return element;
216 },
217
218 onNode: function( context, node ) {
219 var type = node.type;
220
221 return type == CKEDITOR.NODE_ELEMENT ? this.onElement( context, node ) :
222 type == CKEDITOR.NODE_TEXT ? new CKEDITOR.htmlParser.text( this.onText( context, node.value ) ) :
223 type == CKEDITOR.NODE_COMMENT ? new CKEDITOR.htmlParser.comment( this.onComment( context, node.value ) ) : null;
224 },
225
226 onAttribute: function( context, element, name, value ) {
227 var rulesGroup = this.attributesRules[ name ];
228
229 if ( rulesGroup )
230 return rulesGroup.exec( context, value, element, this );
231 return value;
232 }
233 }
234 } );
235
236 /**
237 * Class grouping filter rules for one subject (like element or attribute names).
238 *
239 * @class CKEDITOR.htmlParser.filterRulesGroup
240 */
241 function filterRulesGroup() {
242 /**
243 * Array of objects containing rule, priority and options.
244 *
245 * @property {Object[]}
246 * @readonly
247 */
248 this.rules = [];
249 }
250
251 CKEDITOR.htmlParser.filterRulesGroup = filterRulesGroup;
252
253 filterRulesGroup.prototype = {
254 /**
255 * Adds specified rule to this group.
256 *
257 * @param {Function/Array} rule Function for function based rule or [ pattern, replacement ] array for
258 * rule applicable to names.
259 * @param {Number} priority
260 * @param options
261 */
262 add: function( rule, priority, options ) {
263 this.rules.splice( this.findIndex( priority ), 0, {
264 value: rule,
265 priority: priority,
266 options: options
267 } );
268 },
269
270 /**
271 * Adds specified rules to this group.
272 *
273 * @param {Array} rules Array of rules - see {@link #add}.
274 * @param {Number} priority
275 * @param options
276 */
277 addMany: function( rules, priority, options ) {
278 var args = [ this.findIndex( priority ), 0 ];
279
280 for ( var i = 0, len = rules.length; i < len; i++ ) {
281 args.push( {
282 value: rules[ i ],
283 priority: priority,
284 options: options
285 } );
286 }
287
288 this.rules.splice.apply( this.rules, args );
289 },
290
291 /**
292 * Finds an index at which rule with given priority should be inserted.
293 *
294 * @param {Number} priority
295 * @returns {Number} Index.
296 */
297 findIndex: function( priority ) {
298 var rules = this.rules,
299 len = rules.length,
300 i = len - 1;
301
302 // Search from the end, because usually rules will be added with default priority, so
303 // we will be able to stop loop quickly.
304 while ( i >= 0 && priority < rules[ i ].priority )
305 i--;
306
307 return i + 1;
308 },
309
310 /**
311 * Executes this rules group on given value. Applicable only if function based rules were added.
312 *
313 * All arguments passed to this function will be forwarded to rules' functions.
314 *
315 * @param {CKEDITOR.htmlParser.node/CKEDITOR.htmlParser.fragment/String} currentValue The value to be filtered.
316 * @returns {CKEDITOR.htmlParser.node/CKEDITOR.htmlParser.fragment/String} Filtered value.
317 */
318 exec: function( context, currentValue ) {
319 var isNode = currentValue instanceof CKEDITOR.htmlParser.node || currentValue instanceof CKEDITOR.htmlParser.fragment,
320 // Splice '1' to remove context, which we don't want to pass to filter rules.
321 args = Array.prototype.slice.call( arguments, 1 ),
322 rules = this.rules,
323 len = rules.length,
324 orgType, orgName, ret, i, rule;
325
326 for ( i = 0; i < len; i++ ) {
327 // Backup the node info before filtering.
328 if ( isNode ) {
329 orgType = currentValue.type;
330 orgName = currentValue.name;
331 }
332
333 rule = rules[ i ];
334 if ( isRuleApplicable( context, rule ) ) {
335 ret = rule.value.apply( null, args );
336
337 if ( ret === false )
338 return ret;
339
340 // We're filtering node (element/fragment).
341 // No further filtering if it's not anymore fitable for the subsequent filters.
342 if ( isNode && ret && ( ret.name != orgName || ret.type != orgType ) )
343 return ret;
344
345 // Update currentValue and corresponding argument in args array.
346 // Updated values will be used in next for-loop step.
347 if ( ret != null )
348 args[ 0 ] = currentValue = ret;
349
350 // ret == undefined will continue loop as nothing has happened.
351 }
352 }
353
354 return currentValue;
355 },
356
357 /**
358 * Executes this rules group on name. Applicable only if filter rules for names were added.
359 *
360 * @param {String} currentName The name to be filtered.
361 * @returns {String} Filtered name.
362 */
363 execOnName: function( context, currentName ) {
364 var i = 0,
365 rules = this.rules,
366 len = rules.length,
367 rule;
368
369 for ( ; currentName && i < len; i++ ) {
370 rule = rules[ i ];
371 if ( isRuleApplicable( context, rule ) )
372 currentName = currentName.replace( rule.value[ 0 ], rule.value[ 1 ] );
373 }
374
375 return currentName;
376 }
377 };
378
379 function addNamedRules( rulesGroups, newRules, priority, options ) {
380 var ruleName, rulesGroup;
381
382 for ( ruleName in newRules ) {
383 rulesGroup = rulesGroups[ ruleName ];
384
385 if ( !rulesGroup )
386 rulesGroup = rulesGroups[ ruleName ] = new filterRulesGroup();
387
388 rulesGroup.add( newRules[ ruleName ], priority, options );
389 }
390 }
391
392 function isRuleApplicable( context, rule ) {
393 if ( context.nonEditable && !rule.options.applyToAll )
394 return false;
395
396 if ( context.nestedEditable && rule.options.excludeNestedEditable )
397 return false;
398
399 return true;
400 }
401
402} )();
403
404/**
405 * @class CKEDITOR.htmlParser.filterRulesDefinition
406 * @abstract
407 */
diff --git a/sources/core/htmlparser/fragment.js b/sources/core/htmlparser/fragment.js
new file mode 100644
index 0000000..f696a12
--- /dev/null
+++ b/sources/core/htmlparser/fragment.js
@@ -0,0 +1,646 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8/**
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 0000000..b38c8a8
--- /dev/null
+++ b/sources/core/htmlparser/node.js
@@ -0,0 +1,156 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8( function() {
9 /**
10 * A lightweight representation of HTML node.
11 *
12 * @since 4.1
13 * @class
14 * @constructor Creates a node class instance.
15 */
16 CKEDITOR.htmlParser.node = function() {};
17
18 CKEDITOR.htmlParser.node.prototype = {
19 /**
20 * Remove this node from a tree.
21 *
22 * @since 4.1
23 */
24 remove: function() {
25 var children = this.parent.children,
26 index = CKEDITOR.tools.indexOf( children, this ),
27 previous = this.previous,
28 next = this.next;
29
30 previous && ( previous.next = next );
31 next && ( next.previous = previous );
32 children.splice( index, 1 );
33 this.parent = null;
34 },
35
36 /**
37 * Replace this node with given one.
38 *
39 * @since 4.1
40 * @param {CKEDITOR.htmlParser.node} node The node that will replace this one.
41 */
42 replaceWith: function( node ) {
43 var children = this.parent.children,
44 index = CKEDITOR.tools.indexOf( children, this ),
45 previous = node.previous = this.previous,
46 next = node.next = this.next;
47
48 previous && ( previous.next = node );
49 next && ( next.previous = node );
50
51 children[ index ] = node;
52
53 node.parent = this.parent;
54 this.parent = null;
55 },
56
57 /**
58 * Insert this node after given one.
59 *
60 * @since 4.1
61 * @param {CKEDITOR.htmlParser.node} node The node that will precede this element.
62 */
63 insertAfter: function( node ) {
64 var children = node.parent.children,
65 index = CKEDITOR.tools.indexOf( children, node ),
66 next = node.next;
67
68 children.splice( index + 1, 0, this );
69
70 this.next = node.next;
71 this.previous = node;
72 node.next = this;
73 next && ( next.previous = this );
74
75 this.parent = node.parent;
76 },
77
78 /**
79 * Insert this node before given one.
80 *
81 * @since 4.1
82 * @param {CKEDITOR.htmlParser.node} node The node that will follow this element.
83 */
84 insertBefore: function( node ) {
85 var children = node.parent.children,
86 index = CKEDITOR.tools.indexOf( children, node );
87
88 children.splice( index, 0, this );
89
90 this.next = node;
91 this.previous = node.previous;
92 node.previous && ( node.previous.next = this );
93 node.previous = this;
94
95 this.parent = node.parent;
96 },
97
98 /**
99 * Gets the closest ancestor element of this element which satisfies given condition
100 *
101 * @since 4.3
102 * @param {String/Object/Function} condition Name of an ancestor, hash of names or validator function.
103 * @returns {CKEDITOR.htmlParser.element} The closest ancestor which satisfies given condition or `null`.
104 */
105 getAscendant: function( condition ) {
106 var checkFn =
107 typeof condition == 'function' ?
108 condition :
109 typeof condition == 'string' ?
110 function( el ) {
111 return el.name == condition;
112 } :
113 function( el ) {
114 return el.name in condition;
115 };
116
117 var parent = this.parent;
118
119 // Parent has to be an element - don't check doc fragment.
120 while ( parent && parent.type == CKEDITOR.NODE_ELEMENT ) {
121 if ( checkFn( parent ) )
122 return parent;
123 parent = parent.parent;
124 }
125
126 return null;
127 },
128
129 /**
130 * Wraps this element with given `wrapper`.
131 *
132 * @since 4.3
133 * @param {CKEDITOR.htmlParser.element} wrapper The element which will be this element's new parent.
134 * @returns {CKEDITOR.htmlParser.element} Wrapper.
135 */
136 wrapWith: function( wrapper ) {
137 this.replaceWith( wrapper );
138 wrapper.add( this );
139 return wrapper;
140 },
141
142 /**
143 * Gets this node's index in its parent's children array.
144 *
145 * @since 4.3
146 * @returns {Number}
147 */
148 getIndex: function() {
149 return CKEDITOR.tools.indexOf( this.parent.children, this );
150 },
151
152 getFilterContext: function( context ) {
153 return context || {};
154 }
155 };
156} )();
diff --git a/sources/core/htmlparser/text.js b/sources/core/htmlparser/text.js
new file mode 100644
index 0000000..190e85a
--- /dev/null
+++ b/sources/core/htmlparser/text.js
@@ -0,0 +1,70 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 'use strict';
7
8( function() {
9 /**
10 * A lightweight representation of HTML text.
11 *
12 * @class
13 * @extends CKEDITOR.htmlParser.node
14 * @constructor Creates a text class instance.
15 * @param {String} value The text node value.
16 */
17 CKEDITOR.htmlParser.text = function( value ) {
18 /**
19 * The text value.
20 *
21 * @property {String}
22 */
23 this.value = value;
24
25 /** @private */
26 this._ = {
27 isBlockLike: false
28 };
29 };
30
31 CKEDITOR.htmlParser.text.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
32 /**
33 * The node type. This is a constant value set to {@link CKEDITOR#NODE_TEXT}.
34 *
35 * @readonly
36 * @property {Number} [=CKEDITOR.NODE_TEXT]
37 */
38 type: CKEDITOR.NODE_TEXT,
39
40 /**
41 * Filter this text node with given filter.
42 *
43 * @since 4.1
44 * @param {CKEDITOR.htmlParser.filter} filter
45 * @returns {Boolean} Method returns `false` when this text node has
46 * been removed. This is an information for {@link CKEDITOR.htmlParser.element#filterChildren}
47 * that it has to repeat filter on current position in parent's children array.
48 */
49 filter: function( filter, context ) {
50 if ( !( this.value = filter.onText( context, this.value, this ) ) ) {
51 this.remove();
52 return false;
53 }
54 },
55
56 /**
57 * Writes the HTML representation of this text to a {CKEDITOR.htmlParser.basicWriter}.
58 *
59 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
60 * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
61 * **Note:** it's unsafe to filter offline (not appended) node.
62 */
63 writeHtml: function( writer, filter ) {
64 if ( filter )
65 this.filter( filter );
66
67 writer.text( this.value );
68 }
69 } );
70} )();
diff --git a/sources/core/keystrokehandler.js b/sources/core/keystrokehandler.js
new file mode 100644
index 0000000..c84089b
--- /dev/null
+++ b/sources/core/keystrokehandler.js
@@ -0,0 +1,169 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * 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 0000000..8766055
--- /dev/null
+++ b/sources/core/lang.js
@@ -0,0 +1,103 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 /**
8 * 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, az: 1, bg: 1, bn: 1, bs: 1, ca: 1, cs: 1, cy: 1, da: 1, de: 1, 'de-ch': 1, el: 1,
21 'en-au': 1, 'en-ca': 1, 'en-gb': 1, en: 1, eo: 1, es: 1, et: 1, eu: 1, fa: 1, fi: 1, fo: 1,
22 'fr-ca': 1, fr: 1, gl: 1, gu: 1, he: 1, hi: 1, hr: 1, hu: 1, id: 1, is: 1, it: 1, ja: 1, ka: 1,
23 km: 1, ko: 1, ku: 1, lt: 1, lv: 1, mk: 1, mn: 1, ms: 1, nb: 1, nl: 1, no: 1, oc: 1, pl: 1, 'pt-br': 1,
24 pt: 1, ro: 1, ru: 1, si: 1, sk: 1, sl: 1, sq: 1, 'sr-latn': 1, sr: 1, sv: 1, th: 1, tr: 1, tt: 1, ug: 1,
25 uk: 1, vi: 1, 'zh-cn': 1, zh: 1
26 },
27
28 /**
29 * The list of languages that are written Right-To-Left (RTL) and are supported by the editor.
30 */
31 rtl: { ar: 1, fa: 1, he: 1, ku: 1, ug: 1 },
32
33 /**
34 * Loads a specific language file, or auto detects it. A callback is
35 * then called when the file gets loaded.
36 *
37 * @param {String} languageCode The code of the language file to be
38 * loaded. If null or empty, autodetection will be performed. The
39 * same happens if the language is not supported.
40 * @param {String} defaultLanguage The language to be used if
41 * `languageCode` is not supported or if the autodetection fails.
42 * @param {Function} callback A function to be called once the
43 * language file is loaded. Two parameters are passed to this
44 * function: the language code and the loaded language entries.
45 */
46 load: function( languageCode, defaultLanguage, callback ) {
47 // If no languageCode - fallback to browser or default.
48 // If languageCode - fallback to no-localized version or default.
49 if ( !languageCode || !CKEDITOR.lang.languages[ languageCode ] )
50 languageCode = this.detect( defaultLanguage, languageCode );
51
52 var that = this,
53 loadedCallback = function() {
54 that[ languageCode ].dir = that.rtl[ languageCode ] ? 'rtl' : 'ltr';
55 callback( languageCode, that[ languageCode ] );
56 };
57
58 if ( !this[ languageCode ] )
59 CKEDITOR.scriptLoader.load( CKEDITOR.getUrl( 'lang/' + languageCode + '.js' ), loadedCallback, this );
60 else
61 loadedCallback();
62 },
63
64 /**
65 * Returns the language that best fits the user language. For example,
66 * suppose that the user language is "pt-br". If this language is
67 * supported by the editor, it is returned. Otherwise, if only "pt" is
68 * supported, it is returned instead. If none of the previous are
69 * supported, a default language is then returned.
70 *
71 * alert( CKEDITOR.lang.detect( 'en' ) ); // e.g., in a German browser: 'de'
72 *
73 * @param {String} defaultLanguage The default language to be returned
74 * if the user language is not supported.
75 * @param {String} [probeLanguage] A language code to try to use,
76 * instead of the browser-based autodetection.
77 * @returns {String} The detected language code.
78 */
79 detect: function( defaultLanguage, probeLanguage ) {
80 var languages = this.languages;
81 probeLanguage = probeLanguage || navigator.userLanguage || navigator.language || defaultLanguage;
82
83 var parts = probeLanguage.toLowerCase().match( /([a-z]+)(?:-([a-z]+))?/ ),
84 lang = parts[ 1 ],
85 locale = parts[ 2 ];
86
87 if ( languages[ lang + '-' + locale ] )
88 lang = lang + '-' + locale;
89 else if ( !languages[ lang ] )
90 lang = null;
91
92 CKEDITOR.lang.detect = lang ?
93 function() {
94 return lang;
95 } : function( defaultLanguage ) {
96 return defaultLanguage;
97 };
98
99 return lang || defaultLanguage;
100 }
101 };
102
103} )();
diff --git a/sources/core/loader.js b/sources/core/loader.js
new file mode 100644
index 0000000..249a8cf
--- /dev/null
+++ b/sources/core/loader.js
@@ -0,0 +1,225 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 0000000..228789e
--- /dev/null
+++ b/sources/core/log.js
@@ -0,0 +1,127 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines {@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 0000000..9ff7683
--- /dev/null
+++ b/sources/core/plugindefinition.js
@@ -0,0 +1,177 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the "virtual" {@link CKEDITOR.pluginDefinition} class which
8 * contains the defintion of a plugin. This file serves documentation
9 * purposes only.
10 */
11
12/**
13 * A virtual class that just illustrates the features of plugin objects which are
14 * passed to the {@link CKEDITOR.plugins#add} method.
15 *
16 * This class is not really a part of the API, so its constructor should not be called.
17 *
18 * See also:
19 *
20 * * [The Plugin SDK](#!/guide/plugin_sdk_intro)
21 * * [Creating a CKEditor plugin in 20 Lines of Code](#!/guide/plugin_sdk_sample)
22 * * [Creating a Simple Plugin Tutorial](#!/guide/plugin_sdk_sample_1)
23 *
24 * @class CKEDITOR.pluginDefinition
25 * @abstract
26 */
27
28/**
29 * A list of plugins that are required by this plugin. Note that this property
30 * does not determine the loading order of the plugins.
31 *
32 * CKEDITOR.plugins.add( 'sample', {
33 * requires: 'button,selection'
34 * } );
35 *
36 * Or:
37 *
38 * CKEDITOR.plugins.add( 'sample', {
39 * requires: [ 'button', 'selection' ]
40 * } );
41 *
42 * @property {String/String[]} requires
43 */
44
45/**
46 * The list of language files available for this plugin. These files are stored inside
47 * the `lang` directory in the plugin directory, follow the name
48 * pattern of `langCode.js`, and contain the language definition created with
49 * {@link CKEDITOR.plugins#setLang}.
50 *
51 * When the plugin is being loaded, the editor checks this list to see if
52 * a language file in the current editor language ({@link CKEDITOR.editor#langCode})
53 * is available, and if so, loads it. Otherwise, the file represented by the first item
54 * in the list is loaded.
55 *
56 * CKEDITOR.plugins.add( 'sample', {
57 * lang: 'en,fr'
58 * } );
59 *
60 * Or:
61 *
62 * CKEDITOR.plugins.add( 'sample', {
63 * lang: [ 'en', 'fr' ]
64 * } );
65 *
66 * @property {String/String[]} lang
67 */
68
69/**
70 * A function called when the plugin definition is loaded for the first time.
71 * It is usually used to execute some code once for the entire page,
72 * for instance code that uses the {@link CKEDITOR}'s methods such as the {@link CKEDITOR#addCss} method.
73 *
74 * CKEDITOR.plugins.add( 'sample', {
75 * onLoad: function() {
76 * CKEDITOR.addCss( '.cke_some_class { ... }' );
77 * }
78 * } );
79 *
80 * Read more about the initialization order in the {@link #init} method documentation.
81 *
82 * @method onLoad
83 */
84
85/**
86 * A function called on initialization of every editor instance created on the
87 * page before the {@link #init} call task. This feature makes it possible to
88 * initialize things that could be used in the `init` function of other plugins.
89 *
90 * CKEDITOR.plugins.add( 'sample1', {
91 * beforeInit: function( editor ) {
92 * editor.foo = 'bar';
93 * }
94 * } );
95 *
96 * CKEDITOR.plugins.add( 'sample2', {
97 * init: function( editor ) {
98 * // This will work regardless of order in which
99 * // plugins sample1 and sample2 where initialized.
100 * console.log( editor.foo ); // 'bar'
101 * }
102 * } );
103 *
104 * Read more about the initialization order in the {@link #init} method documentation.
105 *
106 * @method beforeInit
107 * @param {CKEDITOR.editor} editor The editor instance being initialized.
108 */
109
110/**
111 * A function called on initialization of every editor instance created on the page.
112 *
113 * CKEDITOR.plugins.add( 'sample', {
114 * init: function( editor ) {
115 * console.log( 'Editor "' + editor.name + '" is being initialized!' );
116 * }
117 * } );
118 *
119 * Initialization order:
120 *
121 * 1. The {@link #beforeInit} methods of all enabled plugins are executed.
122 * 2. The {@link #init} methods of all enabled plugins are executed.
123 * 3. The {@link #afterInit} methods of all enabled plugins are executed.
124 * 4. The {@link CKEDITOR.editor#pluginsLoaded} event is fired.
125 *
126 * **Note:** The order in which the `init` methods are called does not depend on the plugins' {@link #requires requirements}
127 * or the order set in the {@link CKEDITOR.config#plugins} option. It may be random and therefore it is
128 * recommended to use the {@link #beforeInit} and {@link #afterInit} methods in order to ensure
129 * the right execution sequence.
130 *
131 * See also the {@link #onLoad} method.
132 *
133 * @method init
134 * @param {CKEDITOR.editor} editor The editor instance being initialized.
135 */
136
137/**
138 * A function called on initialization of every editor instance created on the
139 * page after the {@link #init} call task. This feature makes it possible to use things
140 * that were initialized in the `init` function of other plugins.
141 *
142 * CKEDITOR.plugins.add( 'sample1', {
143 * afterInit: function( editor ) {
144 * // This will work regardless of order in which
145 * // plugins sample1 and sample2 where initialized.
146 * console.log( editor.foo ); // 'bar'
147 * }
148 * } );
149 *
150 * CKEDITOR.plugins.add( 'sample2', {
151 * init: function( editor ) {
152 * editor.foo = 'bar';
153 * }
154 * } );
155 *
156 * Read more about the initialization order in the {@link #init} method documentation.
157 *
158 * @method afterInit
159 * @param {CKEDITOR.editor} editor The editor instance being initialized.
160 */
161
162/**
163 * Announces the plugin as HiDPI-ready (optimized for high pixel density screens, e.g. *Retina*)
164 * by providing high-resolution icons and images. HiDPI icons must be twice as big
165 * (defaults are `16px x 16px`) and stored under `plugin_name/icons/hidpi/` directory.
166 *
167 * The common place for additional HiDPI images used by the plugin (**but not icons**)
168 * is the `plugin_name/images/hidpi/` directory.
169 *
170 * This property is optional and only makes sense if `32px x 32px` icons
171 * and high-resolution images actually exist. If this flag is set to `true`, the editor
172 * will automatically detect the HiDPI environment and attempt to load the
173 * high-resolution resources.
174 *
175 * @since 4.2
176 * @property {Boolean} hidpi
177 */
diff --git a/sources/core/plugins.js b/sources/core/plugins.js
new file mode 100644
index 0000000..75cc41e
--- /dev/null
+++ b/sources/core/plugins.js
@@ -0,0 +1,119 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 0000000..c33c0f8
--- /dev/null
+++ b/sources/core/resourcemanager.js
@@ -0,0 +1,228 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 '/myplugins/sample/plugin.js'.
128 * CKEDITOR.plugins.addExternal( 'sample', '/myplugins/sample/' );
129 *
130 * // Loads a plugin from '/myplugins/sample/my_plugin.js'.
131 * CKEDITOR.plugins.addExternal( 'sample', '/myplugins/sample/', 'my_plugin.js' );
132 *
133 * // Loads a plugin from '/myplugins/sample/my_plugin.js'.
134 * CKEDITOR.plugins.addExternal( 'sample', '/myplugins/sample/my_plugin.js', '' );
135 *
136 * @param {String} names The resource names, separated by commas.
137 * @param {String} path The path of the folder containing the resource.
138 * @param {String} [fileName] The resource file name. If not provided, the
139 * default name is used. If provided with a empty string, will implicitly indicates that `path` argument
140 * is already the full path.
141 */
142 addExternal: function( names, path, fileName ) {
143 names = names.split( ',' );
144 for ( var i = 0; i < names.length; i++ ) {
145 var name = names[ i ];
146
147 // If "fileName" is not provided, we assume that it may be available
148 // in "path". Try to extract it in this case.
149 if ( !fileName ) {
150 path = path.replace( /[^\/]+$/, function( match ) {
151 fileName = match;
152 return '';
153 } );
154 }
155
156 this.externals[ name ] = {
157 dir: path,
158
159 // Use the default file name if there is no "fileName" and it
160 // was not found in "path".
161 file: fileName || ( this.fileName + '.js' )
162 };
163 }
164 },
165
166 /**
167 * Loads one or more resources.
168 *
169 * CKEDITOR.plugins.load( 'myplugin', function( plugins ) {
170 * alert( plugins[ 'myplugin' ] ); // object
171 * } );
172 *
173 * @param {String/Array} name The name of the resource to load. It may be a
174 * string with a single resource name, or an array with several names.
175 * @param {Function} callback A function to be called when all resources
176 * are loaded. The callback will receive an array containing all loaded names.
177 * @param {Object} [scope] The scope object to be used for the callback call.
178 */
179 load: function( names, callback, scope ) {
180 // Ensure that we have an array of names.
181 if ( !CKEDITOR.tools.isArray( names ) )
182 names = names ? [ names ] : [];
183
184 var loaded = this.loaded,
185 registered = this.registered,
186 urls = [],
187 urlsNames = {},
188 resources = {};
189
190 // Loop through all names.
191 for ( var i = 0; i < names.length; i++ ) {
192 var name = names[ i ];
193
194 if ( !name )
195 continue;
196
197 // If not available yet.
198 if ( !loaded[ name ] && !registered[ name ] ) {
199 var url = this.getFilePath( name );
200 urls.push( url );
201 if ( !( url in urlsNames ) )
202 urlsNames[ url ] = [];
203 urlsNames[ url ].push( name );
204 } else {
205 resources[ name ] = this.get( name );
206 }
207 }
208
209 CKEDITOR.scriptLoader.load( urls, function( completed, failed ) {
210 if ( failed.length ) {
211 throw new Error( '[CKEDITOR.resourceManager.load] Resource name "' + urlsNames[ failed[ 0 ] ].join( ',' ) +
212 '" was not found at "' + failed[ 0 ] + '".' );
213 }
214
215 for ( var i = 0; i < completed.length; i++ ) {
216 var nameList = urlsNames[ completed[ i ] ];
217 for ( var j = 0; j < nameList.length; j++ ) {
218 var name = nameList[ j ];
219 resources[ name ] = this.get( name );
220
221 loaded[ name ] = 1;
222 }
223 }
224
225 callback.call( scope, resources );
226 }, this );
227 }
228};
diff --git a/sources/core/scriptloader.js b/sources/core/scriptloader.js
new file mode 100644
index 0000000..a2b9cca
--- /dev/null
+++ b/sources/core/scriptloader.js
@@ -0,0 +1,202 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 // The onload or onerror event does not fire in IE8 and IE9 Quirks Mode (#14849).
123 if ( CKEDITOR.env.ie && ( CKEDITOR.env.version <= 8 || CKEDITOR.env.ie9Compat ) ) {
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 script.$.onerror = function() {
140 onLoad( url, false );
141 };
142 }
143 }
144
145 // Append it to <head>.
146 script.appendTo( CKEDITOR.document.getHead() );
147
148 CKEDITOR.fire( 'download', url ); // %REMOVE_LINE%
149 };
150
151 showBusy && CKEDITOR.document.getDocumentElement().setStyle( 'cursor', 'wait' );
152 for ( var i = 0; i < scriptCount; i++ ) {
153 loadScript( scriptUrl[ i ] );
154 }
155 },
156
157 /**
158 * Loads a script in a queue, so only one is loaded at the same time.
159 *
160 * @since 4.1.2
161 * @param {String} scriptUrl URL pointing to the script to be loaded.
162 * @param {Function} [callback] A function to be called when the script
163 * is loaded and executed. A boolean parameter is passed to the callback,
164 * indicating the success of the load.
165 *
166 * @see CKEDITOR.scriptLoader#load
167 */
168 queue: ( function() {
169 var pending = [];
170
171 // Loads the very first script from queue and removes it.
172 function loadNext() {
173 var script;
174
175 if ( ( script = pending[ 0 ] ) )
176 this.load( script.scriptUrl, script.callback, CKEDITOR, 0 );
177 }
178
179 return function( scriptUrl, callback ) {
180 var that = this;
181
182 // This callback calls the standard callback for the script
183 // and loads the very next script from pending list.
184 function callbackWrapper() {
185 callback && callback.apply( this, arguments );
186
187 // Removed the just loaded script from the queue.
188 pending.shift();
189
190 loadNext.call( that );
191 }
192
193 // Let's add this script to the queue
194 pending.push( { scriptUrl: scriptUrl, callback: callbackWrapper } );
195
196 // If the queue was empty, then start loading.
197 if ( pending.length == 1 )
198 loadNext.call( this );
199 };
200 } )()
201 };
202} )();
diff --git a/sources/core/selection.js b/sources/core/selection.js
new file mode 100644
index 0000000..eef28a3
--- /dev/null
+++ b/sources/core/selection.js
@@ -0,0 +1,2204 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6( function() {
7 var isMSSelection = typeof window.getSelection != 'function',
8 nextRev = 1,
9 // #13816
10 fillingCharSequence = CKEDITOR.tools.repeat( '\u200b', 7 ),
11 fillingCharSequenceRegExp = new RegExp( fillingCharSequence + '( )?', 'g' );
12
13 // #### checkSelectionChange : START
14
15 // The selection change check basically saves the element parent tree of
16 // the current node and check it on successive requests. If there is any
17 // change on the tree, then the selectionChange event gets fired.
18 function checkSelectionChange() {
19 // A possibly available fake-selection.
20 var sel = this._.fakeSelection,
21 realSel;
22
23 if ( sel ) {
24 realSel = this.getSelection( 1 );
25
26 // If real (not locked/stored) selection was moved from hidden container,
27 // then the fake-selection must be invalidated.
28 if ( !realSel || !realSel.isHidden() ) {
29 // Remove the cache from fake-selection references in use elsewhere.
30 sel.reset();
31
32 // Have the code using the native selection.
33 sel = 0;
34 }
35 }
36
37 // If not fake-selection is available then get the native selection.
38 if ( !sel ) {
39 sel = realSel || this.getSelection( 1 );
40
41 // Editor may have no selection at all.
42 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
43 return;
44 }
45
46 this.fire( 'selectionCheck', sel );
47
48 var currentPath = this.elementPath();
49 if ( !currentPath.compare( this._.selectionPreviousPath ) ) {
50 // Handle case when dialog inserts new element but parent block and path (so also focus context) does not change. (#13362)
51 var sameBlockParent = this._.selectionPreviousPath && this._.selectionPreviousPath.blockLimit.equals( currentPath.blockLimit );
52 // Cache the active element, which we'll eventually lose on Webkit.
53 if ( CKEDITOR.env.webkit && !sameBlockParent )
54 this._.previousActive = this.document.getActive();
55
56 this._.selectionPreviousPath = currentPath;
57 this.fire( 'selectionChange', { selection: sel, path: currentPath } );
58 }
59 }
60
61 var checkSelectionChangeTimer, checkSelectionChangeTimeoutPending;
62
63 function checkSelectionChangeTimeout() {
64 // Firing the "OnSelectionChange" event on every key press started to
65 // be too slow. This function guarantees that there will be at least
66 // 200ms delay between selection checks.
67
68 checkSelectionChangeTimeoutPending = true;
69
70 if ( checkSelectionChangeTimer )
71 return;
72
73 checkSelectionChangeTimeoutExec.call( this );
74
75 checkSelectionChangeTimer = CKEDITOR.tools.setTimeout( checkSelectionChangeTimeoutExec, 200, this );
76 }
77
78 function checkSelectionChangeTimeoutExec() {
79 checkSelectionChangeTimer = null;
80
81 if ( checkSelectionChangeTimeoutPending ) {
82 // Call this with a timeout so the browser properly moves the
83 // selection after the mouseup. It happened that the selection was
84 // being moved after the mouseup when clicking inside selected text
85 // with Firefox.
86 CKEDITOR.tools.setTimeout( checkSelectionChange, 0, this );
87
88 checkSelectionChangeTimeoutPending = false;
89 }
90 }
91
92 // #### checkSelectionChange : END
93
94 var isVisible = CKEDITOR.dom.walker.invisible( 1 );
95
96 // May absorb the caret if:
97 // * is a visible node,
98 // * is a non-empty element (this rule will accept elements like <strong></strong> because they
99 // they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret).
100 // See #12621.
101 function mayAbsorbCaret( node ) {
102 if ( isVisible( node ) )
103 return true;
104
105 if ( node.type == CKEDITOR.NODE_ELEMENT && !node.is( CKEDITOR.dtd.$empty ) )
106 return true;
107
108 return false;
109 }
110
111 function rangeRequiresFix( range ) {
112 // Whether we must prevent from absorbing caret by this context node.
113 // Also checks whether there's an editable position next to that node.
114 function ctxRequiresFix( node, isAtEnd ) {
115 // It's ok for us if a text node absorbs the caret, because
116 // the caret container element isn't changed then.
117 if ( !node || node.type == CKEDITOR.NODE_TEXT )
118 return false;
119
120 var testRng = range.clone();
121 return testRng[ 'moveToElementEdit' + ( isAtEnd ? 'End' : 'Start' ) ]( node );
122 }
123
124 // Range root must be the editable element, it's to avoid creating filler char
125 // on any temporary internal selection.
126 if ( !( range.root instanceof CKEDITOR.editable ) )
127 return false;
128
129 var ct = range.startContainer;
130
131 var previous = range.getPreviousNode( mayAbsorbCaret, null, ct ),
132 next = range.getNextNode( mayAbsorbCaret, null, ct );
133
134 // Any adjacent text container may absorb the caret, e.g.
135 // <p><strong>text</strong>^foo</p>
136 // <p>foo^<strong>text</strong></p>
137 // <div>^<p>foo</p></div>
138 if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) )
139 return true;
140
141 // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (#7222)
142 // If you found this line confusing check #12655.
143 if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) )
144 return true;
145
146 return false;
147 }
148
149 function createFillingCharSequenceNode( editable ) {
150 removeFillingCharSequenceNode( editable, false );
151
152 var fillingChar = editable.getDocument().createText( fillingCharSequence );
153 editable.setCustomData( 'cke-fillingChar', fillingChar );
154
155 return fillingChar;
156 }
157
158 // Checks if a filling char has been used, eventualy removing it (#1272).
159 function checkFillingCharSequenceNodeReady( editable ) {
160 var fillingChar = editable.getCustomData( 'cke-fillingChar' );
161
162 if ( fillingChar ) {
163 // Use this flag to avoid removing the filling char right after
164 // creating it.
165 if ( fillingChar.getCustomData( 'ready' ) ) {
166 removeFillingCharSequenceNode( editable );
167 } else {
168 fillingChar.setCustomData( 'ready', 1 );
169 }
170 }
171 }
172
173 function removeFillingCharSequenceNode( editable, keepSelection ) {
174 var fillingChar = editable && editable.removeCustomData( 'cke-fillingChar' );
175
176 if ( fillingChar ) {
177 // Text selection position might get mangled by
178 // subsequent dom modification, save it now for restoring. (#8617)
179 if ( keepSelection !== false ) {
180 var sel = editable.getDocument().getSelection().getNative(),
181 // Be error proof.
182 range = sel && sel.type != 'None' && sel.getRangeAt( 0 ),
183 fillingCharSeqLength = fillingCharSequence.length;
184
185 // If there's some text other than the sequence in the FC text node and the range
186 // intersects with that node...
187 if ( fillingChar.getLength() > fillingCharSeqLength && range && range.intersectsNode( fillingChar.$ ) ) {
188 var bm = createNativeSelectionBookmark( sel );
189
190 // Correct start offset anticipating the removal of FC.
191 if ( sel.anchorNode == fillingChar.$ && sel.anchorOffset > fillingCharSeqLength ) {
192 bm[ 0 ].offset -= fillingCharSeqLength;
193 }
194
195 // Correct end offset anticipating the removal of FC.
196 if ( sel.focusNode == fillingChar.$ && sel.focusOffset > fillingCharSeqLength ) {
197 bm[ 1 ].offset -= fillingCharSeqLength;
198 }
199 }
200 }
201
202 // We can't simply remove the filling node because the user
203 // will actually enlarge it when typing, so we just remove the
204 // invisible char from it.
205 fillingChar.setText( removeFillingCharSequenceString( fillingChar.getText(), 1 ) );
206
207 // Restore the bookmark preserving selection's direction.
208 if ( bm ) {
209 moveNativeSelectionToBookmark( editable.getDocument().$, bm );
210 }
211 }
212 }
213
214 // #13816
215 function removeFillingCharSequenceString( str, nbspAware ) {
216 if ( nbspAware ) {
217 return str.replace( fillingCharSequenceRegExp, function( m, p ) {
218 // #10291 if filling char is followed by a space replace it with NBSP.
219 return p ? '\xa0' : '';
220 } );
221 } else {
222 return str.replace( fillingCharSequence, '' );
223 }
224 }
225
226 function createNativeSelectionBookmark( sel ) {
227 return [
228 { node: sel.anchorNode, offset: sel.anchorOffset },
229 { node: sel.focusNode, offset: sel.focusOffset }
230 ];
231 }
232
233 function moveNativeSelectionToBookmark( document, bm ) {
234 var sel = document.getSelection(),
235 range = document.createRange();
236
237 range.setStart( bm[ 0 ].node, bm[ 0 ].offset );
238 range.collapse( true );
239 sel.removeAllRanges();
240 sel.addRange( range );
241 sel.extend( bm[ 1 ].node, bm[ 1 ].offset );
242 }
243
244 // Creates cke_hidden_sel container and puts real selection there.
245 function hideSelection( editor, ariaLabel ) {
246 var content = ariaLabel || '&nbsp;',
247 style = CKEDITOR.env.ie && CKEDITOR.env.version < 14 ? 'display:none' : 'position:fixed;top:0;left:-1000px',
248 hiddenEl = CKEDITOR.dom.element.createFromHtml(
249 '<div data-cke-hidden-sel="1" data-cke-temp="1" style="' + style + '">' + content + '</div>',
250 editor.document );
251
252 editor.fire( 'lockSnapshot' );
253
254 editor.editable().append( hiddenEl );
255
256 // Always use real selection to avoid overriding locked one (http://dev.ckeditor.com/ticket/11104#comment:13).
257 var sel = editor.getSelection( 1 ),
258 range = editor.createRange(),
259 // Cancel selectionchange fired by selectRanges - prevent from firing selectionChange.
260 listener = sel.root.on( 'selectionchange', function( evt ) {
261 evt.cancel();
262 }, null, null, 0 );
263
264 range.setStartAt( hiddenEl, CKEDITOR.POSITION_AFTER_START );
265 range.setEndAt( hiddenEl, CKEDITOR.POSITION_BEFORE_END );
266 sel.selectRanges( [ range ] );
267
268 listener.removeListener();
269
270 editor.fire( 'unlockSnapshot' );
271
272 // Set this value at the end, so reset() executed by selectRanges()
273 // will clean up old hidden selection container.
274 editor._.hiddenSelectionContainer = hiddenEl;
275 }
276
277 function removeHiddenSelectionContainer( editor ) {
278 var hiddenEl = editor._.hiddenSelectionContainer;
279
280 if ( hiddenEl ) {
281 var isDirty = editor.checkDirty();
282
283 editor.fire( 'lockSnapshot' );
284 hiddenEl.remove();
285 editor.fire( 'unlockSnapshot' );
286
287 !isDirty && editor.resetDirty();
288 }
289
290 delete editor._.hiddenSelectionContainer;
291 }
292
293 // Object containing keystroke handlers for fake selection.
294 var fakeSelectionDefaultKeystrokeHandlers = ( function() {
295 function leave( right ) {
296 return function( evt ) {
297 var range = evt.editor.createRange();
298
299 // Move selection only if there's a editable place for it.
300 // It no, then do nothing (keystroke will be blocked, widget selection kept).
301 if ( range.moveToClosestEditablePosition( evt.selected, right ) )
302 evt.editor.getSelection().selectRanges( [ range ] );
303
304 // Prevent default.
305 return false;
306 };
307 }
308
309 function del( right ) {
310 return function( evt ) {
311 var editor = evt.editor,
312 range = editor.createRange(),
313 found;
314
315 // If haven't found place for caret on the default side,
316 // try to find it on the other side.
317 if ( !( found = range.moveToClosestEditablePosition( evt.selected, right ) ) )
318 found = range.moveToClosestEditablePosition( evt.selected, !right );
319
320 if ( found )
321 editor.getSelection().selectRanges( [ range ] );
322
323 // Save the state before removing selected element.
324 editor.fire( 'saveSnapshot' );
325
326 evt.selected.remove();
327
328 // Haven't found any editable space before removing element,
329 // try to place the caret anywhere (most likely, in empty editable).
330 if ( !found ) {
331 range.moveToElementEditablePosition( editor.editable() );
332 editor.getSelection().selectRanges( [ range ] );
333 }
334
335 editor.fire( 'saveSnapshot' );
336
337 // Prevent default.
338 return false;
339 };
340 }
341
342 var leaveLeft = leave(),
343 leaveRight = leave( 1 );
344
345 return {
346 37: leaveLeft, // LEFT
347 38: leaveLeft, // UP
348 39: leaveRight, // RIGHT
349 40: leaveRight, // DOWN
350 8: del(), // BACKSPACE
351 46: del( 1 ) // DELETE
352 };
353 } )();
354
355 // Handle left, right, delete and backspace keystrokes next to non-editable elements
356 // by faking selection on them.
357 function getOnKeyDownListener( editor ) {
358 var keystrokes = { 37: 1, 39: 1, 8: 1, 46: 1 };
359
360 return function( evt ) {
361 var keystroke = evt.data.getKeystroke();
362
363 // Handle only left/right/del/bspace keys.
364 if ( !keystrokes[ keystroke ] )
365 return;
366
367 var sel = editor.getSelection(),
368 ranges = sel.getRanges(),
369 range = ranges[ 0 ];
370
371 // Handle only single range and it has to be collapsed.
372 if ( ranges.length != 1 || !range.collapsed )
373 return;
374
375 var next = range[ keystroke < 38 ? 'getPreviousEditableNode' : 'getNextEditableNode' ]();
376
377 if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getAttribute( 'contenteditable' ) == 'false' ) {
378 editor.getSelection().fake( next );
379 evt.data.preventDefault();
380 evt.cancel();
381 }
382 };
383 }
384
385 // If fake selection should be applied this function will return instance of
386 // CKEDITOR.dom.element which should gain fake selection.
387 function getNonEditableFakeSelectionReceiver( ranges ) {
388 var enclosedNode, shrinkedNode, clone, range;
389
390 if ( ranges.length == 1 && !( range = ranges[ 0 ] ).collapsed &&
391 ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) {
392 // So far we can't say that enclosed element is non-editable. Before checking,
393 // we'll shrink range (clone). Shrinking will stop on non-editable range, or
394 // innermost element (#11114).
395 clone = range.clone();
396 clone.shrink( CKEDITOR.SHRINK_ELEMENT, true );
397
398 // If shrinked range still encloses an element, check this one (shrink stops only on non-editable elements).
399 if ( ( shrinkedNode = clone.getEnclosedNode() ) && shrinkedNode.type == CKEDITOR.NODE_ELEMENT )
400 enclosedNode = shrinkedNode;
401
402 if ( enclosedNode.getAttribute( 'contenteditable' ) == 'false' )
403 return enclosedNode;
404 }
405 }
406
407 // Fix ranges which may end after hidden selection container.
408 // Note: this function may only be used if hidden selection container
409 // is not in DOM any more.
410 function fixRangesAfterHiddenSelectionContainer( ranges, root ) {
411 var range;
412 for ( var i = 0; i < ranges.length; ++i ) {
413 range = ranges[ i ];
414 if ( range.endContainer.equals( root ) ) {
415 // We can use getChildCount() because hidden selection container is not in DOM.
416 range.endOffset = Math.min( range.endOffset, root.getChildCount() );
417 }
418 }
419 }
420
421 // Extract only editable part or ranges.
422 // Note: this function modifies ranges list!
423 // @param {CKEDITOR.dom.rangeList} ranges
424 function extractEditableRanges( ranges ) {
425 for ( var i = 0; i < ranges.length; i++ ) {
426 var range = ranges[ i ];
427
428 // Drop range spans inside one ready-only node.
429 var parent = range.getCommonAncestor();
430 if ( parent.isReadOnly() )
431 ranges.splice( i, 1 );
432
433 if ( range.collapsed )
434 continue;
435
436 // Range may start inside a non-editable element,
437 // replace the range start after it.
438 if ( range.startContainer.isReadOnly() ) {
439 var current = range.startContainer,
440 isElement;
441
442 while ( current ) {
443 isElement = current.type == CKEDITOR.NODE_ELEMENT;
444
445 if ( ( isElement && current.is( 'body' ) ) || !current.isReadOnly() )
446 break;
447
448 if ( isElement && current.getAttribute( 'contentEditable' ) == 'false' )
449 range.setStartAfter( current );
450
451 current = current.getParent();
452 }
453 }
454
455 var startContainer = range.startContainer,
456 endContainer = range.endContainer,
457 startOffset = range.startOffset,
458 endOffset = range.endOffset,
459 walkerRange = range.clone();
460
461 // Enlarge range start/end with text node to avoid walker
462 // being DOM destructive, it doesn't interfere our checking
463 // of elements below as well.
464 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
465 if ( startOffset >= startContainer.getLength() )
466 walkerRange.setStartAfter( startContainer );
467 else
468 walkerRange.setStartBefore( startContainer );
469 }
470
471 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
472 if ( !endOffset )
473 walkerRange.setEndBefore( endContainer );
474 else
475 walkerRange.setEndAfter( endContainer );
476 }
477
478 // Looking for non-editable element inside the range.
479 var walker = new CKEDITOR.dom.walker( walkerRange );
480 walker.evaluator = function( node ) {
481 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isReadOnly() ) {
482 var newRange = range.clone();
483 range.setEndBefore( node );
484
485 // Drop collapsed range around read-only elements,
486 // it make sure the range list empty when selecting
487 // only non-editable elements.
488 if ( range.collapsed )
489 ranges.splice( i--, 1 );
490
491 // Avoid creating invalid range.
492 if ( !( node.getPosition( walkerRange.endContainer ) & CKEDITOR.POSITION_CONTAINS ) ) {
493 newRange.setStartAfter( node );
494 if ( !newRange.collapsed )
495 ranges.splice( i + 1, 0, newRange );
496 }
497
498 return true;
499 }
500
501 return false;
502 };
503
504 walker.next();
505 }
506
507 return ranges;
508 }
509
510 // Setup all editor instances for the necessary selection hooks.
511 CKEDITOR.on( 'instanceCreated', function( ev ) {
512 var editor = ev.editor;
513
514 editor.on( 'contentDom', function() {
515 var doc = editor.document,
516 outerDoc = CKEDITOR.document,
517 editable = editor.editable(),
518 body = doc.getBody(),
519 html = doc.getDocumentElement();
520
521 var isInline = editable.isInline();
522
523 var restoreSel,
524 lastSel;
525
526 // Give the editable an initial selection on first focus,
527 // put selection at a consistent position at the start
528 // of the contents. (#9507)
529 if ( CKEDITOR.env.gecko ) {
530 editable.attachListener( editable, 'focus', function( evt ) {
531 evt.removeListener();
532
533 if ( restoreSel !== 0 ) {
534 var nativ = editor.getSelection().getNative();
535 // Do it only if the native selection is at an unwanted
536 // place (at the very start of the editable). #10119
537 if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) {
538 var rng = editor.createRange();
539 rng.moveToElementEditStart( editable );
540 rng.select();
541 }
542 }
543 }, null, null, -2 );
544 }
545
546 // Plays the magic here to restore/save dom selection on editable focus/blur.
547 editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusIn' : 'focus', function() {
548 // On Webkit we use DOMFocusIn which is fired more often than focus - e.g. when moving from main editable
549 // to nested editable (or the opposite). Unlock selection all, but restore only when it was locked
550 // for the same active element, what will e.g. mean restoring after displaying dialog.
551 if ( restoreSel && CKEDITOR.env.webkit ) {
552 restoreSel = editor._.previousActive && editor._.previousActive.equals( doc.getActive() );
553
554 // On Webkit when editor uses divarea, native focus causes editable viewport to scroll
555 // to the top (when there is no active selection inside while focusing) so the scroll
556 // position should be restored after focusing back editable area. (#14659)
557 if ( restoreSel && editor._.previousScrollTop != null && editor._.previousScrollTop != editable.$.scrollTop ) {
558 editable.$.scrollTop = editor._.previousScrollTop;
559 }
560 }
561
562 editor.unlockSelection( restoreSel );
563 restoreSel = 0;
564 }, null, null, -1 );
565
566 // Disable selection restoring when clicking in.
567 editable.attachListener( editable, 'mousedown', function() {
568 restoreSel = 0;
569 } );
570
571 // Save a cloned version of current selection.
572 function saveSel() {
573 lastSel = new CKEDITOR.dom.selection( editor.getSelection() );
574 lastSel.lock();
575 }
576
577 // Browsers could loose the selection once the editable lost focus,
578 // in such case we need to reproduce it by saving a locked selection
579 // and restoring it upon focus gain.
580 if ( CKEDITOR.env.ie || isInline ) {
581 // For old IEs, we can retrieve the last correct DOM selection upon the "beforedeactivate" event.
582 // For the rest, a more frequent check is required for each selection change made.
583 if ( isMSSelection )
584 editable.attachListener( editable, 'beforedeactivate', saveSel, null, null, -1 );
585 else
586 editable.attachListener( editor, 'selectionCheck', saveSel, null, null, -1 );
587
588 // Lock the selection and mark it to be restored.
589 // On Webkit we use DOMFocusOut which is fired more often than blur. I.e. it will also be
590 // fired when nested editable is blurred.
591 editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusOut' : 'blur', function() {
592 editor.lockSelection( lastSel );
593 restoreSel = 1;
594 }, null, null, -1 );
595
596 // Disable selection restoring when clicking in.
597 editable.attachListener( editable, 'mousedown', function() {
598 restoreSel = 0;
599 } );
600 }
601
602 // The following selection-related fixes only apply to classic (`iframe`-based) editable.
603 if ( CKEDITOR.env.ie && !isInline ) {
604 var scroll;
605 editable.attachListener( editable, 'mousedown', function( evt ) {
606 // IE scrolls document to top on right mousedown
607 // when editor has no focus, remember this scroll
608 // position and revert it before context menu opens. (#5778)
609 if ( evt.data.$.button == 2 ) {
610 var sel = editor.document.getSelection();
611 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
612 scroll = editor.window.getScrollPosition();
613 }
614 } );
615
616 editable.attachListener( editable, 'mouseup', function( evt ) {
617 // Restore recorded scroll position when needed on right mouseup.
618 if ( evt.data.$.button == 2 && scroll ) {
619 editor.document.$.documentElement.scrollLeft = scroll.x;
620 editor.document.$.documentElement.scrollTop = scroll.y;
621 }
622 scroll = null;
623 } );
624
625 // When content doc is in standards mode, IE doesn't focus the editor when
626 // clicking at the region below body (on html element) content, we emulate
627 // the normal behavior on old IEs. (#1659, #7932)
628 if ( doc.$.compatMode != 'BackCompat' ) {
629 if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
630 var textRng,
631 startRng;
632
633 html.on( 'mousedown', function( evt ) {
634 evt = evt.data;
635
636 // Expand the text range along with mouse move.
637 function onHover( evt ) {
638 evt = evt.data.$;
639 if ( textRng ) {
640 // Read the current cursor.
641 var rngEnd = body.$.createTextRange();
642
643 moveRangeToPoint( rngEnd, evt.clientX, evt.clientY );
644
645 // Handle drag directions.
646 textRng.setEndPoint(
647 startRng.compareEndPoints( 'StartToStart', rngEnd ) < 0 ?
648 'EndToEnd' : 'StartToStart', rngEnd );
649
650 // Update selection with new range.
651 textRng.select();
652 }
653 }
654
655 function removeListeners() {
656 outerDoc.removeListener( 'mouseup', onSelectEnd );
657 html.removeListener( 'mouseup', onSelectEnd );
658 }
659
660 function onSelectEnd() {
661 html.removeListener( 'mousemove', onHover );
662 removeListeners();
663
664 // Make it in effect on mouse up. (#9022)
665 textRng.select();
666 }
667
668
669 // We're sure that the click happens at the region
670 // below body, but not on scrollbar.
671 if ( evt.getTarget().is( 'html' ) &&
672 evt.$.y < html.$.clientHeight &&
673 evt.$.x < html.$.clientWidth ) {
674 // Start to build the text range.
675 textRng = body.$.createTextRange();
676 moveRangeToPoint( textRng, evt.$.clientX, evt.$.clientY );
677
678 // Records the dragging start of the above text range.
679 startRng = textRng.duplicate();
680
681 html.on( 'mousemove', onHover );
682 outerDoc.on( 'mouseup', onSelectEnd );
683 html.on( 'mouseup', onSelectEnd );
684 }
685 } );
686 }
687
688 // It's much simpler for IE8+, we just need to reselect the reported range.
689 // This hack does not work on IE>=11 because there's no old selection&range APIs.
690 if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) {
691 html.on( 'mousedown', function( evt ) {
692 if ( evt.data.getTarget().is( 'html' ) ) {
693 // Limit the text selection mouse move inside of editable. (#9715)
694 outerDoc.on( 'mouseup', onSelectEnd );
695 html.on( 'mouseup', onSelectEnd );
696 }
697 } );
698 }
699 }
700 }
701
702 // We check the selection change:
703 // 1. Upon "selectionchange" event from the editable element. (which might be faked event fired by our code)
704 // 2. After the accomplish of keyboard and mouse events.
705 editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor );
706 editable.attachListener( editable, 'keyup', checkSelectionChangeTimeout, editor );
707 // Always fire the selection change on focus gain.
708 // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and
709 // we need synchronization between those listeners to not lost cached editor._.previousActive property
710 // (which is updated on selectionCheck).
711 editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusIn' : 'focus', function() {
712 editor.forceNextSelectionCheck();
713 editor.selectionChange( 1 );
714 } );
715
716 // #9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
717 // by dragging and releasing mouse button outside editable. Dragging (mousedown)
718 // has to be initialized in editable, but for mouseup we listen on document element.
719 if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) {
720 var mouseDown;
721 editable.attachListener( editable, 'mousedown', function() {
722 mouseDown = 1;
723 } );
724 editable.attachListener( doc.getDocumentElement(), 'mouseup', function() {
725 if ( mouseDown )
726 checkSelectionChangeTimeout.call( editor );
727 mouseDown = 0;
728 } );
729 }
730 // In all other cases listen on simple mouseup over editable, as we did before #9699.
731 //
732 // Use document instead of editable in non-IEs for observing mouseup
733 // since editable won't fire the event if selection process started within iframe and ended out
734 // of the editor (#9851).
735 else {
736 editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor );
737 }
738
739 if ( CKEDITOR.env.webkit ) {
740 // Before keystroke is handled by editor, check to remove the filling char.
741 editable.attachListener( doc, 'keydown', function( evt ) {
742 var key = evt.data.getKey();
743 // Remove the filling char before some keys get
744 // executed, so they'll not get blocked by it.
745 switch ( key ) {
746 case 13: // ENTER
747 case 33: // PAGEUP
748 case 34: // PAGEDOWN
749 case 35: // HOME
750 case 36: // END
751 case 37: // LEFT-ARROW
752 case 39: // RIGHT-ARROW
753 case 8: // BACKSPACE
754 case 45: // INS
755 case 46: // DEl
756 removeFillingCharSequenceNode( editable );
757 }
758
759 }, null, null, -1 );
760 }
761
762 // Automatically select non-editable element when navigating into
763 // it by left/right or backspace/del keys.
764 editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 );
765
766 function moveRangeToPoint( range, x, y ) {
767 // Error prune in IE7. (#9034, #9110)
768 try {
769 range.moveToPoint( x, y );
770 } catch ( e ) {}
771 }
772
773 function removeListeners() {
774 outerDoc.removeListener( 'mouseup', onSelectEnd );
775 html.removeListener( 'mouseup', onSelectEnd );
776 }
777
778 function onSelectEnd() {
779 removeListeners();
780
781 // The event is not fired when clicking on the scrollbars,
782 // so we can safely check the following to understand
783 // whether the empty space following <body> has been clicked.
784 var sel = CKEDITOR.document.$.selection,
785 range = sel.createRange();
786
787 // The selection range is reported on host, but actually it should applies to the content doc.
788 if ( sel.type != 'None' && range.parentElement().ownerDocument == doc.$ )
789 range.select();
790 }
791 } );
792
793 editor.on( 'setData', function() {
794 // Invalidate locked selection when unloading DOM.
795 // (#9521, #5217#comment:32 and #11500#comment:11)
796 editor.unlockSelection();
797
798 // Webkit's selection will mess up after the data loading.
799 if ( CKEDITOR.env.webkit )
800 clearSelection();
801 } );
802
803 // Catch all the cases which above setData listener couldn't catch.
804 // For example: switching to source mode and destroying editor.
805 editor.on( 'contentDomUnload', function() {
806 editor.unlockSelection();
807 } );
808
809 // IE9 might cease to work if there's an object selection inside the iframe (#7639).
810 if ( CKEDITOR.env.ie9Compat )
811 editor.on( 'beforeDestroy', clearSelection, null, null, 9 );
812
813 // Check selection change on data reload.
814 editor.on( 'dataReady', function() {
815 // Clean up fake selection after setting data.
816 delete editor._.fakeSelection;
817 delete editor._.hiddenSelectionContainer;
818
819 editor.selectionChange( 1 );
820 } );
821
822 // When loaded data are ready check whether hidden selection container was not loaded.
823 editor.on( 'loadSnapshot', function() {
824 var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ),
825 // TODO replace with el.find() which will be introduced in #9764,
826 // because it may happen that hidden sel container won't be the last element.
827 last = editor.editable().getLast( isElement );
828
829 if ( last && last.hasAttribute( 'data-cke-hidden-sel' ) ) {
830 last.remove();
831
832 // Firefox does a very unfortunate thing. When a non-editable element is the only
833 // element in the editable, when we remove the hidden selection container, Firefox
834 // will insert a bogus <br> at the beginning of the editable...
835 // See: https://bugzilla.mozilla.org/show_bug.cgi?id=911201
836 //
837 // This behavior is never desired because this <br> pushes the content lower, but in
838 // this case it is especially dangerous, because it happens when a bookmark is being restored.
839 // Since this <br> is inserted at the beginning it changes indexes and thus breaks the bookmark2
840 // what results in errors.
841 //
842 // So... let's revert what Firefox broke.
843 if ( CKEDITOR.env.gecko ) {
844 var first = editor.editable().getFirst( isElement );
845 if ( first && first.is( 'br' ) && first.getAttribute( '_moz_editor_bogus_node' ) ) {
846 first.remove();
847 }
848 }
849 }
850 }, null, null, 100 );
851
852 editor.on( 'key', function( evt ) {
853 if ( editor.mode != 'wysiwyg' )
854 return;
855
856 var sel = editor.getSelection();
857 if ( !sel.isFake )
858 return;
859
860 var handler = fakeSelectionDefaultKeystrokeHandlers[ evt.data.keyCode ];
861 if ( handler )
862 return handler( { editor: editor, selected: sel.getSelectedElement(), selection: sel, keyEvent: evt } );
863 } );
864
865 function clearSelection() {
866 var sel = editor.getSelection();
867 sel && sel.removeAllRanges();
868 }
869 } );
870
871 // On WebKit only, we need a special "filling" char on some situations
872 // (#1272). Here we set the events that should invalidate that char.
873 if ( CKEDITOR.env.webkit ) {
874 CKEDITOR.on( 'instanceReady', function( evt ) {
875 var editor = evt.editor;
876
877 editor.on( 'selectionChange', function() {
878 checkFillingCharSequenceNodeReady( editor.editable() );
879 }, null, null, -1 );
880
881 editor.on( 'beforeSetMode', function() {
882 removeFillingCharSequenceNode( editor.editable() );
883 }, null, null, -1 );
884
885 // Filter Undo snapshot's HTML to get rid of Filling Char Sequence.
886 // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's
887 // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (#13816).
888 editor.on( 'getSnapshot', function( evt ) {
889 if ( evt.data ) {
890 evt.data = removeFillingCharSequenceString( evt.data );
891 }
892 }, editor, null, 20 );
893
894 // Filter data to get rid of Filling Char Sequence. Filter on #toDataFormat
895 // instead of #getData because once removed, FCSeq may leave an empty element,
896 // which should be pruned by the dataProcessor (#13816).
897 // Note: Used low priority to filter when dataProcessor works on strings,
898 // not pseudo–DOM.
899 editor.on( 'toDataFormat', function( evt ) {
900 evt.data.dataValue = removeFillingCharSequenceString( evt.data.dataValue );
901 }, null, null, 0 );
902 } );
903 }
904
905 /**
906 * Check the selection change in editor and potentially fires
907 * the {@link CKEDITOR.editor#event-selectionChange} event.
908 *
909 * @method
910 * @member CKEDITOR.editor
911 * @param {Boolean} [checkNow=false] Force the check to happen immediately
912 * instead of coming with a timeout delay (default).
913 */
914 CKEDITOR.editor.prototype.selectionChange = function( checkNow ) {
915 ( checkNow ? checkSelectionChange : checkSelectionChangeTimeout ).call( this );
916 };
917
918 /**
919 * Retrieve the editor selection in scope of editable element.
920 *
921 * **Note:** Since the native browser selection provides only one single
922 * selection at a time per document, so if editor's editable element has lost focus,
923 * this method will return a null value unless the {@link CKEDITOR.editor#lockSelection}
924 * has been called beforehand so the saved selection is retrieved.
925 *
926 * var selection = CKEDITOR.instances.editor1.getSelection();
927 * alert( selection.getType() );
928 *
929 * @method
930 * @member CKEDITOR.editor
931 * @param {Boolean} forceRealSelection Return real selection, instead of saved or fake one.
932 * @returns {CKEDITOR.dom.selection} A selection object or null if not available for the moment.
933 */
934 CKEDITOR.editor.prototype.getSelection = function( forceRealSelection ) {
935
936 // Check if there exists a locked or fake selection.
937 if ( ( this._.savedSelection || this._.fakeSelection ) && !forceRealSelection )
938 return this._.savedSelection || this._.fakeSelection;
939
940 // Editable element might be absent or editor might not be in a wysiwyg mode.
941 var editable = this.editable();
942 return editable && this.mode == 'wysiwyg' ? new CKEDITOR.dom.selection( editable ) : null;
943 };
944
945 /**
946 * Locks the selection made in the editor in order to make it possible to
947 * manipulate it without browser interference. A locked selection is
948 * cached and remains unchanged until it is released with the
949 * {@link CKEDITOR.editor#unlockSelection} method.
950 *
951 * @method
952 * @member CKEDITOR.editor
953 * @param {CKEDITOR.dom.selection} [sel] Specify the selection to be locked.
954 * @returns {Boolean} `true` if selection was locked.
955 */
956 CKEDITOR.editor.prototype.lockSelection = function( sel ) {
957 sel = sel || this.getSelection( 1 );
958 if ( sel.getType() != CKEDITOR.SELECTION_NONE ) {
959 !sel.isLocked && sel.lock();
960 this._.savedSelection = sel;
961 return true;
962 }
963 return false;
964 };
965
966 /**
967 * Unlocks the selection made in the editor and locked with the
968 * {@link CKEDITOR.editor#unlockSelection} method. An unlocked selection
969 * is no longer cached and can be changed.
970 *
971 * @method
972 * @member CKEDITOR.editor
973 * @param {Boolean} [restore] If set to `true`, the selection is
974 * restored back to the selection saved earlier by using the
975 * {@link CKEDITOR.dom.selection#lock} method.
976 */
977 CKEDITOR.editor.prototype.unlockSelection = function( restore ) {
978 var sel = this._.savedSelection;
979 if ( sel ) {
980 sel.unlock( restore );
981 delete this._.savedSelection;
982 return true;
983 }
984
985 return false;
986 };
987
988 /**
989 * @method
990 * @member CKEDITOR.editor
991 * @todo
992 */
993 CKEDITOR.editor.prototype.forceNextSelectionCheck = function() {
994 delete this._.selectionPreviousPath;
995 };
996
997 /**
998 * Gets the current selection in context of the document's body element.
999 *
1000 * var selection = CKEDITOR.instances.editor1.document.getSelection();
1001 * alert( selection.getType() );
1002 *
1003 * @method
1004 * @member CKEDITOR.dom.document
1005 * @returns {CKEDITOR.dom.selection} A selection object.
1006 */
1007 CKEDITOR.dom.document.prototype.getSelection = function() {
1008 return new CKEDITOR.dom.selection( this );
1009 };
1010
1011 /**
1012 * Select this range as the only one with {@link CKEDITOR.dom.selection#selectRanges}.
1013 *
1014 * @method
1015 * @returns {CKEDITOR.dom.selection}
1016 * @member CKEDITOR.dom.range
1017 */
1018 CKEDITOR.dom.range.prototype.select = function() {
1019 var sel = this.root instanceof CKEDITOR.editable ? this.root.editor.getSelection() : new CKEDITOR.dom.selection( this.root );
1020
1021 sel.selectRanges( [ this ] );
1022
1023 return sel;
1024 };
1025
1026 /**
1027 * No selection.
1028 *
1029 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE )
1030 * alert( 'Nothing is selected' );
1031 *
1032 * @readonly
1033 * @property {Number} [=1]
1034 * @member CKEDITOR
1035 */
1036 CKEDITOR.SELECTION_NONE = 1;
1037
1038 /**
1039 * A text or a collapsed selection.
1040 *
1041 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
1042 * alert( 'A text is selected' );
1043 *
1044 * @readonly
1045 * @property {Number} [=2]
1046 * @member CKEDITOR
1047 */
1048 CKEDITOR.SELECTION_TEXT = 2;
1049
1050 /**
1051 * Element selection.
1052 *
1053 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT )
1054 * alert( 'An element is selected' );
1055 *
1056 * @readonly
1057 * @property {Number} [=3]
1058 * @member CKEDITOR
1059 */
1060 CKEDITOR.SELECTION_ELEMENT = 3;
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.tools.extend( CKEDITOR.dom.selection, {
1163 _removeFillingCharSequenceString: removeFillingCharSequenceString,
1164 _createFillingCharSequenceNode: createFillingCharSequenceNode,
1165
1166 /**
1167 * The sequence used in a WebKit-based browser to create a Filling Character. By default it is
1168 * a string of 7 zero-width space characters (U+200B).
1169 *
1170 * @since 4.5.7
1171 * @readonly
1172 * @property {String}
1173 */
1174 FILLING_CHAR_SEQUENCE: fillingCharSequence
1175 } );
1176
1177 CKEDITOR.dom.selection.prototype = {
1178 /**
1179 * Gets the native selection object from the browser.
1180 *
1181 * var selection = editor.getSelection().getNative();
1182 *
1183 * @returns {Object} The native browser selection object.
1184 */
1185 getNative: function() {
1186 if ( this._.cache.nativeSel !== undefined )
1187 return this._.cache.nativeSel;
1188
1189 return ( this._.cache.nativeSel = isMSSelection ? this.document.$.selection : this.document.getWindow().$.getSelection() );
1190 },
1191
1192 /**
1193 * Gets the type of the current selection. The following values are
1194 * available:
1195 *
1196 * * {@link CKEDITOR#SELECTION_NONE} (1): No selection.
1197 * * {@link CKEDITOR#SELECTION_TEXT} (2): A text or a collapsed selection is selected.
1198 * * {@link CKEDITOR#SELECTION_ELEMENT} (3): An element is selected.
1199 *
1200 * Example:
1201 *
1202 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
1203 * alert( 'A text is selected' );
1204 *
1205 * @method
1206 * @returns {Number} One of the following constant values: {@link CKEDITOR#SELECTION_NONE},
1207 * {@link CKEDITOR#SELECTION_TEXT} or {@link CKEDITOR#SELECTION_ELEMENT}.
1208 */
1209 getType: isMSSelection ?
1210 function() {
1211 var cache = this._.cache;
1212 if ( cache.type )
1213 return cache.type;
1214
1215 var type = CKEDITOR.SELECTION_NONE;
1216
1217 try {
1218 var sel = this.getNative(),
1219 ieType = sel.type;
1220
1221 if ( ieType == 'Text' )
1222 type = CKEDITOR.SELECTION_TEXT;
1223
1224 if ( ieType == 'Control' )
1225 type = CKEDITOR.SELECTION_ELEMENT;
1226
1227 // It is possible that we can still get a text range
1228 // object even when type == 'None' is returned by IE.
1229 // So we'd better check the object returned by
1230 // createRange() rather than by looking at the type.
1231 if ( sel.createRange().parentElement() )
1232 type = CKEDITOR.SELECTION_TEXT;
1233 } catch ( e ) {}
1234
1235 return ( cache.type = type );
1236 } : function() {
1237 var cache = this._.cache;
1238 if ( cache.type )
1239 return cache.type;
1240
1241 var type = CKEDITOR.SELECTION_TEXT;
1242
1243 var sel = this.getNative();
1244
1245 if ( !( sel && sel.rangeCount ) )
1246 type = CKEDITOR.SELECTION_NONE;
1247 else if ( sel.rangeCount == 1 ) {
1248 // Check if the actual selection is a control (IMG,
1249 // TABLE, HR, etc...).
1250
1251 var range = sel.getRangeAt( 0 ),
1252 startContainer = range.startContainer;
1253
1254 if ( startContainer == range.endContainer && startContainer.nodeType == 1 &&
1255 ( range.endOffset - range.startOffset ) == 1 &&
1256 styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] ) {
1257 type = CKEDITOR.SELECTION_ELEMENT;
1258 }
1259
1260 }
1261
1262 return ( cache.type = type );
1263 },
1264
1265 /**
1266 * Retrieves the {@link CKEDITOR.dom.range} instances that represent the current selection.
1267 *
1268 * Note: Some browsers return multiple ranges even for a continuous selection. Firefox, for example, returns
1269 * one range for each table cell when one or more table rows are selected.
1270 *
1271 * var ranges = selection.getRanges();
1272 * alert( ranges.length );
1273 *
1274 * @method
1275 * @param {Boolean} [onlyEditables] If set to `true`, this function retrives editable ranges only.
1276 * @returns {Array} Range instances that represent the current selection.
1277 */
1278 getRanges: ( function() {
1279 var func = isMSSelection ? ( function() {
1280 function getNodeIndex( node ) {
1281 return new CKEDITOR.dom.node( node ).getIndex();
1282 }
1283
1284 // Finds the container and offset for a specific boundary
1285 // of an IE range.
1286 var getBoundaryInformation = function( range, start ) {
1287 // Creates a collapsed range at the requested boundary.
1288 range = range.duplicate();
1289 range.collapse( start );
1290
1291 // Gets the element that encloses the range entirely.
1292 var parent = range.parentElement();
1293
1294 // Empty parent element, e.g. <i>^</i>
1295 if ( !parent.hasChildNodes() )
1296 return { container: parent, offset: 0 };
1297
1298 var siblings = parent.children,
1299 child, sibling,
1300 testRange = range.duplicate(),
1301 startIndex = 0,
1302 endIndex = siblings.length - 1,
1303 index = -1,
1304 position, distance, container;
1305
1306 // Binary search over all element childs to test the range to see whether
1307 // range is right on the boundary of one element.
1308 while ( startIndex <= endIndex ) {
1309 index = Math.floor( ( startIndex + endIndex ) / 2 );
1310 child = siblings[ index ];
1311 testRange.moveToElementText( child );
1312 position = testRange.compareEndPoints( 'StartToStart', range );
1313
1314 if ( position > 0 )
1315 endIndex = index - 1;
1316 else if ( position < 0 )
1317 startIndex = index + 1;
1318 else
1319 return { container: parent, offset: getNodeIndex( child ) };
1320 }
1321
1322 // All childs are text nodes,
1323 // or to the right hand of test range are all text nodes. (#6992)
1324 if ( index == -1 || index == siblings.length - 1 && position < 0 ) {
1325 // Adapt test range to embrace the entire parent contents.
1326 testRange.moveToElementText( parent );
1327 testRange.setEndPoint( 'StartToStart', range );
1328
1329 // IE report line break as CRLF with range.text but
1330 // only LF with textnode.nodeValue, normalize them to avoid
1331 // breaking character counting logic below. (#3949)
1332 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1333
1334 siblings = parent.childNodes;
1335
1336 // Actual range anchor right beside test range at the boundary of text node.
1337 if ( !distance ) {
1338 child = siblings[ siblings.length - 1 ];
1339
1340 if ( child.nodeType != CKEDITOR.NODE_TEXT )
1341 return { container: parent, offset: siblings.length };
1342 else
1343 return { container: child, offset: child.nodeValue.length };
1344 }
1345
1346 // Start the measuring until distance overflows, meanwhile count the text nodes.
1347 var i = siblings.length;
1348 while ( distance > 0 && i > 0 ) {
1349 sibling = siblings[ --i ];
1350 if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) {
1351 container = sibling;
1352 distance -= sibling.nodeValue.length;
1353 }
1354 }
1355
1356 return { container: container, offset: -distance };
1357 }
1358 // Test range was one offset beyond OR behind the anchored text node.
1359 else {
1360 // Adapt one side of test range to the actual range
1361 // for measuring the offset between them.
1362 testRange.collapse( position > 0 ? true : false );
1363 testRange.setEndPoint( position > 0 ? 'StartToStart' : 'EndToStart', range );
1364
1365 // IE report line break as CRLF with range.text but
1366 // only LF with textnode.nodeValue, normalize them to avoid
1367 // breaking character counting logic below. (#3949)
1368 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1369
1370 // Actual range anchor right beside test range at the inner boundary of text node.
1371 if ( !distance )
1372 return { container: parent, offset: getNodeIndex( child ) + ( position > 0 ? 0 : 1 ) };
1373
1374 // Start the measuring until distance overflows, meanwhile count the text nodes.
1375 while ( distance > 0 ) {
1376 try {
1377 sibling = child[ position > 0 ? 'previousSibling' : 'nextSibling' ];
1378 if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) {
1379 distance -= sibling.nodeValue.length;
1380 container = sibling;
1381 }
1382 child = sibling;
1383 }
1384 // Measurement in IE could be somtimes wrong because of <select> element. (#4611)
1385 catch ( e ) {
1386 return { container: parent, offset: getNodeIndex( child ) };
1387 }
1388 }
1389
1390 return { container: container, offset: position > 0 ? -distance : container.nodeValue.length + distance };
1391 }
1392 };
1393
1394 return function() {
1395 // IE doesn't have range support (in the W3C way), so we
1396 // need to do some magic to transform selections into
1397 // CKEDITOR.dom.range instances.
1398
1399 var sel = this.getNative(),
1400 nativeRange = sel && sel.createRange(),
1401 type = this.getType(),
1402 range;
1403
1404 if ( !sel )
1405 return [];
1406
1407 if ( type == CKEDITOR.SELECTION_TEXT ) {
1408 range = new CKEDITOR.dom.range( this.root );
1409
1410 var boundaryInfo = getBoundaryInformation( nativeRange, true );
1411 range.setStart( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
1412
1413 boundaryInfo = getBoundaryInformation( nativeRange );
1414 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
1415
1416 // Correct an invalid IE range case on empty list item. (#5850)
1417 if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING && range.endOffset <= range.startContainer.getIndex() )
1418 range.collapse();
1419
1420 return [ range ];
1421 } else if ( type == CKEDITOR.SELECTION_ELEMENT ) {
1422 var retval = [];
1423
1424 for ( var i = 0; i < nativeRange.length; i++ ) {
1425 var element = nativeRange.item( i ),
1426 parentElement = element.parentNode,
1427 j = 0;
1428
1429 range = new CKEDITOR.dom.range( this.root );
1430
1431 for ( ; j < parentElement.childNodes.length && parentElement.childNodes[ j ] != element; j++ ) {
1432
1433 }
1434
1435 range.setStart( new CKEDITOR.dom.node( parentElement ), j );
1436 range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 );
1437 retval.push( range );
1438 }
1439
1440 return retval;
1441 }
1442
1443 return [];
1444 };
1445 } )() :
1446 function() {
1447 // On browsers implementing the W3C range, we simply
1448 // tranform the native ranges in CKEDITOR.dom.range
1449 // instances.
1450
1451 var ranges = [],
1452 range,
1453 sel = this.getNative();
1454
1455 if ( !sel )
1456 return ranges;
1457
1458 for ( var i = 0; i < sel.rangeCount; i++ ) {
1459 var nativeRange = sel.getRangeAt( i );
1460
1461 range = new CKEDITOR.dom.range( this.root );
1462
1463 range.setStart( new CKEDITOR.dom.node( nativeRange.startContainer ), nativeRange.startOffset );
1464 range.setEnd( new CKEDITOR.dom.node( nativeRange.endContainer ), nativeRange.endOffset );
1465 ranges.push( range );
1466 }
1467 return ranges;
1468 };
1469
1470 return function( onlyEditables ) {
1471 var cache = this._.cache,
1472 ranges = cache.ranges;
1473
1474 if ( !ranges )
1475 cache.ranges = ranges = new CKEDITOR.dom.rangeList( func.call( this ) );
1476
1477 if ( !onlyEditables )
1478 return ranges;
1479
1480 // Split range into multiple by read-only nodes.
1481 // Clone ranges array to avoid changing cached ranges (#11493).
1482 return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) );
1483 };
1484 } )(),
1485
1486 /**
1487 * Gets the DOM element in which the selection starts.
1488 *
1489 * var element = editor.getSelection().getStartElement();
1490 * alert( element.getName() );
1491 *
1492 * @returns {CKEDITOR.dom.element} The element at the beginning of the selection.
1493 */
1494 getStartElement: function() {
1495 var cache = this._.cache;
1496 if ( cache.startElement !== undefined )
1497 return cache.startElement;
1498
1499 var node;
1500
1501 switch ( this.getType() ) {
1502 case CKEDITOR.SELECTION_ELEMENT:
1503 return this.getSelectedElement();
1504
1505 case CKEDITOR.SELECTION_TEXT:
1506
1507 var range = this.getRanges()[ 0 ];
1508
1509 if ( range ) {
1510 if ( !range.collapsed ) {
1511 range.optimize();
1512
1513 // Decrease the range content to exclude particial
1514 // selected node on the start which doesn't have
1515 // visual impact. ( #3231 )
1516 while ( 1 ) {
1517 var startContainer = range.startContainer,
1518 startOffset = range.startOffset;
1519 // Limit the fix only to non-block elements.(#3950)
1520 if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() )
1521 range.setStartAfter( startContainer );
1522 else
1523 break;
1524 }
1525
1526 node = range.startContainer;
1527
1528 if ( node.type != CKEDITOR.NODE_ELEMENT )
1529 return node.getParent();
1530
1531 node = node.getChild( range.startOffset );
1532
1533 if ( !node || node.type != CKEDITOR.NODE_ELEMENT )
1534 node = range.startContainer;
1535 else {
1536 var child = node.getFirst();
1537 while ( child && child.type == CKEDITOR.NODE_ELEMENT ) {
1538 node = child;
1539 child = child.getFirst();
1540 }
1541 }
1542 } else {
1543 node = range.startContainer;
1544 if ( node.type != CKEDITOR.NODE_ELEMENT )
1545 node = node.getParent();
1546 }
1547
1548 node = node.$;
1549 }
1550 }
1551
1552 return cache.startElement = ( node ? new CKEDITOR.dom.element( node ) : null );
1553 },
1554
1555 /**
1556 * Gets the currently selected element.
1557 *
1558 * var element = editor.getSelection().getSelectedElement();
1559 * alert( element.getName() );
1560 *
1561 * @returns {CKEDITOR.dom.element} The selected element. Null if no
1562 * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}.
1563 */
1564 getSelectedElement: function() {
1565 var cache = this._.cache;
1566 if ( cache.selectedElement !== undefined )
1567 return cache.selectedElement;
1568
1569 var self = this;
1570
1571 var node = CKEDITOR.tools.tryThese(
1572 // Is it native IE control type selection?
1573 function() {
1574 return self.getNative().createRange().item( 0 );
1575 },
1576 // Figure it out by checking if there's a single enclosed
1577 // node of the range.
1578 function() {
1579 var range = self.getRanges()[ 0 ].clone(),
1580 enclosed, selected;
1581
1582 // Check first any enclosed element, e.g. <ul>[<li><a href="#">item</a></li>]</ul>
1583 for ( var i = 2; i && !( ( enclosed = range.getEnclosedNode() ) && ( enclosed.type == CKEDITOR.NODE_ELEMENT ) && styleObjectElements[ enclosed.getName() ] && ( selected = enclosed ) ); i-- ) {
1584 // Then check any deep wrapped element, e.g. [<b><i><img /></i></b>]
1585 range.shrink( CKEDITOR.SHRINK_ELEMENT );
1586 }
1587
1588 return selected && selected.$;
1589 }
1590 );
1591
1592 return cache.selectedElement = ( node ? new CKEDITOR.dom.element( node ) : null );
1593 },
1594
1595 /**
1596 * Retrieves the text contained within the range. An empty string is returned for non-text selection.
1597 *
1598 * var text = editor.getSelection().getSelectedText();
1599 * alert( text );
1600 *
1601 * @since 3.6.1
1602 * @returns {String} A string of text within the current selection.
1603 */
1604 getSelectedText: function() {
1605 var cache = this._.cache;
1606 if ( cache.selectedText !== undefined )
1607 return cache.selectedText;
1608
1609 var nativeSel = this.getNative(),
1610 text = isMSSelection ? nativeSel.type == 'Control' ? '' : nativeSel.createRange().text : nativeSel.toString();
1611
1612 return ( cache.selectedText = text );
1613 },
1614
1615 /**
1616 * Locks the selection made in the editor in order to make it possible to
1617 * manipulate it without browser interference. A locked selection is
1618 * cached and remains unchanged until it is released with the {@link #unlock} method.
1619 *
1620 * editor.getSelection().lock();
1621 */
1622 lock: function() {
1623 // Call all cacheable function.
1624 this.getRanges();
1625 this.getStartElement();
1626 this.getSelectedElement();
1627 this.getSelectedText();
1628
1629 // The native selection is not available when locked.
1630 this._.cache.nativeSel = null;
1631
1632 this.isLocked = 1;
1633 },
1634
1635 /**
1636 * @todo
1637 */
1638 unlock: function( restore ) {
1639 if ( !this.isLocked )
1640 return;
1641
1642 if ( restore ) {
1643 var selectedElement = this.getSelectedElement(),
1644 ranges = !selectedElement && this.getRanges(),
1645 faked = this.isFake;
1646 }
1647
1648 this.isLocked = 0;
1649 this.reset();
1650
1651 if ( restore ) {
1652 // Saved selection may be outdated (e.g. anchored in offline nodes).
1653 // Avoid getting broken by such.
1654 var common = selectedElement || ranges[ 0 ] && ranges[ 0 ].getCommonAncestor();
1655 if ( !( common && common.getAscendant( 'body', 1 ) ) )
1656 return;
1657
1658 if ( faked )
1659 this.fake( selectedElement );
1660 else if ( selectedElement )
1661 this.selectElement( selectedElement );
1662 else
1663 this.selectRanges( ranges );
1664 }
1665 },
1666
1667 /**
1668 * Clears the selection cache.
1669 *
1670 * editor.getSelection().reset();
1671 */
1672 reset: function() {
1673 this._.cache = {};
1674 this.isFake = 0;
1675
1676 var editor = this.root.editor;
1677
1678 // Invalidate any fake selection available in the editor.
1679 if ( editor && editor._.fakeSelection ) {
1680 // Test whether this selection is the one that was
1681 // faked or its clone.
1682 if ( this.rev == editor._.fakeSelection.rev ) {
1683 delete editor._.fakeSelection;
1684
1685 removeHiddenSelectionContainer( editor );
1686 }
1687 else {
1688 CKEDITOR.warn( 'selection-fake-reset' );
1689 }
1690 }
1691
1692 this.rev = nextRev++;
1693 },
1694
1695 /**
1696 * Makes the current selection of type {@link CKEDITOR#SELECTION_ELEMENT} by enclosing the specified element.
1697 *
1698 * var element = editor.document.getById( 'sampleElement' );
1699 * editor.getSelection().selectElement( element );
1700 *
1701 * @param {CKEDITOR.dom.element} element The element to enclose in the selection.
1702 */
1703 selectElement: function( element ) {
1704 var range = new CKEDITOR.dom.range( this.root );
1705 range.setStartBefore( element );
1706 range.setEndAfter( element );
1707 this.selectRanges( [ range ] );
1708 },
1709
1710 /**
1711 * Clears the original selection and adds the specified ranges to the document selection.
1712 *
1713 * // Move selection to the end of the editable element.
1714 * var range = editor.createRange();
1715 * range.moveToPosition( range.root, CKEDITOR.POSITION_BEFORE_END );
1716 * editor.getSelection().selectRanges( [ ranges ] );
1717 *
1718 * @param {Array} ranges An array of {@link CKEDITOR.dom.range} instances
1719 * representing ranges to be added to the document.
1720 */
1721 selectRanges: function( ranges ) {
1722 var editor = this.root.editor,
1723 hadHiddenSelectionContainer = editor && editor._.hiddenSelectionContainer;
1724
1725 this.reset();
1726
1727 // Check if there's a hiddenSelectionContainer in editable at some index.
1728 // Some ranges may be anchored after the hiddenSelectionContainer and,
1729 // once the container is removed while resetting the selection, they
1730 // may need new endOffset (one element less within the range) (#11021 #11393).
1731 if ( hadHiddenSelectionContainer )
1732 fixRangesAfterHiddenSelectionContainer( ranges, this.root );
1733
1734 if ( !ranges.length )
1735 return;
1736
1737 // Refresh the locked selection.
1738 if ( this.isLocked ) {
1739 // making a new DOM selection will force the focus on editable in certain situation,
1740 // we have to save the currently focused element for later recovery.
1741 var focused = CKEDITOR.document.getActive();
1742 this.unlock();
1743 this.selectRanges( ranges );
1744 this.lock();
1745 // Return to the previously focused element.
1746 focused && !focused.equals( this.root ) && focused.focus();
1747 return;
1748 }
1749
1750 // Handle special case - automatic fake selection on non-editable elements.
1751 var receiver = getNonEditableFakeSelectionReceiver( ranges );
1752
1753 if ( receiver ) {
1754 this.fake( receiver );
1755 return;
1756 }
1757
1758 if ( isMSSelection ) {
1759 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
1760 fillerTextRegex = /\ufeff|\u00a0/,
1761 nonCells = { table: 1, tbody: 1, tr: 1 };
1762
1763 if ( ranges.length > 1 ) {
1764 // IE doesn't accept multiple ranges selection, so we join all into one.
1765 var last = ranges[ ranges.length - 1 ];
1766 ranges[ 0 ].setEnd( last.endContainer, last.endOffset );
1767 }
1768
1769 var range = ranges[ 0 ];
1770 var collapsed = range.collapsed,
1771 isStartMarkerAlone, dummySpan, ieRange;
1772
1773 // Try to make a object selection, be careful with selecting phase element in IE
1774 // will breaks the selection in non-framed environment.
1775 var selected = range.getEnclosedNode();
1776 if ( selected && selected.type == CKEDITOR.NODE_ELEMENT && selected.getName() in styleObjectElements &&
1777 !( selected.is( 'a' ) && selected.getText() ) ) {
1778 try {
1779 ieRange = selected.$.createControlRange();
1780 ieRange.addElement( selected.$ );
1781 ieRange.select();
1782 return;
1783 } catch ( er ) {}
1784 }
1785
1786 // IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g.
1787 // <table><tbody><tr>[<td>cell</b></td>... => <table><tbody><tr><td>[cell</td>...
1788 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells ||
1789 range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) {
1790 range.shrink( CKEDITOR.NODE_ELEMENT, true );
1791 // The range might get collapsed (#7975). Update cached variable.
1792 collapsed = range.collapsed;
1793 }
1794
1795 var bookmark = range.createBookmark();
1796
1797 // Create marker tags for the start and end boundaries.
1798 var startNode = bookmark.startNode;
1799
1800 var endNode;
1801 if ( !collapsed )
1802 endNode = bookmark.endNode;
1803
1804 // Create the main range which will be used for the selection.
1805 ieRange = range.document.$.body.createTextRange();
1806
1807 // Position the range at the start boundary.
1808 ieRange.moveToElementText( startNode.$ );
1809 ieRange.moveStart( 'character', 1 );
1810
1811 if ( endNode ) {
1812 // Create a tool range for the end.
1813 var ieRangeEnd = range.document.$.body.createTextRange();
1814
1815 // Position the tool range at the end.
1816 ieRangeEnd.moveToElementText( endNode.$ );
1817
1818 // Move the end boundary of the main range to match the tool range.
1819 ieRange.setEndPoint( 'EndToEnd', ieRangeEnd );
1820 ieRange.moveEnd( 'character', -1 );
1821 } else {
1822 // The isStartMarkerAlone logic comes from V2. It guarantees that the lines
1823 // will expand and that the cursor will be blinking on the right place.
1824 // Actually, we are using this flag just to avoid using this hack in all
1825 // situations, but just on those needed.
1826 var next = startNode.getNext( notWhitespaces );
1827 var inPre = startNode.hasAscendant( 'pre' );
1828 isStartMarkerAlone = ( !( next && next.getText && next.getText().match( fillerTextRegex ) ) && // already a filler there?
1829 ( inPre || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) );
1830
1831 // Append a temporary <span>&#65279;</span> before the selection.
1832 // This is needed to avoid IE destroying selections inside empty
1833 // inline elements, like <b></b> (#253).
1834 // It is also needed when placing the selection right after an inline
1835 // element to avoid the selection moving inside of it.
1836 dummySpan = range.document.createElement( 'span' );
1837 dummySpan.setHtml( '&#65279;' ); // Zero Width No-Break Space (U+FEFF). See #1359.
1838 dummySpan.insertBefore( startNode );
1839
1840 if ( isStartMarkerAlone ) {
1841 // To expand empty blocks or line spaces after <br>, we need
1842 // instead to have any char, which will be later deleted using the
1843 // selection.
1844 // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359)
1845 range.document.createText( '\ufeff' ).insertBefore( startNode );
1846 }
1847 }
1848
1849 // Remove the markers (reset the position, because of the changes in the DOM tree).
1850 range.setStartBefore( startNode );
1851 startNode.remove();
1852
1853 if ( collapsed ) {
1854 if ( isStartMarkerAlone ) {
1855 // Move the selection start to include the temporary \ufeff.
1856 ieRange.moveStart( 'character', -1 );
1857
1858 ieRange.select();
1859
1860 // Remove our temporary stuff.
1861 range.document.$.selection.clear();
1862 } else {
1863 ieRange.select();
1864 }
1865
1866 range.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START );
1867 dummySpan.remove();
1868 } else {
1869 range.setEndBefore( endNode );
1870 endNode.remove();
1871 ieRange.select();
1872 }
1873 } else {
1874 var sel = this.getNative();
1875
1876 // getNative() returns null if iframe is "display:none" in FF. (#6577)
1877 if ( !sel )
1878 return;
1879
1880 this.removeAllRanges();
1881
1882 for ( var i = 0; i < ranges.length; i++ ) {
1883 // Joining sequential ranges introduced by
1884 // readonly elements protection.
1885 if ( i < ranges.length - 1 ) {
1886 var left = ranges[ i ],
1887 right = ranges[ i + 1 ],
1888 between = left.clone();
1889 between.setStart( left.endContainer, left.endOffset );
1890 between.setEnd( right.startContainer, right.startOffset );
1891
1892 // Don't confused by Firefox adjancent multi-ranges
1893 // introduced by table cells selection.
1894 if ( !between.collapsed ) {
1895 between.shrink( CKEDITOR.NODE_ELEMENT, true );
1896 var ancestor = between.getCommonAncestor(),
1897 enclosed = between.getEnclosedNode();
1898
1899 // The following cases has to be considered:
1900 // 1. <span contenteditable="false">[placeholder]</span>
1901 // 2. <input contenteditable="false" type="radio"/> (#6621)
1902 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) {
1903 right.setStart( left.startContainer, left.startOffset );
1904 ranges.splice( i--, 1 );
1905 continue;
1906 }
1907 }
1908 }
1909
1910 range = ranges[ i ];
1911
1912 var nativeRange = this.document.$.createRange();
1913
1914 if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) {
1915 // Append a zero-width space so WebKit will not try to
1916 // move the selection by itself (#1272).
1917 var fillingChar = createFillingCharSequenceNode( this.root );
1918 range.insertNode( fillingChar );
1919
1920 next = fillingChar.getNext();
1921
1922 // If the filling char is followed by a <br>, whithout
1923 // having something before it, it'll not blink.
1924 // Let's remove it in this case.
1925 if ( next && !fillingChar.getPrevious() && next.type == CKEDITOR.NODE_ELEMENT && next.getName() == 'br' ) {
1926 removeFillingCharSequenceNode( this.root );
1927 range.moveToPosition( next, CKEDITOR.POSITION_BEFORE_START );
1928 } else {
1929 range.moveToPosition( fillingChar, CKEDITOR.POSITION_AFTER_END );
1930 }
1931 }
1932
1933 nativeRange.setStart( range.startContainer.$, range.startOffset );
1934
1935 try {
1936 nativeRange.setEnd( range.endContainer.$, range.endOffset );
1937 } catch ( e ) {
1938 // There is a bug in Firefox implementation (it would be too easy
1939 // otherwise). The new start can't be after the end (W3C says it can).
1940 // So, let's create a new range and collapse it to the desired point.
1941 if ( e.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 ) {
1942 range.collapse( 1 );
1943 nativeRange.setEnd( range.endContainer.$, range.endOffset );
1944 } else {
1945 throw e;
1946 }
1947 }
1948
1949 // Select the range.
1950 sel.addRange( nativeRange );
1951 }
1952 }
1953
1954 this.reset();
1955
1956 // Fakes the IE DOM event "selectionchange" on editable.
1957 this.root.fire( 'selectionchange' );
1958 },
1959
1960 /**
1961 * Makes a "fake selection" of an element.
1962 *
1963 * A fake selection does not render UI artifacts over the selected
1964 * element. Additionally, the browser native selection system is not
1965 * aware of the fake selection. In practice, the native selection is
1966 * moved to a hidden place where no native selection UI artifacts are
1967 * displayed to the user.
1968 *
1969 * @param {CKEDITOR.dom.element} element The element to be "selected".
1970 * @param {String} [ariaLabel] A string to be used by the screen reader to describe the selection.
1971 */
1972 fake: function( element, ariaLabel ) {
1973 var editor = this.root.editor;
1974
1975 // Attempt to retreive aria-label if possible (#14539).
1976 if ( ariaLabel === undefined && element.hasAttribute( 'aria-label' ) ) {
1977 ariaLabel = element.getAttribute( 'aria-label' );
1978 }
1979
1980 // Cleanup after previous selection - e.g. remove hidden sel container.
1981 this.reset();
1982
1983 hideSelection( editor, ariaLabel );
1984
1985 // Set this value after executing hiseSelection, because it may
1986 // cause reset() which overwrites cache.
1987 var cache = this._.cache;
1988
1989 // Caches a range than holds the element.
1990 var range = new CKEDITOR.dom.range( this.root );
1991 range.setStartBefore( element );
1992 range.setEndAfter( element );
1993 cache.ranges = new CKEDITOR.dom.rangeList( range );
1994
1995 // Put this element in the cache.
1996 cache.selectedElement = cache.startElement = element;
1997 cache.type = CKEDITOR.SELECTION_ELEMENT;
1998
1999 // Properties that will not be available when isFake.
2000 cache.selectedText = cache.nativeSel = null;
2001
2002 this.isFake = 1;
2003 this.rev = nextRev++;
2004
2005 // Save this selection, so it can be returned by editor.getSelection().
2006 editor._.fakeSelection = this;
2007
2008 // Fire selectionchange, just like a normal selection.
2009 this.root.fire( 'selectionchange' );
2010 },
2011
2012 /**
2013 * Checks whether selection is placed in hidden element.
2014 *
2015 * This method is to be used to verify whether fake selection
2016 * (see {@link #fake}) is still hidden.
2017 *
2018 * **Note:** this method should be executed on real selection - e.g.:
2019 *
2020 * editor.getSelection( true ).isHidden();
2021 *
2022 * @returns {Boolean}
2023 */
2024 isHidden: function() {
2025 var el = this.getCommonAncestor();
2026
2027 if ( el && el.type == CKEDITOR.NODE_TEXT )
2028 el = el.getParent();
2029
2030 return !!( el && el.data( 'cke-hidden-sel' ) );
2031 },
2032
2033 /**
2034 * Creates a bookmark for each range of this selection (from {@link #getRanges})
2035 * by calling the {@link CKEDITOR.dom.range#createBookmark} method,
2036 * with extra care taken to avoid interference among those ranges. The arguments
2037 * received are the same as with the underlying range method.
2038 *
2039 * var bookmarks = editor.getSelection().createBookmarks();
2040 *
2041 * @returns {Array} Array of bookmarks for each range.
2042 */
2043 createBookmarks: function( serializable ) {
2044 var bookmark = this.getRanges().createBookmarks( serializable );
2045 this.isFake && ( bookmark.isFake = 1 );
2046 return bookmark;
2047 },
2048
2049 /**
2050 * Creates a bookmark for each range of this selection (from {@link #getRanges})
2051 * by calling the {@link CKEDITOR.dom.range#createBookmark2} method,
2052 * with extra care taken to avoid interference among those ranges. The arguments
2053 * received are the same as with the underlying range method.
2054 *
2055 * var bookmarks = editor.getSelection().createBookmarks2();
2056 *
2057 * @returns {Array} Array of bookmarks for each range.
2058 */
2059 createBookmarks2: function( normalized ) {
2060 var bookmark = this.getRanges().createBookmarks2( normalized );
2061 this.isFake && ( bookmark.isFake = 1 );
2062 return bookmark;
2063 },
2064
2065 /**
2066 * Selects the virtual ranges denoted by the bookmarks by calling {@link #selectRanges}.
2067 *
2068 * var bookmarks = editor.getSelection().createBookmarks();
2069 * editor.getSelection().selectBookmarks( bookmarks );
2070 *
2071 * @param {Array} bookmarks The bookmarks representing ranges to be selected.
2072 * @returns {CKEDITOR.dom.selection} This selection object, after the ranges were selected.
2073 */
2074 selectBookmarks: function( bookmarks ) {
2075 var ranges = [],
2076 node;
2077
2078 for ( var i = 0; i < bookmarks.length; i++ ) {
2079 var range = new CKEDITOR.dom.range( this.root );
2080 range.moveToBookmark( bookmarks[ i ] );
2081 ranges.push( range );
2082 }
2083
2084 // It may happen that the content change during loading, before selection is set so bookmark leads to text node.
2085 if ( bookmarks.isFake ) {
2086 node = ranges[ 0 ].getEnclosedNode();
2087 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) {
2088 CKEDITOR.warn( 'selection-not-fake' );
2089 bookmarks.isFake = 0;
2090 }
2091 }
2092
2093 if ( bookmarks.isFake )
2094 this.fake( node );
2095 else
2096 this.selectRanges( ranges );
2097
2098 return this;
2099 },
2100
2101 /**
2102 * Retrieves the common ancestor node of the first range and the last range.
2103 *
2104 * var ancestor = editor.getSelection().getCommonAncestor();
2105 *
2106 * @returns {CKEDITOR.dom.element} The common ancestor of the selection or `null` if selection is empty.
2107 */
2108 getCommonAncestor: function() {
2109 var ranges = this.getRanges();
2110 if ( !ranges.length )
2111 return null;
2112
2113 var startNode = ranges[ 0 ].startContainer,
2114 endNode = ranges[ ranges.length - 1 ].endContainer;
2115 return startNode.getCommonAncestor( endNode );
2116 },
2117
2118 /**
2119 * Moves the scrollbar to the starting position of the current selection.
2120 *
2121 * editor.getSelection().scrollIntoView();
2122 */
2123 scrollIntoView: function() {
2124 // Scrolls the first range into view.
2125 if ( this.type != CKEDITOR.SELECTION_NONE )
2126 this.getRanges()[ 0 ].scrollIntoView();
2127 },
2128
2129 /**
2130 * Remove all the selection ranges from the document.
2131 */
2132 removeAllRanges: function() {
2133 // Don't clear selection outside this selection's root (#11500).
2134 if ( this.getType() == CKEDITOR.SELECTION_NONE )
2135 return;
2136
2137 var nativ = this.getNative();
2138
2139 try {
2140 nativ && nativ[ isMSSelection ? 'empty' : 'removeAllRanges' ]();
2141 } catch ( er ) {}
2142
2143 this.reset();
2144 }
2145 };
2146
2147} )();
2148
2149
2150/**
2151 * Fired when selection inside editor has been changed. Note that this event
2152 * is fired only when selection's start element (container of a selecion start)
2153 * changes, not on every possible selection change. Thanks to that `selectionChange`
2154 * is fired less frequently, but on every context
2155 * (the {@link CKEDITOR.editor#elementPath elements path} holding selection's start) change.
2156 *
2157 * @event selectionChange
2158 * @member CKEDITOR.editor
2159 * @param {CKEDITOR.editor} editor This editor instance.
2160 * @param data
2161 * @param {CKEDITOR.dom.selection} data.selection
2162 * @param {CKEDITOR.dom.elementPath} data.path
2163 */
2164
2165/**
2166 * Selection's revision. This value is incremented every time new
2167 * selection is created or existing one is modified.
2168 *
2169 * @since 4.3
2170 * @readonly
2171 * @property {Number} rev
2172 */
2173
2174/**
2175 * Document in which selection is anchored.
2176 *
2177 * @readonly
2178 * @property {CKEDITOR.dom.document} document
2179 */
2180
2181/**
2182 * Selection's root element.
2183 *
2184 * @readonly
2185 * @property {CKEDITOR.dom.element} root
2186 */
2187
2188/**
2189 * Whether selection is locked (cannot be modified).
2190 *
2191 * See {@link #lock} and {@link #unlock} methods.
2192 *
2193 * @readonly
2194 * @property {Boolean} isLocked
2195 */
2196
2197/**
2198 * Whether selection is a fake selection.
2199 *
2200 * See {@link #fake} method.
2201 *
2202 * @readonly
2203 * @property {Boolean} isFake
2204 */
diff --git a/sources/core/skin.js b/sources/core/skin.js
new file mode 100644
index 0000000..4f0ee7c
--- /dev/null
+++ b/sources/core/skin.js
@@ -0,0 +1,350 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.skin} class that is used to manage skin parts.
8 */
9
10( function() {
11 var cssLoaded = {};
12
13 function getName() {
14 return CKEDITOR.skinName.split( ',' )[ 0 ];
15 }
16
17 function getConfigPath() {
18 return CKEDITOR.getUrl( CKEDITOR.skinName.split( ',' )[ 1 ] || ( 'skins/' + getName() + '/' ) );
19 }
20
21 /**
22 * Manages the loading of skin parts among all editor instances.
23 *
24 * @class
25 * @singleton
26 */
27 CKEDITOR.skin = {
28 /**
29 * Returns the root path to the skin directory.
30 *
31 * @method
32 * @todo
33 */
34 path: getConfigPath,
35
36 /**
37 * Loads a skin part into the page. Does nothing if the part has already been loaded.
38 *
39 * **Note:** The "editor" part is always auto loaded upon instance creation,
40 * thus this function is mainly used to **lazy load** other parts of the skin
41 * that do not have to be displayed until requested.
42 *
43 * // Load the dialog part.
44 * editor.skin.loadPart( 'dialog' );
45 *
46 * @param {String} part The name of the skin part CSS file that resides in the skin directory.
47 * @param {Function} fn The provided callback function which is invoked after the part is loaded.
48 */
49 loadPart: function( part, fn ) {
50 if ( CKEDITOR.skin.name != getName() ) {
51 CKEDITOR.scriptLoader.load( CKEDITOR.getUrl( getConfigPath() + 'skin.js' ), function() {
52 loadCss( part, fn );
53 } );
54 } else {
55 loadCss( part, fn );
56 }
57 },
58
59 /**
60 * Retrieves the real URL of a (CSS) skin part.
61 *
62 * @param {String} part
63 */
64 getPath: function( part ) {
65 return CKEDITOR.getUrl( getCssPath( part ) );
66 },
67
68 /**
69 * The list of registered icons. To add new icons to this list, use {@link #addIcon}.
70 */
71 icons: {},
72
73 /**
74 * Registers an icon.
75 *
76 * @param {String} name The icon name.
77 * @param {String} path The path to the icon image file.
78 * @param {Number} [offset] The vertical offset position of the icon, if
79 * available inside a strip image.
80 * @param {String} [bgsize] The value of the CSS "background-size" property to
81 * use for this icon
82 */
83 addIcon: function( name, path, offset, bgsize ) {
84 name = name.toLowerCase();
85 if ( !this.icons[ name ] ) {
86 this.icons[ name ] = {
87 path: path,
88 offset: offset || 0,
89 bgsize: bgsize || '16px'
90 };
91 }
92 },
93
94 /**
95 * Gets the CSS background styles to be used to render a specific icon.
96 *
97 * @param {String} name The icon name, as registered with {@link #addIcon}.
98 * @param {Boolean} [rtl] Indicates that the RTL version of the icon is
99 * to be used, if available.
100 * @param {String} [overridePath] The path to the icon image file. It
101 * overrides the path defined by the named icon, if available, and is
102 * used if the named icon was not registered.
103 * @param {Number} [overrideOffset] The vertical offset position of the
104 * icon. It overrides the offset defined by the named icon, if
105 * available, and is used if the named icon was not registered.
106 * @param {String} [overrideBgsize] The value of the CSS "background-size" property
107 * to use for the icon. It overrides the value defined by the named icon,
108 * if available, and is used if the named icon was not registered.
109 */
110 getIconStyle: function( name, rtl, overridePath, overrideOffset, overrideBgsize ) {
111 var icon, path, offset, bgsize;
112
113 if ( name ) {
114 name = name.toLowerCase();
115 // If we're in RTL, try to get the RTL version of the icon.
116 if ( rtl )
117 icon = this.icons[ name + '-rtl' ];
118
119 // If not in LTR or no RTL version available, get the generic one.
120 if ( !icon )
121 icon = this.icons[ name ];
122 }
123
124 path = overridePath || ( icon && icon.path ) || '';
125 offset = overrideOffset || ( icon && icon.offset );
126 bgsize = overrideBgsize || ( icon && icon.bgsize ) || '16px';
127
128 // If we use apostrophes in background-image, we must escape apostrophes in path (just to be sure). (#13361)
129 if ( path )
130 path = path.replace( /'/g, '\\\'' );
131
132 return path &&
133 ( 'background-image:url(\'' + CKEDITOR.getUrl( path ) + '\');background-position:0 ' + offset + 'px;background-size:' + bgsize + ';' );
134 }
135 };
136
137 function getCssPath( part ) {
138 // Check for ua-specific version of skin part.
139 var uas = CKEDITOR.skin[ 'ua_' + part ], env = CKEDITOR.env;
140 if ( uas ) {
141
142 // Having versioned UA checked first.
143 uas = uas.split( ',' ).sort( function( a, b ) {
144 return a > b ? -1 : 1;
145 } );
146
147 // Loop through all ua entries, checking is any of them match the current ua.
148 for ( var i = 0, ua; i < uas.length; i++ ) {
149 ua = uas[ i ];
150
151 if ( env.ie ) {
152 if ( ( ua.replace( /^ie/, '' ) == env.version ) || ( env.quirks && ua == 'iequirks' ) )
153 ua = 'ie';
154 }
155
156 if ( env[ ua ] ) {
157 part += '_' + uas[ i ];
158 break;
159 }
160 }
161 }
162 return CKEDITOR.getUrl( getConfigPath() + part + '.css' );
163 }
164
165 function loadCss( part, callback ) {
166 // Avoid reload.
167 if ( !cssLoaded[ part ] ) {
168 CKEDITOR.document.appendStyleSheet( getCssPath( part ) );
169 cssLoaded[ part ] = 1;
170 }
171
172 // CSS loading should not be blocking.
173 callback && callback();
174 }
175
176 CKEDITOR.tools.extend( CKEDITOR.editor.prototype, {
177 /** Gets the color of the editor user interface.
178 *
179 * CKEDITOR.instances.editor1.getUiColor();
180 *
181 * @method
182 * @member CKEDITOR.editor
183 * @returns {String} uiColor The editor UI color or `undefined` if the UI color is not set.
184 */
185 getUiColor: function() {
186 return this.uiColor;
187 },
188
189 /** Sets the color of the editor user interface. This method accepts a color value in
190 * hexadecimal notation, with a `#` character (e.g. #ffffff).
191 *
192 * CKEDITOR.instances.editor1.setUiColor( '#ff00ff' );
193 *
194 * @method
195 * @member CKEDITOR.editor
196 * @param {String} color The desired editor UI color in hexadecimal notation.
197 */
198 setUiColor: function( color ) {
199 var uiStyle = getStylesheet( CKEDITOR.document );
200
201 return ( this.setUiColor = function( color ) {
202 this.uiColor = color;
203
204 var chameleon = CKEDITOR.skin.chameleon,
205 editorStyleContent = '',
206 panelStyleContent = '';
207
208 if ( typeof chameleon == 'function' ) {
209 editorStyleContent = chameleon( this, 'editor' );
210 panelStyleContent = chameleon( this, 'panel' );
211 }
212
213 var replace = [ [ uiColorRegexp, color ] ];
214
215 // Update general style.
216 updateStylesheets( [ uiStyle ], editorStyleContent, replace );
217
218 // Update panel styles.
219 updateStylesheets( uiColorMenus, panelStyleContent, replace );
220 } ).call( this, color );
221 }
222 } );
223
224 var uiColorStylesheetId = 'cke_ui_color',
225 uiColorMenus = [],
226 uiColorRegexp = /\$color/g;
227
228 function getStylesheet( document ) {
229 var node = document.getById( uiColorStylesheetId );
230 if ( !node ) {
231 node = document.getHead().append( 'style' );
232 node.setAttribute( 'id', uiColorStylesheetId );
233 node.setAttribute( 'type', 'text/css' );
234 }
235 return node;
236 }
237
238 function updateStylesheets( styleNodes, styleContent, replace ) {
239 var r, i, content;
240
241 // We have to split CSS declarations for webkit.
242 if ( CKEDITOR.env.webkit ) {
243 styleContent = styleContent.split( '}' ).slice( 0, -1 );
244 for ( i = 0; i < styleContent.length; i++ )
245 styleContent[ i ] = styleContent[ i ].split( '{' );
246 }
247
248 for ( var id = 0; id < styleNodes.length; id++ ) {
249 if ( CKEDITOR.env.webkit ) {
250 for ( i = 0; i < styleContent.length; i++ ) {
251 content = styleContent[ i ][ 1 ];
252 for ( r = 0; r < replace.length; r++ )
253 content = content.replace( replace[ r ][ 0 ], replace[ r ][ 1 ] );
254
255 styleNodes[ id ].$.sheet.addRule( styleContent[ i ][ 0 ], content );
256 }
257 } else {
258 content = styleContent;
259 for ( r = 0; r < replace.length; r++ )
260 content = content.replace( replace[ r ][ 0 ], replace[ r ][ 1 ] );
261
262 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 )
263 styleNodes[ id ].$.styleSheet.cssText += content;
264 else
265 styleNodes[ id ].$.innerHTML += content;
266 }
267 }
268 }
269
270 CKEDITOR.on( 'instanceLoaded', function( evt ) {
271 // The chameleon feature is not for IE quirks.
272 if ( CKEDITOR.env.ie && CKEDITOR.env.quirks )
273 return;
274
275 var editor = evt.editor,
276 showCallback = function( event ) {
277 var panel = event.data[ 0 ] || event.data;
278 var iframe = panel.element.getElementsByTag( 'iframe' ).getItem( 0 ).getFrameDocument();
279
280 // Add stylesheet if missing.
281 if ( !iframe.getById( 'cke_ui_color' ) ) {
282 var node = getStylesheet( iframe );
283 uiColorMenus.push( node );
284
285 var color = editor.getUiColor();
286 // Set uiColor for new panel.
287 if ( color )
288 updateStylesheets( [ node ], CKEDITOR.skin.chameleon( editor, 'panel' ), [ [ uiColorRegexp, color ] ] );
289
290 }
291 };
292
293 editor.on( 'panelShow', showCallback );
294 editor.on( 'menuShow', showCallback );
295
296 // Apply UI color if specified in config.
297 if ( editor.config.uiColor )
298 editor.setUiColor( editor.config.uiColor );
299 } );
300} )();
301
302/**
303 * The list of file names matching the browser user agent string from
304 * {@link CKEDITOR.env}. This is used to load the skin part file in addition
305 * to the "main" skin file for a particular browser.
306 *
307 * **Note:** For each of the defined skin parts the corresponding
308 * CSS file with the same name as the user agent must exist inside
309 * the skin directory.
310 *
311 * @property ua
312 * @todo type?
313 */
314
315/**
316 * The name of the skin that is currently used.
317 *
318 * @property {String} name
319 * @todo
320 */
321
322/**
323 * The editor skin name. Note that it is not possible to have editors with
324 * different skin settings in the same page. In such case just one of the
325 * skins will be used for all editors.
326 *
327 * This is a shortcut to {@link CKEDITOR#skinName}.
328 *
329 * It is possible to install skins outside the default `skin` folder in the
330 * editor installation. In that case, the absolute URL path to that folder
331 * should be provided, separated by a comma (`'skin_name,skin_path'`).
332 *
333 * config.skin = 'moono';
334 *
335 * config.skin = 'myskin,/customstuff/myskin/';
336 *
337 * @cfg {String} skin
338 * @member CKEDITOR.config
339 */
340
341/**
342 * A function that supports the chameleon (skin color switch) feature, providing
343 * the skin color style updates to be applied in runtime.
344 *
345 * **Note:** The embedded `$color` variable is to be substituted with a specific UI color.
346 *
347 * @method chameleon
348 * @param {String} editor The editor instance that the color changes apply to.
349 * @param {String} part The name of the skin part where the color changes take place.
350 */
diff --git a/sources/core/style.js b/sources/core/style.js
new file mode 100644
index 0000000..efb9d7f
--- /dev/null
+++ b/sources/core/style.js
@@ -0,0 +1,2102 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6'use strict';
7
8/**
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 // @returns {Boolean}
1798 function compareCssText( source, target ) {
1799 function filter( string, propertyName ) {
1800 // In case of font-families we'll skip quotes. (#10750)
1801 return propertyName.toLowerCase() == 'font-family' ? string.replace( /["']/g, '' ) : string;
1802 }
1803
1804 if ( typeof source == 'string' )
1805 source = CKEDITOR.tools.parseCssText( source );
1806 if ( typeof target == 'string' )
1807 target = CKEDITOR.tools.parseCssText( target, true );
1808
1809 for ( var name in source ) {
1810 if ( !( name in target ) ) {
1811 return false;
1812 }
1813
1814 if ( !( filter( target[ name ], name ) == filter( source[ name ], name ) ||
1815 source[ name ] == 'inherit' ||
1816 target[ name ] == 'inherit' ) ) {
1817 return false;
1818 }
1819 }
1820 return true;
1821 }
1822
1823 function applyStyleOnSelection( selection, remove, editor ) {
1824 var doc = selection.document,
1825 ranges = selection.getRanges(),
1826 func = remove ? this.removeFromRange : this.applyToRange,
1827 range;
1828
1829 var iterator = ranges.createIterator();
1830 while ( ( range = iterator.getNextRange() ) )
1831 func.call( this, range, editor );
1832
1833 selection.selectRanges( ranges );
1834 doc.removeCustomData( 'doc_processing_style' );
1835 }
1836} )();
1837
1838/**
1839 * Generic style command. It applies a specific style when executed.
1840 *
1841 * var boldStyle = new CKEDITOR.style( { element: 'strong' } );
1842 * // Register the "bold" command, which applies the bold style.
1843 * editor.addCommand( 'bold', new CKEDITOR.styleCommand( boldStyle ) );
1844 *
1845 * @class
1846 * @constructor Creates a styleCommand class instance.
1847 * @extends CKEDITOR.commandDefinition
1848 * @param {CKEDITOR.style} style The style to be applied when command is executed.
1849 * @param {Object} [ext] Additional command definition's properties.
1850 */
1851CKEDITOR.styleCommand = function( style, ext ) {
1852 this.style = style;
1853 this.allowedContent = style;
1854 this.requiredContent = style;
1855
1856 CKEDITOR.tools.extend( this, ext, true );
1857};
1858
1859/**
1860 * @param {CKEDITOR.editor} editor
1861 * @todo
1862 */
1863CKEDITOR.styleCommand.prototype.exec = function( editor ) {
1864 editor.focus();
1865
1866 if ( this.state == CKEDITOR.TRISTATE_OFF )
1867 editor.applyStyle( this.style );
1868 else if ( this.state == CKEDITOR.TRISTATE_ON )
1869 editor.removeStyle( this.style );
1870};
1871
1872/**
1873 * Manages styles registration and loading. See also {@link CKEDITOR.config#stylesSet}.
1874 *
1875 * // The set of styles for the <b>Styles</b> drop-down list.
1876 * CKEDITOR.stylesSet.add( 'default', [
1877 * // Block Styles
1878 * { name: 'Blue Title', element: 'h3', styles: { 'color': 'Blue' } },
1879 * { name: 'Red Title', element: 'h3', styles: { 'color': 'Red' } },
1880 *
1881 * // Inline Styles
1882 * { name: 'Marker: Yellow', element: 'span', styles: { 'background-color': 'Yellow' } },
1883 * { name: 'Marker: Green', element: 'span', styles: { 'background-color': 'Lime' } },
1884 *
1885 * // Object Styles
1886 * {
1887 * name: 'Image on Left',
1888 * element: 'img',
1889 * attributes: {
1890 * style: 'padding: 5px; margin-right: 5px',
1891 * border: '2',
1892 * align: 'left'
1893 * }
1894 * }
1895 * ] );
1896 *
1897 * @since 3.2
1898 * @class
1899 * @singleton
1900 * @extends CKEDITOR.resourceManager
1901 */
1902CKEDITOR.stylesSet = new CKEDITOR.resourceManager( '', 'stylesSet' );
1903
1904// Backward compatibility (#5025).
1905CKEDITOR.addStylesSet = CKEDITOR.tools.bind( CKEDITOR.stylesSet.add, CKEDITOR.stylesSet );
1906CKEDITOR.loadStylesSet = function( name, url, callback ) {
1907 CKEDITOR.stylesSet.addExternal( name, url, '' );
1908 CKEDITOR.stylesSet.load( name, callback );
1909};
1910
1911CKEDITOR.tools.extend( CKEDITOR.editor.prototype, {
1912 /**
1913 * Registers a function to be called whenever the selection position changes in the
1914 * editing area. The current state is passed to the function. The possible
1915 * states are {@link CKEDITOR#TRISTATE_ON} and {@link CKEDITOR#TRISTATE_OFF}.
1916 *
1917 * // Create a style object for the <b> element.
1918 * var style = new CKEDITOR.style( { element: 'b' } );
1919 * var editor = CKEDITOR.instances.editor1;
1920 * editor.attachStyleStateChange( style, function( state ) {
1921 * if ( state == CKEDITOR.TRISTATE_ON )
1922 * alert( 'The current state for the B element is ON' );
1923 * else
1924 * alert( 'The current state for the B element is OFF' );
1925 * } );
1926 *
1927 * @member CKEDITOR.editor
1928 * @param {CKEDITOR.style} style The style to be watched.
1929 * @param {Function} callback The function to be called.
1930 */
1931 attachStyleStateChange: function( style, callback ) {
1932 // Try to get the list of attached callbacks.
1933 var styleStateChangeCallbacks = this._.styleStateChangeCallbacks;
1934
1935 // If it doesn't exist, it means this is the first call. So, let's create
1936 // all the structure to manage the style checks and the callback calls.
1937 if ( !styleStateChangeCallbacks ) {
1938 // Create the callbacks array.
1939 styleStateChangeCallbacks = this._.styleStateChangeCallbacks = [];
1940
1941 // Attach to the selectionChange event, so we can check the styles at
1942 // that point.
1943 this.on( 'selectionChange', function( ev ) {
1944 // Loop throw all registered callbacks.
1945 for ( var i = 0; i < styleStateChangeCallbacks.length; i++ ) {
1946 var callback = styleStateChangeCallbacks[ i ];
1947
1948 // Check the current state for the style defined for that callback.
1949 var currentState = callback.style.checkActive( ev.data.path, this ) ?
1950 CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF;
1951
1952 // Call the callback function, passing the current state to it.
1953 callback.fn.call( this, currentState );
1954 }
1955 } );
1956 }
1957
1958 // Save the callback info, so it can be checked on the next occurrence of
1959 // selectionChange.
1960 styleStateChangeCallbacks.push( { style: style, fn: callback } );
1961 },
1962
1963 /**
1964 * Applies the style upon the editor's current selection. Shorthand for
1965 * {@link CKEDITOR.style#apply}.
1966 *
1967 * @member CKEDITOR.editor
1968 * @param {CKEDITOR.style} style
1969 */
1970 applyStyle: function( style ) {
1971 style.apply( this );
1972 },
1973
1974 /**
1975 * Removes the style from the editor's current selection. Shorthand for
1976 * {@link CKEDITOR.style#remove}.
1977 *
1978 * @member CKEDITOR.editor
1979 * @param {CKEDITOR.style} style
1980 */
1981 removeStyle: function( style ) {
1982 style.remove( this );
1983 },
1984
1985 /**
1986 * Gets the current `stylesSet` for this instance.
1987 *
1988 * editor.getStylesSet( function( stylesDefinitions ) {} );
1989 *
1990 * See also {@link CKEDITOR.editor#stylesSet} event.
1991 *
1992 * @member CKEDITOR.editor
1993 * @param {Function} callback The function to be called with the styles data.
1994 */
1995 getStylesSet: function( callback ) {
1996 if ( !this._.stylesDefinitions ) {
1997 var editor = this,
1998 // Respect the backwards compatible definition entry
1999 configStyleSet = editor.config.stylesCombo_stylesSet || editor.config.stylesSet;
2000
2001 // The false value means that none styles should be loaded.
2002 if ( configStyleSet === false ) {
2003 callback( null );
2004 return;
2005 }
2006
2007 // #5352 Allow to define the styles directly in the config object
2008 if ( configStyleSet instanceof Array ) {
2009 editor._.stylesDefinitions = configStyleSet;
2010 callback( configStyleSet );
2011 return;
2012 }
2013
2014 // Default value is 'default'.
2015 if ( !configStyleSet )
2016 configStyleSet = 'default';
2017
2018 var partsStylesSet = configStyleSet.split( ':' ),
2019 styleSetName = partsStylesSet[ 0 ],
2020 externalPath = partsStylesSet[ 1 ];
2021
2022 CKEDITOR.stylesSet.addExternal( styleSetName, externalPath ? partsStylesSet.slice( 1 ).join( ':' ) : CKEDITOR.getUrl( 'styles.js' ), '' );
2023
2024 CKEDITOR.stylesSet.load( styleSetName, function( stylesSet ) {
2025 editor._.stylesDefinitions = stylesSet[ styleSetName ];
2026 callback( editor._.stylesDefinitions );
2027 } );
2028 } else {
2029 callback( this._.stylesDefinitions );
2030 }
2031 }
2032} );
2033
2034/**
2035 * Indicates that fully selected read-only elements will be included when
2036 * applying the style (for inline styles only).
2037 *
2038 * @since 3.5
2039 * @property {Boolean} [includeReadonly=false]
2040 * @member CKEDITOR.style
2041 */
2042
2043/**
2044 * Indicates that any matches element of this style will be eventually removed
2045 * when calling {@link CKEDITOR.editor#removeStyle}.
2046 *
2047 * @since 4.0
2048 * @property {Boolean} [alwaysRemoveElement=false]
2049 * @member CKEDITOR.style
2050 */
2051
2052/**
2053 * Disables inline styling on read-only elements.
2054 *
2055 * @since 3.5
2056 * @cfg {Boolean} [disableReadonlyStyling=false]
2057 * @member CKEDITOR.config
2058 */
2059
2060/**
2061 * The "styles definition set" to use in the editor. They will be used in the
2062 * styles combo and the style selector of the div container.
2063 *
2064 * The styles may be defined in the page containing the editor, or can be
2065 * loaded on demand from an external file. In the second case, if this setting
2066 * contains only a name, the `styles.js` file will be loaded from the
2067 * CKEditor root folder (what ensures backward compatibility with CKEditor 4.0).
2068 *
2069 * Otherwise, this setting has the `name:url` syntax, making it
2070 * possible to set the URL from which the styles file will be loaded.
2071 * Note that the `name` has to be equal to the name used in
2072 * {@link CKEDITOR.stylesSet#add} while registering the styles set.
2073 *
2074 * **Note**: Since 4.1 it is possible to set `stylesSet` to `false`
2075 * to prevent loading any styles set.
2076 *
2077 * Read more in the [documentation](#!/guide/dev_styles)
2078 * and see the [SDK sample](http://sdk.ckeditor.com/samples/styles.html).
2079 *
2080 * // Do not load any file. The styles set is empty.
2081 * config.stylesSet = false;
2082 *
2083 * // Load the 'mystyles' styles set from the styles.js file.
2084 * config.stylesSet = 'mystyles';
2085 *
2086 * // Load the 'mystyles' styles set from a relative URL.
2087 * config.stylesSet = 'mystyles:/editorstyles/styles.js';
2088 *
2089 * // Load the 'mystyles' styles set from a full URL.
2090 * config.stylesSet = 'mystyles:http://www.example.com/editorstyles/styles.js';
2091 *
2092 * // Load from a list of definitions.
2093 * config.stylesSet = [
2094 * { name: 'Strong Emphasis', element: 'strong' },
2095 * { name: 'Emphasis', element: 'em' },
2096 * ...
2097 * ];
2098 *
2099 * @since 3.3
2100 * @cfg {String/Array/Boolean} [stylesSet='default']
2101 * @member CKEDITOR.config
2102 */
diff --git a/sources/core/template.js b/sources/core/template.js
new file mode 100644
index 0000000..2b9e932
--- /dev/null
+++ b/sources/core/template.js
@@ -0,0 +1,68 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.template} class, which represents
8 * an UI template for an editor instance.
9 */
10
11( function() {
12 var cache = {},
13 rePlaceholder = /{([^}]+)}/g,
14 reEscapableChars = /([\\'])/g,
15 reNewLine = /\n/g,
16 reCarriageReturn = /\r/g;
17
18 /**
19 * Lightweight template used to build the output string from variables.
20 *
21 * // HTML template for presenting a label UI.
22 * var tpl = new CKEDITOR.template( '<div class="{cls}">{label}</div>' );
23 * alert( tpl.output( { cls: 'cke-label', label: 'foo'} ) ); // '<div class="cke-label">foo</div>'
24 *
25 * @class
26 * @constructor Creates a template class instance.
27 * @param {String} source The template source.
28 */
29 CKEDITOR.template = function( source ) {
30 // Builds an optimized function body for the output() method, focused on performance.
31 // For example, if we have this "source":
32 // '<div style="{style}">{editorName}</div>'
33 // ... the resulting function body will be (apart from the "buffer" handling):
34 // return [ '<div style="', data['style'] == undefined ? '{style}' : data['style'], '">', data['editorName'] == undefined ? '{editorName}' : data['editorName'], '</div>' ].join('');
35
36 // Try to read from the cache.
37 if ( cache[ source ] )
38 this.output = cache[ source ];
39 else {
40 var fn = source
41 // Escape chars like slash "\" or single quote "'".
42 .replace( reEscapableChars, '\\$1' )
43 .replace( reNewLine, '\\n' )
44 .replace( reCarriageReturn, '\\r' )
45 // Inject the template keys replacement.
46 .replace( rePlaceholder, function( m, key ) {
47 return "',data['" + key + "']==undefined?'{" + key + "}':data['" + key + "'],'";
48 } );
49
50 fn = "return buffer?buffer.push('" + fn + "'):['" + fn + "'].join('');";
51 this.output = cache[ source ] = Function( 'data', 'buffer', fn );
52 }
53 };
54} )();
55
56/**
57 * Processes the template, filling its variables with the provided data.
58 *
59 * @method output
60 * @param {Object} data An object containing properties which values will be
61 * used to fill the template variables. The property names must match the
62 * template variables names. Variables without matching properties will be
63 * kept untouched.
64 * @param {Array} [buffer] An array into which the output data will be pushed into.
65 * The number of entries appended to the array is unknown.
66 * @returns {String/Number} If `buffer` has not been provided, the processed
67 * template output data, otherwise the new length of `buffer`.
68 */
diff --git a/sources/core/tools.js b/sources/core/tools.js
new file mode 100644
index 0000000..7e0083f
--- /dev/null
+++ b/sources/core/tools.js
@@ -0,0 +1,1916 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * @fileOverview Defines the {@link CKEDITOR.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 * Normalizes hexadecimal notation so that the color string is always 6 characters long and lowercase.
985 *
986 * @param {String} styleText The style data (or just a string containing hex colors) to be converted.
987 * @returns {String} The style data with hex colors normalized.
988 */
989 normalizeHex: function( styleText ) {
990 return styleText.replace( /#(([0-9a-f]{3}){1,2})($|;|\s+)/gi, function( match, hexColor, hexColorPart, separator ) {
991 var normalizedHexColor = hexColor.toLowerCase();
992 if ( normalizedHexColor.length == 3 ) {
993 var parts = normalizedHexColor.split( '' );
994 normalizedHexColor = [ parts[ 0 ], parts[ 0 ], parts[ 1 ], parts[ 1 ], parts[ 2 ], parts[ 2 ] ].join( '' );
995 }
996 return '#' + normalizedHexColor + separator;
997 } );
998 },
999
1000 /**
1001 * Turns inline style text properties into one hash.
1002 *
1003 * @param {String} styleText The style data to be parsed.
1004 * @param {Boolean} [normalize=false] Normalize properties and values
1005 * (e.g. trim spaces, convert to lower case).
1006 * @param {Boolean} [nativeNormalize=false] Parse the data using the browser.
1007 * @returns {Object} The object containing parsed properties.
1008 */
1009 parseCssText: function( styleText, normalize, nativeNormalize ) {
1010 var retval = {};
1011
1012 if ( nativeNormalize ) {
1013 // Injects the style in a temporary span object, so the browser parses it,
1014 // retrieving its final format.
1015 var temp = new CKEDITOR.dom.element( 'span' );
1016 styleText = temp.setAttribute( 'style', styleText ).getAttribute( 'style' ) || '';
1017 }
1018
1019 // Normalize colors.
1020 if ( styleText ) {
1021 styleText = CKEDITOR.tools.normalizeHex( CKEDITOR.tools.convertRgbToHex( styleText ) );
1022 }
1023
1024 // IE will leave a single semicolon when failed to parse the style text. (#3891)
1025 if ( !styleText || styleText == ';' )
1026 return retval;
1027
1028 styleText.replace( /&quot;/g, '"' ).replace( /\s*([^:;\s]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value ) {
1029 if ( normalize ) {
1030 name = name.toLowerCase();
1031 // Drop extra whitespacing from font-family.
1032 if ( name == 'font-family' )
1033 value = value.replace( /\s*,\s*/g, ',' );
1034 value = CKEDITOR.tools.trim( value );
1035 }
1036
1037 retval[ name ] = value;
1038 } );
1039 return retval;
1040 },
1041
1042 /**
1043 * Serializes the `style name => value` hash to a style text.
1044 *
1045 * var styleObj = CKEDITOR.tools.parseCssText( 'color: red; border: none' );
1046 * console.log( styleObj.color ); // -> 'red'
1047 * CKEDITOR.tools.writeCssText( styleObj ); // -> 'color:red; border:none'
1048 * CKEDITOR.tools.writeCssText( styleObj, true ); // -> 'border:none; color:red'
1049 *
1050 * @since 4.1
1051 * @param {Object} styles The object contaning style properties.
1052 * @param {Boolean} [sort] Whether to sort CSS properties.
1053 * @returns {String} The serialized style text.
1054 */
1055 writeCssText: function( styles, sort ) {
1056 var name,
1057 stylesArr = [];
1058
1059 for ( name in styles )
1060 stylesArr.push( name + ':' + styles[ name ] );
1061
1062 if ( sort )
1063 stylesArr.sort();
1064
1065 return stylesArr.join( '; ' );
1066 },
1067
1068 /**
1069 * Compares two objects.
1070 *
1071 * **Note:** This method performs shallow, non-strict comparison.
1072 *
1073 * @since 4.1
1074 * @param {Object} left
1075 * @param {Object} right
1076 * @param {Boolean} [onlyLeft] Check only the properties that are present in the `left` object.
1077 * @returns {Boolean} Whether objects are identical.
1078 */
1079 objectCompare: function( left, right, onlyLeft ) {
1080 var name;
1081
1082 if ( !left && !right )
1083 return true;
1084 if ( !left || !right )
1085 return false;
1086
1087 for ( name in left ) {
1088 if ( left[ name ] != right[ name ] )
1089 return false;
1090
1091 }
1092
1093 if ( !onlyLeft ) {
1094 for ( name in right ) {
1095 if ( left[ name ] != right[ name ] )
1096 return false;
1097 }
1098 }
1099
1100 return true;
1101 },
1102
1103 /**
1104 * Returns an array of passed object's keys.
1105 *
1106 * console.log( CKEDITOR.tools.objectKeys( { foo: 1, bar: false } );
1107 * // -> [ 'foo', 'bar' ]
1108 *
1109 * @since 4.1
1110 * @param {Object} obj
1111 * @returns {Array} Object's keys.
1112 */
1113 objectKeys: function( obj ) {
1114 var keys = [];
1115 for ( var i in obj )
1116 keys.push( i );
1117
1118 return keys;
1119 },
1120
1121 /**
1122 * Converts an array to an object by rewriting array items
1123 * to object properties.
1124 *
1125 * var arr = [ 'foo', 'bar', 'foo' ];
1126 * console.log( CKEDITOR.tools.convertArrayToObject( arr ) );
1127 * // -> { foo: true, bar: true }
1128 * console.log( CKEDITOR.tools.convertArrayToObject( arr, 1 ) );
1129 * // -> { foo: 1, bar: 1 }
1130 *
1131 * @since 4.1
1132 * @param {Array} arr The array to be converted to an object.
1133 * @param [fillWith=true] Set each property of an object to `fillWith` value.
1134 */
1135 convertArrayToObject: function( arr, fillWith ) {
1136 var obj = {};
1137
1138 if ( arguments.length == 1 )
1139 fillWith = true;
1140
1141 for ( var i = 0, l = arr.length; i < l; ++i )
1142 obj[ arr[ i ] ] = fillWith;
1143
1144 return obj;
1145 },
1146
1147 /**
1148 * Tries to fix the `document.domain` of the current document to match the
1149 * parent window domain, avoiding "Same Origin" policy issues.
1150 * This is an Internet Explorer only requirement.
1151 *
1152 * @since 4.1.2
1153 * @returns {Boolean} `true` if the current domain is already good or if
1154 * it has been fixed successfully.
1155 */
1156 fixDomain: function() {
1157 var domain;
1158
1159 while ( 1 ) {
1160 try {
1161 // Try to access the parent document. It throws
1162 // "access denied" if restricted by the "Same Origin" policy.
1163 domain = window.parent.document.domain;
1164 break;
1165 } catch ( e ) {
1166 // Calculate the value to set to document.domain.
1167 domain = domain ?
1168
1169 // If it is not the first pass, strip one part of the
1170 // name. E.g. "test.example.com" => "example.com"
1171 domain.replace( /.+?(?:\.|$)/, '' ) :
1172
1173 // In the first pass, we'll handle the
1174 // "document.domain = document.domain" case.
1175 document.domain;
1176
1177 // Stop here if there is no more domain parts available.
1178 if ( !domain )
1179 break;
1180
1181 document.domain = domain;
1182 }
1183 }
1184
1185 return !!domain;
1186 },
1187
1188 /**
1189 * Buffers `input` events (or any `input` calls)
1190 * and triggers `output` not more often than once per `minInterval`.
1191 *
1192 * var buffer = CKEDITOR.tools.eventsBuffer( 200, function() {
1193 * console.log( 'foo!' );
1194 * } );
1195 *
1196 * buffer.input();
1197 * // 'foo!' logged immediately.
1198 * buffer.input();
1199 * // Nothing logged.
1200 * buffer.input();
1201 * // Nothing logged.
1202 * // ... after 200ms a single 'foo!' will be logged.
1203 *
1204 * Can be easily used with events:
1205 *
1206 * var buffer = CKEDITOR.tools.eventsBuffer( 200, function() {
1207 * console.log( 'foo!' );
1208 * } );
1209 *
1210 * editor.on( 'key', buffer.input );
1211 * // Note: There is no need to bind buffer as a context.
1212 *
1213 * @since 4.2.1
1214 * @param {Number} minInterval Minimum interval between `output` calls in milliseconds.
1215 * @param {Function} output Function that will be executed as `output`.
1216 * @param {Object} [scopeObj] The object used to scope the listener call (the `this` object).
1217 * @returns {Object}
1218 * @returns {Function} return.input Buffer's input method.
1219 * @returns {Function} return.reset Resets buffered events &mdash; `output` will not be executed
1220 * until next `input` is triggered.
1221 */
1222 eventsBuffer: function( minInterval, output, scopeObj ) {
1223 var scheduled,
1224 lastOutput = 0;
1225
1226 function triggerOutput() {
1227 lastOutput = ( new Date() ).getTime();
1228 scheduled = false;
1229 if ( scopeObj ) {
1230 output.call( scopeObj );
1231 } else {
1232 output();
1233 }
1234 }
1235
1236 return {
1237 input: function() {
1238 if ( scheduled )
1239 return;
1240
1241 var diff = ( new Date() ).getTime() - lastOutput;
1242
1243 // If less than minInterval passed after last check,
1244 // schedule next for minInterval after previous one.
1245 if ( diff < minInterval )
1246 scheduled = setTimeout( triggerOutput, minInterval - diff );
1247 else
1248 triggerOutput();
1249 },
1250
1251 reset: function() {
1252 if ( scheduled )
1253 clearTimeout( scheduled );
1254
1255 scheduled = lastOutput = 0;
1256 }
1257 };
1258 },
1259
1260 /**
1261 * Enables HTML5 elements for older browsers (IE8) in the passed document.
1262 *
1263 * In IE8 this method can also be executed on a document fragment.
1264 *
1265 * **Note:** This method has to be used in the `<head>` section of the document.
1266 *
1267 * @since 4.3
1268 * @param {Object} doc Native `Document` or `DocumentFragment` in which the elements will be enabled.
1269 * @param {Boolean} [withAppend] Whether to append created elements to the `doc`.
1270 */
1271 enableHtml5Elements: function( doc, withAppend ) {
1272 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( ',' ),
1273 i = els.length,
1274 el;
1275
1276 while ( i-- ) {
1277 el = doc.createElement( els[ i ] );
1278 if ( withAppend )
1279 doc.appendChild( el );
1280 }
1281 },
1282
1283 /**
1284 * Checks if any of the `arr` items match the provided regular expression.
1285 *
1286 * @param {Array} arr The array whose items will be checked.
1287 * @param {RegExp} regexp The regular expression.
1288 * @returns {Boolean} Returns `true` for the first occurrence of the search pattern.
1289 * @since 4.4
1290 */
1291 checkIfAnyArrayItemMatches: function( arr, regexp ) {
1292 for ( var i = 0, l = arr.length; i < l; ++i ) {
1293 if ( arr[ i ].match( regexp ) )
1294 return true;
1295 }
1296 return false;
1297 },
1298
1299 /**
1300 * Checks if any of the `obj` properties match the provided regular expression.
1301 *
1302 * @param obj The object whose properties will be checked.
1303 * @param {RegExp} regexp The regular expression.
1304 * @returns {Boolean} Returns `true` for the first occurrence of the search pattern.
1305 * @since 4.4
1306 */
1307 checkIfAnyObjectPropertyMatches: function( obj, regexp ) {
1308 for ( var i in obj ) {
1309 if ( i.match( regexp ) )
1310 return true;
1311 }
1312 return false;
1313 },
1314
1315 /**
1316 * Converts a keystroke to its string representation. Returns an object with two fields:
1317 *
1318 * * `display` &ndash; A string that should be used for visible labels.
1319 * For Mac devices it uses `⌥` for `ALT`, `⇧` for `SHIFT` and `⌘` for `COMMAND`.
1320 * * `aria` &ndash; A string that should be used for ARIA descriptions.
1321 * It does not use special characters such as `⌥`, `⇧` or `⌘`.
1322 *
1323 * var lang = editor.lang.common.keyboard;
1324 * var shortcut = CKEDITOR.tools.keystrokeToString( lang, CKEDITOR.CTRL + 88 );
1325 * console.log( shortcut.display ); // 'CTRL + X', on Mac '⌘ + X'.
1326 * console.log( shortcut.aria ); // 'CTRL + X', on Mac 'COMMAND + X'.
1327 *
1328 * @since 4.6.0
1329 * @param {Object} lang A language object with the key name translation.
1330 * @param {Number} keystroke The keystroke to convert.
1331 * @returns {{display: String, aria: String}}
1332 */
1333 keystrokeToString: function( lang, keystroke ) {
1334 var special = keystroke & 0xFF0000,
1335 key = keystroke & 0x00FFFF,
1336 isMac = CKEDITOR.env.mac,
1337 CTRL = 17,
1338 CMD = 224,
1339 ALT = 18,
1340 SHIFT = 16,
1341 display = [],
1342 aria = [];
1343
1344
1345 if ( special & CKEDITOR.CTRL ) {
1346 display.push( isMac ? '⌘' : lang[ CTRL ] );
1347 aria.push( isMac ? lang[ CMD ] : lang[ CTRL ] );
1348 }
1349
1350 if ( special & CKEDITOR.ALT ) {
1351 display.push( isMac ? '⌥' : lang[ ALT ] );
1352 aria.push( lang[ ALT ] );
1353 }
1354
1355 if ( special & CKEDITOR.SHIFT ) {
1356 display.push( isMac ? '⇧' : lang[ SHIFT ] );
1357 aria.push( lang[ SHIFT ] );
1358 }
1359
1360 if ( key ) {
1361 if ( lang[ key ] ) {
1362 display.push( lang[ key ] );
1363 aria.push( lang[ key ] );
1364 } else {
1365 display.push( String.fromCharCode( key ) );
1366 aria.push( String.fromCharCode( key ) );
1367 }
1368 }
1369
1370 return {
1371 display: display.join( '+' ),
1372 aria: aria.join( '+' )
1373 };
1374 },
1375
1376 /**
1377 * The data URI of a transparent image. May be used e.g. in HTML as an image source or in CSS in `url()`.
1378 *
1379 * @since 4.4
1380 * @readonly
1381 */
1382 transparentImageData: 'data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==',
1383
1384
1385 /**
1386 * Returns the value of the cookie with a given name or `null` if the cookie is not found.
1387 *
1388 * @since 4.5.6
1389 * @param {String} name
1390 * @returns {String}
1391 */
1392 getCookie: function( name ) {
1393 name = name.toLowerCase();
1394 var parts = document.cookie.split( ';' );
1395 var pair, key;
1396
1397 for ( var i = 0; i < parts.length; i++ ) {
1398 pair = parts[ i ].split( '=' );
1399 key = decodeURIComponent( CKEDITOR.tools.trim( pair[ 0 ] ).toLowerCase() );
1400
1401 if ( key === name ) {
1402 return decodeURIComponent( pair.length > 1 ? pair[ 1 ] : '' );
1403 }
1404 }
1405
1406 return null;
1407 },
1408
1409 /**
1410 * Sets the value of the cookie with a given name.
1411 *
1412 * @since 4.5.6
1413 * @param {String} name
1414 * @param {String} value
1415 */
1416 setCookie: function( name, value ) {
1417 document.cookie = encodeURIComponent( name ) + '=' + encodeURIComponent( value ) + ';path=/';
1418 },
1419
1420 /**
1421 * Returns the CSRF token value. The value is a hash stored in `document.cookie`
1422 * under the `ckCsrfToken` key. The CSRF token can be used to secure the communication
1423 * between the web browser and the server, i.e. for the file upload feature in the editor.
1424 *
1425 * @since 4.5.6
1426 * @returns {String}
1427 */
1428 getCsrfToken: function() {
1429 var token = CKEDITOR.tools.getCookie( TOKEN_COOKIE_NAME );
1430
1431 if ( !token || token.length != TOKEN_LENGTH ) {
1432 token = generateToken( TOKEN_LENGTH );
1433 CKEDITOR.tools.setCookie( TOKEN_COOKIE_NAME, token );
1434 }
1435
1436 return token;
1437 },
1438
1439 /**
1440 * Returns an escaped CSS selector. `CSS.escape()` is used if defined, leading digit is escaped otherwise.
1441 *
1442 * @since 4.5.10
1443 * @param {String} selector A CSS selector to escape.
1444 * @returns {String} An escaped selector.
1445 */
1446 escapeCss: function( selector ) {
1447 // Invalid input.
1448 if ( !selector ) {
1449 return '';
1450 }
1451
1452 // CSS.escape() can be used.
1453 if ( window.CSS && CSS.escape ) {
1454 return CSS.escape( selector );
1455 }
1456
1457 // Simple leading digit escape.
1458 if ( !isNaN( parseInt( selector.charAt( 0 ), 10 ) ) ) {
1459 return '\\3' + selector.charAt( 0 ) + ' ' + selector.substring( 1, selector.length );
1460 }
1461
1462 return selector;
1463 },
1464
1465 /**
1466 * A set of functions for operations on styles.
1467 *
1468 * @property {CKEDITOR.tools.style}
1469 */
1470 style: {
1471 /**
1472 * Methods to parse miscellaneous CSS properties.
1473 *
1474 * @property {CKEDITOR.tools.style.parse}
1475 * @member CKEDITOR.tools.style
1476 */
1477 parse: {
1478 // Color list based on https://www.w3.org/TR/css-color-4/#named-colors.
1479 _colors: {
1480 aliceblue: '#F0F8FF',
1481 antiquewhite: '#FAEBD7',
1482 aqua: '#00FFFF',
1483 aquamarine: '#7FFFD4',
1484 azure: '#F0FFFF',
1485 beige: '#F5F5DC',
1486 bisque: '#FFE4C4',
1487 black: '#000000',
1488 blanchedalmond: '#FFEBCD',
1489 blue: '#0000FF',
1490 blueviolet: '#8A2BE2',
1491 brown: '#A52A2A',
1492 burlywood: '#DEB887',
1493 cadetblue: '#5F9EA0',
1494 chartreuse: '#7FFF00',
1495 chocolate: '#D2691E',
1496 coral: '#FF7F50',
1497 cornflowerblue: '#6495ED',
1498 cornsilk: '#FFF8DC',
1499 crimson: '#DC143C',
1500 cyan: '#00FFFF',
1501 darkblue: '#00008B',
1502 darkcyan: '#008B8B',
1503 darkgoldenrod: '#B8860B',
1504 darkgray: '#A9A9A9',
1505 darkgreen: '#006400',
1506 darkgrey: '#A9A9A9',
1507 darkkhaki: '#BDB76B',
1508 darkmagenta: '#8B008B',
1509 darkolivegreen: '#556B2F',
1510 darkorange: '#FF8C00',
1511 darkorchid: '#9932CC',
1512 darkred: '#8B0000',
1513 darksalmon: '#E9967A',
1514 darkseagreen: '#8FBC8F',
1515 darkslateblue: '#483D8B',
1516 darkslategray: '#2F4F4F',
1517 darkslategrey: '#2F4F4F',
1518 darkturquoise: '#00CED1',
1519 darkviolet: '#9400D3',
1520 deeppink: '#FF1493',
1521 deepskyblue: '#00BFFF',
1522 dimgray: '#696969',
1523 dimgrey: '#696969',
1524 dodgerblue: '#1E90FF',
1525 firebrick: '#B22222',
1526 floralwhite: '#FFFAF0',
1527 forestgreen: '#228B22',
1528 fuchsia: '#FF00FF',
1529 gainsboro: '#DCDCDC',
1530 ghostwhite: '#F8F8FF',
1531 gold: '#FFD700',
1532 goldenrod: '#DAA520',
1533 gray: '#808080',
1534 green: '#008000',
1535 greenyellow: '#ADFF2F',
1536 grey: '#808080',
1537 honeydew: '#F0FFF0',
1538 hotpink: '#FF69B4',
1539 indianred: '#CD5C5C',
1540 indigo: '#4B0082',
1541 ivory: '#FFFFF0',
1542 khaki: '#F0E68C',
1543 lavender: '#E6E6FA',
1544 lavenderblush: '#FFF0F5',
1545 lawngreen: '#7CFC00',
1546 lemonchiffon: '#FFFACD',
1547 lightblue: '#ADD8E6',
1548 lightcoral: '#F08080',
1549 lightcyan: '#E0FFFF',
1550 lightgoldenrodyellow: '#FAFAD2',
1551 lightgray: '#D3D3D3',
1552 lightgreen: '#90EE90',
1553 lightgrey: '#D3D3D3',
1554 lightpink: '#FFB6C1',
1555 lightsalmon: '#FFA07A',
1556 lightseagreen: '#20B2AA',
1557 lightskyblue: '#87CEFA',
1558 lightslategray: '#778899',
1559 lightslategrey: '#778899',
1560 lightsteelblue: '#B0C4DE',
1561 lightyellow: '#FFFFE0',
1562 lime: '#00FF00',
1563 limegreen: '#32CD32',
1564 linen: '#FAF0E6',
1565 magenta: '#FF00FF',
1566 maroon: '#800000',
1567 mediumaquamarine: '#66CDAA',
1568 mediumblue: '#0000CD',
1569 mediumorchid: '#BA55D3',
1570 mediumpurple: '#9370DB',
1571 mediumseagreen: '#3CB371',
1572 mediumslateblue: '#7B68EE',
1573 mediumspringgreen: '#00FA9A',
1574 mediumturquoise: '#48D1CC',
1575 mediumvioletred: '#C71585',
1576 midnightblue: '#191970',
1577 mintcream: '#F5FFFA',
1578 mistyrose: '#FFE4E1',
1579 moccasin: '#FFE4B5',
1580 navajowhite: '#FFDEAD',
1581 navy: '#000080',
1582 oldlace: '#FDF5E6',
1583 olive: '#808000',
1584 olivedrab: '#6B8E23',
1585 orange: '#FFA500',
1586 orangered: '#FF4500',
1587 orchid: '#DA70D6',
1588 palegoldenrod: '#EEE8AA',
1589 palegreen: '#98FB98',
1590 paleturquoise: '#AFEEEE',
1591 palevioletred: '#DB7093',
1592 papayawhip: '#FFEFD5',
1593 peachpuff: '#FFDAB9',
1594 peru: '#CD853F',
1595 pink: '#FFC0CB',
1596 plum: '#DDA0DD',
1597 powderblue: '#B0E0E6',
1598 purple: '#800080',
1599 rebeccapurple: '#663399',
1600 red: '#FF0000',
1601 rosybrown: '#BC8F8F',
1602 royalblue: '#4169E1',
1603 saddlebrown: '#8B4513',
1604 salmon: '#FA8072',
1605 sandybrown: '#F4A460',
1606 seagreen: '#2E8B57',
1607 seashell: '#FFF5EE',
1608 sienna: '#A0522D',
1609 silver: '#C0C0C0',
1610 skyblue: '#87CEEB',
1611 slateblue: '#6A5ACD',
1612 slategray: '#708090',
1613 slategrey: '#708090',
1614 snow: '#FFFAFA',
1615 springgreen: '#00FF7F',
1616 steelblue: '#4682B4',
1617 tan: '#D2B48C',
1618 teal: '#008080',
1619 thistle: '#D8BFD8',
1620 tomato: '#FF6347',
1621 turquoise: '#40E0D0',
1622 violet: '#EE82EE',
1623 wheat: '#F5DEB3',
1624 white: '#FFFFFF',
1625 whitesmoke: '#F5F5F5',
1626 yellow: '#FFFF00',
1627 yellowgreen: '#9ACD32'
1628 },
1629
1630 _rgbaRegExp: /rgba?\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*(?:,\s*[0-9.]+\s*)?\)/gi,
1631
1632 _hslaRegExp: /hsla?\(\s*[0-9.]+\s*,\s*\d+%\s*,\s*\d+%\s*(?:,\s*[0-9.]+\s*)?\)/gi,
1633
1634 /**
1635 * Parses the `value` used as a `background` property shorthand and returns information as an object.
1636 *
1637 * **Note:** Currently only the `color` property is extracted. Any other parts will go into the `unprocessed` property.
1638 *
1639 * var background = CKEDITOR.tools.style.parse.background( '#0C0 url(foo.png)' );
1640 * console.log( background );
1641 * // Logs: { color: '#0C0', unprocessed: 'url(foo.png)' }
1642 *
1643 * @param {String} value The value of the `background` property.
1644 * @returns {Object} An object with information extracted from the background.
1645 * @returns {String} return.color The **first** color value found. The color format remains the same as in input.
1646 * @returns {String} return.unprocessed The remaining part of the `value` that has not been processed.
1647 * @member CKEDITOR.tools.style.parse
1648 */
1649 background: function( value ) {
1650 var ret = [],
1651 colors = [];
1652
1653 colors = this._findColor( value );
1654
1655 if ( colors.length ) {
1656 ret.color = colors[ 0 ];
1657
1658 CKEDITOR.tools.array.forEach( colors, function( colorToken ) {
1659 value = value.replace( colorToken, '' );
1660 } );
1661 }
1662
1663 value = CKEDITOR.tools.trim( value );
1664
1665 if ( value ) {
1666 // If anything was left unprocessed include it as unprocessed part.
1667 ret.unprocessed = value;
1668 }
1669
1670 return ret;
1671 },
1672
1673 /**
1674 * Parses the `margin` CSS property shorthand format.
1675 *
1676 * console.log( CKEDITOR.tools.parse.margin( '3px 0 2' ) );
1677 * // Logs: { top: "3px", right: "0", bottom: "2", left: "0" }
1678 *
1679 * @param {String} value The `margin` property value.
1680 * @returns {Object}
1681 * @returns {Number} return.top Top margin.
1682 * @returns {Number} return.right Right margin.
1683 * @returns {Number} return.bottom Bottom margin.
1684 * @returns {Number} return.left Left margin.
1685 * @member CKEDITOR.tools.style.parse
1686 */
1687 margin: function( value ) {
1688 var ret = {};
1689
1690 var widths = value.match( /(?:\-?[\.\d]+(?:%|\w*)|auto|inherit|initial|unset)/g ) || [ '0px' ];
1691
1692 switch ( widths.length ) {
1693 case 1:
1694 mapStyles( [ 0, 0, 0, 0 ] );
1695 break;
1696 case 2:
1697 mapStyles( [ 0, 1, 0, 1 ] );
1698 break;
1699 case 3:
1700 mapStyles( [ 0, 1, 2, 1 ] );
1701 break;
1702 case 4:
1703 mapStyles( [ 0, 1, 2, 3 ] );
1704 break;
1705 }
1706
1707 function mapStyles( map ) {
1708 ret.top = widths[ map[ 0 ] ];
1709 ret.right = widths[ map[ 1 ] ];
1710 ret.bottom = widths[ map[ 2 ] ];
1711 ret.left = widths[ map[ 3 ] ];
1712 }
1713
1714 return ret;
1715 },
1716
1717 /**
1718 * Searches the `value` for any CSS color occurrences and returns it.
1719 *
1720 * @private
1721 * @param {String} value
1722 * @returns {String[]} An array of matched results.
1723 * @member CKEDITOR.tools.style.parse
1724 */
1725 _findColor: function( value ) {
1726 var ret = [],
1727 arrayTools = CKEDITOR.tools.array;
1728
1729
1730 // Check for rgb(a).
1731 ret = ret.concat( value.match( this._rgbaRegExp ) || [] );
1732
1733 // Check for hsl(a).
1734 ret = ret.concat( value.match( this._hslaRegExp ) || [] );
1735
1736 ret = ret.concat( arrayTools.filter( value.split( /\s+/ ), function( colorEntry ) {
1737 // Check for hex format.
1738 if ( colorEntry.match( /^\#[a-f0-9]{3}(?:[a-f0-9]{3})?$/gi ) ) {
1739 return true;
1740 }
1741
1742 // Check for preset names.
1743 return colorEntry.toLowerCase() in CKEDITOR.tools.style.parse._colors;
1744 } ) );
1745
1746 return ret;
1747 }
1748 }
1749 },
1750
1751 /**
1752 * A set of array helpers.
1753 *
1754 * @property {CKEDITOR.tools.array}
1755 * @member CKEDITOR.tools
1756 */
1757 array: {
1758 /**
1759 * Returns a copy of `array` filtered using the `fn` function. Any elements that the `fn` will return `false` for
1760 * will get removed from the returned array.
1761 *
1762 * var filtered = this.array.filter( [ 0, 1, 2, 3 ], function( value ) {
1763 * // Leave only values equal or greater than 2.
1764 * return value >= 2;
1765 * } );
1766 * console.log( filtered );
1767 * // Logs: [ 2, 3 ]
1768 *
1769 * @param {Array} array
1770 * @param {Function} fn A function that gets called with each `array` item. Any item that `fn`
1771 * returned a `false`-alike value for will be filtered out of the `array`.
1772 * @param {Mixed} fn.value The currently iterated array value.
1773 * @param {Number} fn.index The index of the currently iterated value in an array.
1774 * @param {Array} fn.array The original array passed as the `array` variable.
1775 * @param {Mixed} [thisArg=undefined] A context object for `fn`.
1776 * @returns {Array} The filtered array.
1777 * @member CKEDITOR.tools.array
1778 */
1779 filter: function( array, fn, thisArg ) {
1780 var ret = [];
1781
1782 this.forEach( array, function( val, i ) {
1783 if ( fn.call( thisArg, val, i, array ) ) {
1784 ret.push( val );
1785 }
1786 } );
1787
1788 return ret;
1789 },
1790
1791 /**
1792 * Iterates over every element in the `array`.
1793 *
1794 * @param {Array} array An array to be iterated over.
1795 * @param {Function} fn The function called for every `array` element.
1796 * @param {Mixed} fn.value The currently iterated array value.
1797 * @param {Number} fn.index The index of the currently iterated value in an array.
1798 * @param {Array} fn.array The original array passed as an `array` variable.
1799 * @param {Mixed} [thisArg=undefined] The context object for `fn`.
1800 * @member CKEDITOR.tools.array
1801 */
1802 forEach: function( array, fn, thisArg ) {
1803 var len = array.length,
1804 i;
1805
1806 for ( i = 0; i < len; i++ ) {
1807 fn.call( thisArg, array[ i ], i, array );
1808 }
1809 },
1810
1811 /**
1812 * Applies a function to each element of an array and returns the array of results in the same order.
1813 * Note the order of the parameters.
1814 *
1815 * @param {Array} array An array of elements that `fn` is applied on.
1816 * @param {Function} fn A function with the signature `a -> b`.
1817 * @param {Mixed} [thisArg=undefined] The context object for `fn`.
1818 * @returns {Array} An array of mapped elements.
1819 * @member CKEDITOR.tools.array
1820 * @since 4.6.2
1821 */
1822 map: function( array, fn, thisArg ) {
1823 var result = [];
1824 for ( var i = 0; i < array.length; i++ ) {
1825 result.push( fn.call( thisArg, array[ i ], i, array ) );
1826 }
1827 return result;
1828 },
1829
1830 /**
1831 * Applies a function against each value in an array storing the result in an accumulator passed to the next iteration.
1832 * Note the order of the parameters.
1833 *
1834 * @param {Array} array An array of elements that `fn` is applied on.
1835 * @param {Function} fn A function with the signature `(accumulator, a, index, array) -> b`.
1836 * @param {Mixed} initial Initial value of the accumulator.
1837 * @param {Mixed} [thisArg=undefined] The context object for `fn`.
1838 * @returns {Mixed} The final value of the accumulator.
1839 * @member CKEDITOR.tools.array
1840 * @since 4.6.2
1841 */
1842 reduce: function( array, fn, initial, thisArg ) {
1843 var acc = initial;
1844 for ( var i = 0; i < array.length; i++ ) {
1845 acc = fn.call( thisArg, acc, array[ i ], i, array );
1846 }
1847 return acc;
1848 }
1849 }
1850 };
1851
1852 // Generates a CSRF token with a given length.
1853 //
1854 // @since 4.5.6
1855 // @param {Number} length
1856 // @returns {string}
1857 function generateToken( length ) {
1858 var randValues = [];
1859 var result = '';
1860
1861 if ( window.crypto && window.crypto.getRandomValues ) {
1862 randValues = new Uint8Array( length );
1863 window.crypto.getRandomValues( randValues );
1864 } else {
1865 for ( var i = 0; i < length; i++ ) {
1866 randValues.push( Math.floor( Math.random() * 256 ) );
1867 }
1868 }
1869
1870 for ( var j = 0; j < randValues.length; j++ ) {
1871 var character = tokenCharset.charAt( randValues[ j ] % tokenCharset.length );
1872 result += Math.random() > 0.5 ? character.toUpperCase() : character;
1873 }
1874
1875 return result;
1876 }
1877
1878 /**
1879 * @member CKEDITOR.tools.array
1880 * @method indexOf
1881 * @inheritdoc CKEDITOR.tools#indexOf
1882 */
1883 CKEDITOR.tools.array.indexOf = CKEDITOR.tools.indexOf;
1884
1885 /**
1886 * @member CKEDITOR.tools.array
1887 * @method isArray
1888 * @inheritdoc CKEDITOR.tools#isArray
1889 */
1890 CKEDITOR.tools.array.isArray = CKEDITOR.tools.isArray;
1891
1892
1893
1894 /**
1895 * The namespace containing functions to work on CSS properties.
1896 *
1897 * @since 4.6.1
1898 * @class CKEDITOR.tools.style
1899 */
1900
1901 /**
1902 * The namespace with helper functions to parse some common CSS properties.
1903 *
1904 * @since 4.6.1
1905 * @class CKEDITOR.tools.style.parse
1906 */
1907
1908 /**
1909 * The namespace with helper functions and polyfills for arrays.
1910 *
1911 * @since 4.6.1
1912 * @class CKEDITOR.tools.array
1913 */
1914} )();
1915
1916// PACKAGER_RENAME( CKEDITOR.tools )
diff --git a/sources/core/ui.js b/sources/core/ui.js
new file mode 100644
index 0000000..ac5a285
--- /dev/null
+++ b/sources/core/ui.js
@@ -0,0 +1,185 @@
1/**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6/**
7 * 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 */