1 /*
  2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
  3 For licensing, see LICENSE.html or http://ckeditor.com/license
  4 */
  5 
  6 /**
  7  * @name CKEDITOR.theme
  8  * @class
  9  */
 10 
 11 CKEDITOR.themes.add( 'default', (function()
 12 {
 13 	var hiddenSkins = {};
 14 
 15 	function checkSharedSpace( editor, spaceName )
 16 	{
 17 		var container,
 18 			element;
 19 
 20 		// Try to retrieve the target element from the sharedSpaces settings.
 21 		element = editor.config.sharedSpaces;
 22 		element = element && element[ spaceName ];
 23 		element = element && CKEDITOR.document.getById( element );
 24 
 25 		// If the element is available, we'll then create the container for
 26 		// the space.
 27 		if ( element )
 28 		{
 29 			// Creates an HTML structure that reproduces the editor class hierarchy.
 30 			var html =
 31 				'<span class="cke_shared "' +
 32 				' dir="'+ editor.lang.dir + '"' +
 33 				'>' +
 34 				'<span class="' + editor.skinClass + ' ' + editor.id + ' cke_editor_' + editor.name + '">' +
 35 				'<span class="' + CKEDITOR.env.cssClass + '">' +
 36 				'<span class="cke_wrapper cke_' + editor.lang.dir + '">' +
 37 				'<span class="cke_editor">' +
 38 				'<div class="cke_' + spaceName + '">' +
 39 				'</div></span></span></span></span></span>';
 40 
 41 			var mainContainer = element.append( CKEDITOR.dom.element.createFromHtml( html, element.getDocument() ) );
 42 
 43 			// Only the first container starts visible. Others get hidden.
 44 			if ( element.getCustomData( 'cke_hasshared' ) )
 45 				mainContainer.hide();
 46 			else
 47 				element.setCustomData( 'cke_hasshared', 1 );
 48 
 49 			// Get the deeper inner <div>.
 50 			container = mainContainer.getChild( [0,0,0,0] );
 51 
 52 			// Save a reference to the shared space container.
 53 			!editor.sharedSpaces && ( editor.sharedSpaces = {} );
 54 			editor.sharedSpaces[ spaceName ] = container;
 55 
 56 			// When the editor gets focus, we show the space container, hiding others.
 57 			editor.on( 'focus', function()
 58 				{
 59 					for ( var i = 0, sibling, children = element.getChildren() ; ( sibling = children.getItem( i ) ) ; i++ )
 60 					{
 61 						if ( sibling.type == CKEDITOR.NODE_ELEMENT
 62 							&& !sibling.equals( mainContainer )
 63 							&& sibling.hasClass( 'cke_shared' ) )
 64 						{
 65 							sibling.hide();
 66 						}
 67 					}
 68 
 69 					mainContainer.show();
 70 				});
 71 
 72 			editor.on( 'destroy', function()
 73 				{
 74 					mainContainer.remove();
 75 				});
 76 		}
 77 
 78 		return container;
 79 	}
 80 
 81 	return /** @lends CKEDITOR.theme */ {
 82 		build : function( editor, themePath )
 83 		{
 84 			var name = editor.name,
 85 				element = editor.element,
 86 				elementMode = editor.elementMode;
 87 
 88 			if ( !element || elementMode == CKEDITOR.ELEMENT_MODE_NONE )
 89 				return;
 90 
 91 			if ( elementMode == CKEDITOR.ELEMENT_MODE_REPLACE )
 92 				element.hide();
 93 
 94 			// Get the HTML for the predefined spaces.
 95 			var topHtml			= editor.fire( 'themeSpace', { space : 'top', html : '' } ).html;
 96 			var contentsHtml	= editor.fire( 'themeSpace', { space : 'contents', html : '' } ).html;
 97 			var bottomHtml		= editor.fireOnce( 'themeSpace', { space : 'bottom', html : '' } ).html;
 98 
 99 			var height	= contentsHtml && editor.config.height;
100 
101 			var tabIndex = editor.config.tabIndex || editor.element.getAttribute( 'tabindex' ) || 0;
102 
103 			// The editor height is considered only if the contents space got filled.
104 			if ( !contentsHtml )
105 				height = 'auto';
106 			else if ( !isNaN( height ) )
107 				height += 'px';
108 
109 			var style = '';
110 			var width	= editor.config.width;
111 
112 			if ( width )
113 			{
114 				if ( !isNaN( width ) )
115 					width += 'px';
116 
117 				style += "width: " + width + ";";
118 			}
119 
120 			var sharedTop		= topHtml && checkSharedSpace( editor, 'top' ),
121 				sharedBottoms	= checkSharedSpace( editor, 'bottom' );
122 
123 			sharedTop		&& ( sharedTop.setHtml( topHtml )		, topHtml = '' );
124 			sharedBottoms	&& ( sharedBottoms.setHtml( bottomHtml ), bottomHtml = '' );
125 
126 			var hideSkin = '<style>.' + editor.skinClass + '{visibility:hidden;}</style>';
127 			if ( hiddenSkins[ editor.skinClass ] )
128 				hideSkin = '';
129 			else
130 				hiddenSkins[ editor.skinClass ] = 1;
131 
132 			var container = CKEDITOR.dom.element.createFromHtml( [
133 				'<span' +
134 					' id="cke_', name, '"' +
135 					' class="', editor.skinClass, ' ', editor.id, ' cke_editor_', name, '"' +
136 					' dir="', editor.lang.dir, '"' +
137 					' title="', ( CKEDITOR.env.gecko ? ' ' : '' ), '"' +
138 					' lang="', editor.langCode, '"' +
139 						( CKEDITOR.env.webkit? ' tabindex="' + tabIndex + '"' : '' ) +
140 					' role="application"' +
141 					' aria-labelledby="cke_', name, '_arialbl"' +
142 					( style ? ' style="' + style + '"' : '' ) +
143 					'>' +
144 					'<span id="cke_', name, '_arialbl" class="cke_voice_label">' + editor.lang.editor + '</span>' +
145 					'<span class="' , CKEDITOR.env.cssClass, '" role="presentation">' +
146 						'<span class="cke_wrapper cke_', editor.lang.dir, '" role="presentation">' +
147 							'<table class="cke_editor" border="0" cellspacing="0" cellpadding="0" role="presentation"><tbody>' +
148 								'<tr', topHtml		? '' : ' style="display:none"', ' role="presentation"><td id="cke_top_'		, name, '" class="cke_top" role="presentation">'	, topHtml		, '</td></tr>' +
149 								'<tr', contentsHtml	? '' : ' style="display:none"', ' role="presentation"><td id="cke_contents_', name, '" class="cke_contents" style="height:', height, '" role="presentation">', contentsHtml, '</td></tr>' +
150 								'<tr', bottomHtml	? '' : ' style="display:none"', ' role="presentation"><td id="cke_bottom_'	, name, '" class="cke_bottom" role="presentation">'	, bottomHtml	, '</td></tr>' +
151 							'</tbody></table>' +
152 							//Hide the container when loading skins, later restored by skin css.
153 							hideSkin +
154 						'</span>' +
155 					'</span>' +
156 				'</span>' ].join( '' ) );
157 
158 			container.getChild( [1, 0, 0, 0, 0] ).unselectable();
159 			container.getChild( [1, 0, 0, 0, 2] ).unselectable();
160 
161 			if ( elementMode == CKEDITOR.ELEMENT_MODE_REPLACE )
162 				container.insertAfter( element );
163 			else
164 				element.append( container );
165 
166 			/**
167 			 * The DOM element that holds the main editor interface.
168 			 * @name CKEDITOR.editor.prototype.container
169 			 * @type CKEDITOR.dom.element
170 			 * @example
171 			 * var editor = CKEDITOR.instances.editor1;
172 			 * alert( <b>editor.container</b>.getName() );  "span"
173 			 */
174 			editor.container = container;
175 
176 			// Disable browser context menu for editor's chrome.
177 			container.disableContextMenu();
178 
179 			// Use a class to indicate that the current selection is in different direction than the UI.
180 			editor.on( 'contentDirChanged', function( evt )
181 			{
182 				var func = ( editor.lang.dir != evt.data ? 'add' : 'remove' ) + 'Class';
183 
184 				container.getChild( 1 )[ func ]( 'cke_mixed_dir_content' );
185 
186 				// Put the mixed direction class on the respective element also for shared spaces.
187 				var toolbarSpace = this.sharedSpaces && this.sharedSpaces[ this.config.toolbarLocation ];
188 				toolbarSpace && toolbarSpace.getParent().getParent()[ func ]( 'cke_mixed_dir_content' );
189 			});
190 
191 			editor.fireOnce( 'themeLoaded' );
192 			editor.fireOnce( 'uiReady' );
193 		},
194 
195 		buildDialog : function( editor )
196 		{
197 			var baseIdNumber = CKEDITOR.tools.getNextNumber();
198 
199 			var element = CKEDITOR.dom.element.createFromHtml( [
200 					'<div class="', editor.id, '_dialog cke_editor_', editor.name.replace('.', '\\.'), '_dialog cke_skin_', editor.skinName,
201 						'" dir="', editor.lang.dir, '"' +
202 						' lang="', editor.langCode, '"' +
203 						' role="dialog"' +
204 						' aria-labelledby="%title#"' +
205 						'>' +
206 						'<table class="cke_dialog', ' ' + CKEDITOR.env.cssClass,
207 							' cke_', editor.lang.dir, '" style="position:absolute" role="presentation">' +
208 							'<tr><td role="presentation">' +
209 							'<div class="%body" role="presentation">' +
210 								'<div id="%title#" class="%title" role="presentation"></div>' +
211 								'<a id="%close_button#" class="%close_button" href="javascript:void(0)" title="' +  editor.lang.common.close+'" role="button"><span class="cke_label">X</span></a>' +
212 								'<div id="%tabs#" class="%tabs" role="tablist"></div>' +
213 								'<table class="%contents" role="presentation">' +
214 								'<tr>' +
215 								  '<td id="%contents#" class="%contents" role="presentation"></td>' +
216 								'</tr>' +
217 								'<tr>' +
218 								  '<td id="%footer#" class="%footer" role="presentation"></td>' +
219 								'</tr>' +
220 								'</table>' +
221 							'</div>' +
222 							'<div id="%tl#" class="%tl"></div>' +
223 							'<div id="%tc#" class="%tc"></div>' +
224 							'<div id="%tr#" class="%tr"></div>' +
225 							'<div id="%ml#" class="%ml"></div>' +
226 							'<div id="%mr#" class="%mr"></div>' +
227 							'<div id="%bl#" class="%bl"></div>' +
228 							'<div id="%bc#" class="%bc"></div>' +
229 							'<div id="%br#" class="%br"></div>' +
230 							'</td></tr>' +
231 						'</table>',
232 
233 						//Hide the container when loading skins, later restored by skin css.
234 						( CKEDITOR.env.ie ? '' : '<style>.cke_dialog{visibility:hidden;}</style>' ),
235 
236 					'</div>'
237 				].join( '' )
238 					.replace( /#/g, '_' + baseIdNumber )
239 					.replace( /%/g, 'cke_dialog_' ) );
240 
241 			var body = element.getChild( [ 0, 0, 0, 0, 0 ] ),
242 				title = body.getChild( 0 ),
243 				close = body.getChild( 1 );
244 
245 			// IFrame shim for dialog that masks activeX in IE. (#7619)
246 			if ( CKEDITOR.env.ie && !CKEDITOR.env.ie6Compat )
247 			{
248 				var isCustomDomain = CKEDITOR.env.isCustomDomain(),
249 					src = 'javascript:void(function(){' + encodeURIComponent( 'document.open();' + ( isCustomDomain ? ( 'document.domain="' + document.domain + '";' ) : '' ) + 'document.close();' ) + '}())',
250 					iframe = CKEDITOR.dom.element.createFromHtml( '<iframe' +
251   							' frameBorder="0"' +
252 							' class="cke_iframe_shim"' +
253   							' src="' + src + '"' +
254 							' tabIndex="-1"' +
255   							'></iframe>' );
256 				iframe.appendTo( body.getParent() );
257 			}
258 
259 			// Make the Title and Close Button unselectable.
260 			title.unselectable();
261 			close.unselectable();
262 
263 
264 			return {
265 				element : element,
266 				parts :
267 				{
268 					dialog		: element.getChild( 0 ),
269 					title		: title,
270 					close		: close,
271 					tabs		: body.getChild( 2 ),
272 					contents	: body.getChild( [ 3, 0, 0, 0 ] ),
273 					footer		: body.getChild( [ 3, 0, 1, 0 ] )
274 				}
275 			};
276 		},
277 
278 		destroy : function( editor )
279 		{
280 			var container = editor.container,
281 				element = editor.element;
282 
283 			if ( container )
284 			{
285 				container.clearCustomData();
286 				container.remove();
287 			}
288 
289 			if ( element )
290 			{
291 				element.clearCustomData();
292 				editor.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE && element.show();
293 				delete editor.element;
294 			}
295 		}
296 	};
297 })() );
298 
299 /**
300  * Returns the DOM element that represents a theme space. The default theme defines
301  * three spaces, namely "top", "contents" and "bottom", representing the main
302  * blocks that compose the editor interface.
303  * @param {String} spaceName The space name.
304  * @returns {CKEDITOR.dom.element} The element that represents the space.
305  * @example
306  * // Hide the bottom space in the UI.
307  * var bottom = editor.getThemeSpace( 'bottom' );
308  * bottom.setStyle( 'display', 'none' );
309  */
310 CKEDITOR.editor.prototype.getThemeSpace = function( spaceName )
311 {
312 	var spacePrefix = 'cke_' + spaceName;
313 	var space = this._[ spacePrefix ] ||
314 		( this._[ spacePrefix ] = CKEDITOR.document.getById( spacePrefix + '_' + this.name ) );
315 	return space;
316 };
317 
318 /**
319  * Resizes the editor interface.
320  * @param {Number|String} width The new width. It can be an pixels integer or a
321  *		CSS size value.
322  * @param {Number|String} height The new height. It can be an pixels integer or
323  *		a CSS size value.
324  * @param {Boolean} [isContentHeight] Indicates that the provided height is to
325  *		be applied to the editor contents space, not to the entire editor
326  *		interface. Defaults to false.
327  * @param {Boolean} [resizeInner] Indicates that the first inner interface
328  *		element must receive the size, not the outer element. The default theme
329  *		defines the interface inside a pair of span elements
330  *		(<span><span>...</span></span>). By default the
331  *		first span element receives the sizes. If this parameter is set to
332  *		true, the second span is sized instead.
333  * @example
334  * editor.resize( 900, 300 );
335  * @example
336  * editor.resize( '100%', 450, true );
337  */
338 CKEDITOR.editor.prototype.resize = function( width, height, isContentHeight, resizeInner )
339 {
340 	var container = this.container,
341 		contents = CKEDITOR.document.getById( 'cke_contents_' + this.name ),
342 		contentsFrame = CKEDITOR.env.webkit && this.document && this.document.getWindow().$.frameElement,
343 		outer = resizeInner ? container.getChild( 1 ) : container;
344 
345 	// Set as border box width. (#5353)
346 	outer.setSize( 'width',  width, true );
347 
348 	// WebKit needs to refresh the iframe size to avoid rendering issues. (1/2) (#8348)
349 	contentsFrame && ( contentsFrame.style.width = '1%' );
350 
351 	// Get the height delta between the outer table and the content area.
352 	// If we're setting the content area's height, then we don't need the delta.
353 	var delta = isContentHeight ? 0 : ( outer.$.offsetHeight || 0 ) - ( contents.$.clientHeight || 0 );
354 	contents.setStyle( 'height', Math.max( height - delta, 0 ) + 'px' );
355 
356 	// WebKit needs to refresh the iframe size to avoid rendering issues. (2/2) (#8348)
357 	contentsFrame && ( contentsFrame.style.width = '100%' );
358 
359 	// Emit a resize event.
360 	this.fire( 'resize' );
361 };
362 
363 /**
364  * Gets the element that can be freely used to check the editor size. This method
365  * is mainly used by the resize plugin, which adds a UI handle that can be used
366  * to resize the editor.
367  * @param {Boolean} forContents Whether to return the "contents" part of the theme instead of the container.
368  * @returns {CKEDITOR.dom.element} The resizable element.
369  * @example
370  */
371 CKEDITOR.editor.prototype.getResizable = function( forContents )
372 {
373 	return forContents ? CKEDITOR.document.getById( 'cke_contents_' + this.name ) : this.container;
374 };
375 
376 /**
377  * Makes it possible to place some of the editor UI blocks, like the toolbar
378  * and the elements path, into any element in the page.
379  * The elements used to hold the UI blocks can be shared among several editor
380  * instances. In that case, only the blocks of the active editor instance will
381  * display.
382  * @name CKEDITOR.config.sharedSpaces
383  * @type Object
384  * @default undefined
385  * @example
386  * // Place the toolbar inside the element with ID "someElementId" and the
387  * // elements path into the element with ID "anotherId".
388  * config.sharedSpaces =
389  * {
390  *     top : 'someElementId',
391  *     bottom : 'anotherId'
392  * };
393  * @example
394  * // Place the toolbar inside the element with ID "someElementId". The
395  * // elements path will remain attached to the editor UI.
396  * config.sharedSpaces =
397  * {
398  *     top : 'someElementId'
399  * };
400  */
401 
402 /**
403  * Fired after the editor instance is resized through
404  * the {@link CKEDITOR.editor.prototype.resize} method.
405  * @name CKEDITOR.editor#resize
406  * @event
407  */
408