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 CKEDITOR.plugins.add( 'panel',
  7 {
  8 	beforeInit : function( editor )
  9 	{
 10 		editor.ui.addHandler( CKEDITOR.UI_PANEL, CKEDITOR.ui.panel.handler );
 11 	}
 12 });
 13 
 14 /**
 15  * Panel UI element.
 16  * @constant
 17  * @example
 18  */
 19 CKEDITOR.UI_PANEL = 'panel';
 20 
 21 CKEDITOR.ui.panel = function( document, definition )
 22 {
 23 	// Copy all definition properties to this object.
 24 	if ( definition )
 25 		CKEDITOR.tools.extend( this, definition );
 26 
 27 	// Set defaults.
 28 	CKEDITOR.tools.extend( this,
 29 		{
 30 			className : '',
 31 			css : []
 32 		});
 33 
 34 	this.id = CKEDITOR.tools.getNextId();
 35 	this.document = document;
 36 
 37 	this._ =
 38 	{
 39 		blocks : {}
 40 	};
 41 };
 42 
 43 /**
 44  * Transforms a rich combo definition in a {@link CKEDITOR.ui.richCombo}
 45  * instance.
 46  * @type Object
 47  * @example
 48  */
 49 CKEDITOR.ui.panel.handler =
 50 {
 51 	create : function( definition )
 52 	{
 53 		return new CKEDITOR.ui.panel( definition );
 54 	}
 55 };
 56 
 57 CKEDITOR.ui.panel.prototype =
 58 {
 59 	renderHtml : function( editor )
 60 	{
 61 		var output = [];
 62 		this.render( editor, output );
 63 		return output.join( '' );
 64 	},
 65 
 66 	/**
 67 	 * Renders the combo.
 68 	 * @param {CKEDITOR.editor} editor The editor instance which this button is
 69 	 *		to be used by.
 70 	 * @param {Array} output The output array to which append the HTML relative
 71 	 *		to this button.
 72 	 * @example
 73 	 */
 74 	render : function( editor, output )
 75 	{
 76 		var id = this.id;
 77 
 78 		output.push(
 79 			'<div class="', editor.skinClass ,'"' +
 80 				' lang="', editor.langCode, '"' +
 81 				' role="presentation"' +
 82 				// iframe loading need sometime, keep the panel hidden(#4186).
 83 				' style="display:none;z-index:' + ( editor.config.baseFloatZIndex + 1 ) + '">' +
 84 				'<div' +
 85 					' id=', id,
 86 					' dir=', editor.lang.dir,
 87 					' role="presentation"' +
 88 					' class="cke_panel cke_', editor.lang.dir );
 89 
 90 		if ( this.className )
 91 			output.push( ' ', this.className );
 92 
 93 		output.push(
 94 				'">' );
 95 
 96 		if ( this.forceIFrame || this.css.length )
 97 		{
 98 			output.push(
 99 						'<iframe id="', id, '_frame"' +
100 							' frameborder="0"' +
101 							' role="application" src="javascript:void(' );
102 
103 			output.push(
104 							// Support for custom document.domain in IE.
105 							CKEDITOR.env.isCustomDomain() ?
106 								'(function(){' +
107 									'document.open();' +
108 									'document.domain=\'' + document.domain + '\';' +
109 									'document.close();' +
110 								'})()'
111 							:
112 								'0' );
113 
114 			output.push(
115 						')"></iframe>' );
116 		}
117 
118 		output.push(
119 				'</div>' +
120 			'</div>' );
121 
122 		return id;
123 	},
124 
125 	getHolderElement : function()
126 	{
127 		var holder = this._.holder;
128 
129 		if ( !holder )
130 		{
131 			if ( this.forceIFrame || this.css.length )
132 			{
133 				var iframe = this.document.getById( this.id + '_frame' ),
134 					parentDiv = iframe.getParent(),
135 					dir = parentDiv.getAttribute( 'dir' ),
136 					className = parentDiv.getParent().getAttribute( 'class' ),
137 					langCode = parentDiv.getParent().getAttribute( 'lang' ),
138 					doc = iframe.getFrameDocument();
139 
140 				// Make it scrollable on iOS. (#8308)
141 				CKEDITOR.env.iOS && parentDiv.setStyles(
142 					{
143 						'overflow' : 'scroll',
144 						'-webkit-overflow-scrolling' : 'touch'
145 					});
146 
147 				var onLoad = CKEDITOR.tools.addFunction( CKEDITOR.tools.bind( function( ev )
148 					{
149 						this.isLoaded = true;
150 						if ( this.onLoad )
151 							this.onLoad();
152 					}, this ) );
153 
154 				var data =
155 					'<!DOCTYPE html>' +
156 					'<html dir="' + dir + '" class="' + className + '_container" lang="' + langCode + '">' +
157 						'<head>' +
158 							'<style>.' + className + '_container{visibility:hidden}</style>' +
159 							CKEDITOR.tools.buildStyleHtml( this.css ) +
160 						'</head>' +
161 						'<body class="cke_' + dir + ' cke_panel_frame ' + CKEDITOR.env.cssClass + '" style="margin:0;padding:0"' +
162 						' onload="( window.CKEDITOR || window.parent.CKEDITOR ).tools.callFunction(' + onLoad + ');"></body>' +
163 					'<\/html>';
164 
165 				doc.write( data );
166 
167 				var win = doc.getWindow();
168 
169 				// Register the CKEDITOR global.
170 				win.$.CKEDITOR = CKEDITOR;
171 
172 				// Arrow keys for scrolling is only preventable with 'keypress' event in Opera (#4534).
173 				doc.on( 'key' + ( CKEDITOR.env.opera? 'press':'down' ), function( evt )
174 					{
175 						var keystroke = evt.data.getKeystroke(),
176 							dir = this.document.getById( this.id ).getAttribute( 'dir' );
177 
178 						// Delegate key processing to block.
179 						if ( this._.onKeyDown && this._.onKeyDown( keystroke ) === false )
180 						{
181 							evt.data.preventDefault();
182 							return;
183 						}
184 
185 						// ESC/ARROW-LEFT(ltr) OR ARROW-RIGHT(rtl)
186 						if ( keystroke == 27 || keystroke == ( dir == 'rtl' ? 39 : 37 ) )
187 						{
188 							if ( this.onEscape && this.onEscape( keystroke ) === false )
189 								evt.data.preventDefault();
190 						}
191 					},
192 					this );
193 
194 				holder = doc.getBody();
195 				holder.unselectable();
196 				CKEDITOR.env.air && CKEDITOR.tools.callFunction( onLoad );
197 			}
198 			else
199 				holder = this.document.getById( this.id );
200 
201 			this._.holder = holder;
202 		}
203 
204 		return holder;
205 	},
206 
207 	addBlock : function( name, block )
208 	{
209 		block = this._.blocks[ name ] = block instanceof CKEDITOR.ui.panel.block ?  block
210 				: new CKEDITOR.ui.panel.block( this.getHolderElement(), block );
211 
212 		if ( !this._.currentBlock )
213 			this.showBlock( name );
214 
215 		return block;
216 	},
217 
218 	getBlock : function( name )
219 	{
220 		return this._.blocks[ name ];
221 	},
222 
223 	showBlock : function( name )
224 	{
225 		var blocks = this._.blocks,
226 			block = blocks[ name ],
227 			current = this._.currentBlock;
228 
229 		// ARIA role works better in IE on the body element, while on the iframe
230 		// for FF. (#8864)
231 		var holder = !this.forceIFrame || CKEDITOR.env.ie ?
232 				 this._.holder : this.document.getById( this.id + '_frame' );
233 
234 		if ( current )
235 		{
236 			// Clean up the current block's effects on holder.
237 			holder.removeAttributes( current.attributes );
238 			current.hide();
239 		}
240 
241 		this._.currentBlock = block;
242 
243 		holder.setAttributes( block.attributes );
244 		CKEDITOR.fire( 'ariaWidget', holder );
245 
246 		// Reset the focus index, so it will always go into the first one.
247 		block._.focusIndex = -1;
248 
249 		this._.onKeyDown = block.onKeyDown && CKEDITOR.tools.bind( block.onKeyDown, block );
250 
251 		block.show();
252 
253 		return block;
254 	},
255 
256 	destroy : function()
257 	{
258 		this.element && this.element.remove();
259 	}
260 };
261 
262 CKEDITOR.ui.panel.block = CKEDITOR.tools.createClass(
263 {
264 	$ : function( blockHolder, blockDefinition )
265 	{
266 		this.element = blockHolder.append(
267 			blockHolder.getDocument().createElement( 'div',
268 				{
269 					attributes :
270 					{
271 						'tabIndex' : -1,
272 						'class' : 'cke_panel_block',
273 						'role' : 'presentation'
274 					},
275 					styles :
276 					{
277 						display : 'none'
278 					}
279 				}) );
280 
281 		// Copy all definition properties to this object.
282 		if ( blockDefinition )
283 			CKEDITOR.tools.extend( this, blockDefinition );
284 
285 		if ( !this.attributes.title )
286 			this.attributes.title = this.attributes[ 'aria-label' ];
287 
288 		this.keys = {};
289 
290 		this._.focusIndex = -1;
291 
292 		// Disable context menu for panels.
293 		this.element.disableContextMenu();
294 	},
295 
296 	_ : {
297 
298 		/**
299 		 * Mark the item specified by the index as current activated.
300 		 */
301 		markItem: function( index )
302 		{
303 			if ( index == -1 )
304 				return;
305 			var links = this.element.getElementsByTag( 'a' );
306 			var item = links.getItem( this._.focusIndex = index );
307 
308 			// Safari need focus on the iframe window first(#3389), but we need
309 			// lock the blur to avoid hiding the panel.
310 			if ( CKEDITOR.env.webkit || CKEDITOR.env.opera )
311 				item.getDocument().getWindow().focus();
312 			item.focus();
313 
314 			this.onMark && this.onMark( item );
315 		}
316 	},
317 
318 	proto :
319 	{
320 		show : function()
321 		{
322 			this.element.setStyle( 'display', '' );
323 		},
324 
325 		hide : function()
326 		{
327 			if ( !this.onHide || this.onHide.call( this )  !== true )
328 				this.element.setStyle( 'display', 'none' );
329 		},
330 
331 		onKeyDown : function( keystroke )
332 		{
333 			var keyAction = this.keys[ keystroke ];
334 			switch ( keyAction )
335 			{
336 				// Move forward.
337 				case 'next' :
338 					var index = this._.focusIndex,
339 						links = this.element.getElementsByTag( 'a' ),
340 						link;
341 
342 					while ( ( link = links.getItem( ++index ) ) )
343 					{
344 						// Move the focus only if the element is marked with
345 						// the _cke_focus and it it's visible (check if it has
346 						// width).
347 						if ( link.getAttribute( '_cke_focus' ) && link.$.offsetWidth )
348 						{
349 							this._.focusIndex = index;
350 							link.focus();
351 							break;
352 						}
353 					}
354 					return false;
355 
356 				// Move backward.
357 				case 'prev' :
358 					index = this._.focusIndex;
359 					links = this.element.getElementsByTag( 'a' );
360 
361 					while ( index > 0 && ( link = links.getItem( --index ) ) )
362 					{
363 						// Move the focus only if the element is marked with
364 						// the _cke_focus and it it's visible (check if it has
365 						// width).
366 						if ( link.getAttribute( '_cke_focus' ) && link.$.offsetWidth )
367 						{
368 							this._.focusIndex = index;
369 							link.focus();
370 							break;
371 						}
372 					}
373 					return false;
374 
375 				case 'click' :
376 				case 'mouseup' :
377 					index = this._.focusIndex;
378 					link = index >= 0 && this.element.getElementsByTag( 'a' ).getItem( index );
379 
380 					if ( link )
381 						link.$[ keyAction ] ? link.$[ keyAction ]() : link.$[ 'on' + keyAction ]();
382 
383 					return false;
384 			}
385 
386 			return true;
387 		}
388 	}
389 });
390 
391 /**
392  * Fired when a panel is added to the document
393  * @name CKEDITOR#ariaWidget
394  * @event
395  * @param {Object} holder The element wrapping the panel
396  */
397