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( 'menu',
  7 {
  8 	beforeInit : function( editor )
  9 	{
 10 		var groups = editor.config.menu_groups.split( ',' ),
 11 			groupsOrder = editor._.menuGroups = {},
 12 			menuItems = editor._.menuItems = {};
 13 
 14 		for ( var i = 0 ; i < groups.length ; i++ )
 15 			groupsOrder[ groups[ i ] ] = i + 1;
 16 
 17 		/**
 18 		 * Registers an item group to the editor context menu in order to make it
 19 		 * possible to associate it with menu items later.
 20 		 * @name CKEDITOR.editor.prototype.addMenuGroup
 21 		 * @param {String} name Specify a group name.
 22 		 * @param {Number} [order=100] Define the display sequence of this group
 23 		 *  	inside the menu. A smaller value gets displayed first.
 24 		 */
 25 		editor.addMenuGroup = function( name, order )
 26 			{
 27 				groupsOrder[ name ] = order || 100;
 28 			};
 29 
 30 		/**
 31 		 * Adds an item from the specified definition to the editor context menu.
 32 		 * @name CKEDITOR.editor.prototype.addMenuItem
 33 		 * @param {String} name The menu item name.
 34 		 * @param {CKEDITOR.menu.definition} definition The menu item definition.
 35 		 */
 36 		editor.addMenuItem = function( name, definition )
 37 			{
 38 				if ( groupsOrder[ definition.group ] )
 39 					menuItems[ name ] = new CKEDITOR.menuItem( this, name, definition );
 40 			};
 41 
 42 		/**
 43 		 * Adds one or more items from the specified definition array to the editor context menu.
 44 		 * @name CKEDITOR.editor.prototype.addMenuItems
 45 		 * @param {Array} definitions List of definitions for each menu item as if {@link CKEDITOR.editor.addMenuItem} is called.
 46 		 */
 47 		editor.addMenuItems = function( definitions )
 48 			{
 49 				for ( var itemName in definitions )
 50 				{
 51 					this.addMenuItem( itemName, definitions[ itemName ] );
 52 				}
 53 			};
 54 
 55 		/**
 56 		 * Retrieves a particular menu item definition from the editor context menu.
 57 		 * @name CKEDITOR.editor.prototype.getMenuItem
 58 		 * @param {String} name The name of the desired menu item.
 59 		 * @return {CKEDITOR.menu.definition}
 60 		 */
 61 		editor.getMenuItem = function( name )
 62 			{
 63 				return menuItems[ name ];
 64 			};
 65 
 66 		/**
 67 		 * Removes a particular menu item added before from the editor context menu.
 68 		 * @name CKEDITOR.editor.prototype.removeMenuItem
 69 		 * @param {String} name The name of the desired menu item.
 70 		 * @since 3.6.1
 71 		 */
 72 		editor.removeMenuItem = function( name )
 73 			{
 74 				delete menuItems[ name ];
 75 			};
 76 	},
 77 
 78 	requires : [ 'floatpanel' ]
 79 });
 80 
 81 (function()
 82 {
 83 	CKEDITOR.menu = CKEDITOR.tools.createClass(
 84 	{
 85 		$ : function( editor, definition )
 86 		{
 87 			definition = this._.definition = definition || {};
 88 			this.id = CKEDITOR.tools.getNextId();
 89 
 90 			this.editor = editor;
 91 			this.items = [];
 92 			this._.listeners = [];
 93 
 94 			this._.level = definition.level || 1;
 95 
 96 			var panelDefinition = CKEDITOR.tools.extend( {}, definition.panel,
 97 			{
 98 				css : editor.skin.editor.css,
 99 				level : this._.level - 1,
100 				block : {}
101 			} );
102 
103 			var attrs = panelDefinition.block.attributes = ( panelDefinition.attributes || {} );
104 			// Provide default role of 'menu'.
105 			!attrs.role && ( attrs.role = 'menu' );
106 			this._.panelDefinition = panelDefinition;
107 		},
108 
109 		_ :
110 		{
111 			onShow : function()
112 			{
113 				var selection = this.editor.getSelection();
114 
115 				// Selection will be unavailable after menu shows up
116 				// in IE, lock it now.
117 				if ( CKEDITOR.env.ie )
118 					selection && selection.lock();
119 
120 				var element = selection && selection.getStartElement(),
121 					listeners = this._.listeners,
122 					includedItems = [];
123 
124 				this.removeAll();
125 				// Call all listeners, filling the list of items to be displayed.
126 				for ( var i = 0 ; i < listeners.length ; i++ )
127 				{
128 					var listenerItems = listeners[ i ]( element, selection );
129 
130 					if ( listenerItems )
131 					{
132 						for ( var itemName in listenerItems )
133 						{
134 							var item = this.editor.getMenuItem( itemName );
135 
136 							if ( item && ( !item.command || this.editor.getCommand( item.command ).state ) )
137 							{
138 								item.state = listenerItems[ itemName ];
139 								this.add( item );
140 							}
141 						}
142 					}
143 				}
144 			},
145 
146 			onClick : function( item )
147 			{
148 				this.hide( false );
149 
150 				if ( item.onClick )
151 					item.onClick();
152 				else if ( item.command )
153 					this.editor.execCommand( item.command );
154 			},
155 
156 			onEscape : function( keystroke )
157 			{
158 				var parent = this.parent;
159 				// 1. If it's sub-menu, restore the last focused item
160 				// of upper level menu.
161 				// 2. In case of a top-menu, close it.
162 				if ( parent )
163 				{
164 					parent._.panel.hideChild();
165 					// Restore parent block item focus.
166 					var parentBlock = parent._.panel._.panel._.currentBlock,
167 						parentFocusIndex =  parentBlock._.focusIndex;
168 					parentBlock._.markItem( parentFocusIndex );
169 				}
170 				else if ( keystroke == 27 )
171 					this.hide();
172 
173 				return false;
174 			},
175 
176 			onHide : function()
177 			{
178 				// Unlock the selection upon first panel closing.
179 				if ( CKEDITOR.env.ie && !this.parent )
180 				{
181 					var selection = this.editor.getSelection();
182 					selection && selection.unlock( true );
183 				}
184 
185 				this.onHide && this.onHide();
186 			},
187 
188 			showSubMenu : function( index )
189 			{
190 				var menu = this._.subMenu,
191 					item = this.items[ index ],
192 					subItemDefs = item.getItems && item.getItems();
193 
194 				// If this item has no subitems, we just hide the submenu, if
195 				// available, and return back.
196 				if ( !subItemDefs )
197 				{
198 					this._.panel.hideChild();
199 					return;
200 				}
201 
202 				// Record parent menu focused item first (#3389).
203 				var block = this._.panel.getBlock( this.id );
204 				block._.focusIndex = index;
205 
206 				// Create the submenu, if not available, or clean the existing
207 				// one.
208 				if ( menu )
209 					menu.removeAll();
210 				else
211 				{
212 					menu = this._.subMenu = new CKEDITOR.menu( this.editor,
213 								   CKEDITOR.tools.extend( {}, this._.definition, { level : this._.level + 1 }, true ) );
214 					menu.parent = this;
215 					menu._.onClick = CKEDITOR.tools.bind( this._.onClick, this );
216 				}
217 
218 				// Add all submenu items to the menu.
219 				for ( var subItemName in subItemDefs )
220 				{
221 					var subItem = this.editor.getMenuItem( subItemName );
222 					if ( subItem )
223 					{
224 						subItem.state = subItemDefs[ subItemName ];
225 						menu.add( subItem );
226 					}
227 				}
228 
229 				// Get the element representing the current item.
230 				var element = this._.panel.getBlock( this.id ).element.getDocument().getById( this.id + String( index ) );
231 
232 				// Show the submenu.
233 				menu.show( element, 2 );
234 			}
235 		},
236 
237 		proto :
238 		{
239 			add : function( item )
240 			{
241 				// Later we may sort the items, but Array#sort is not stable in
242 				// some browsers, here we're forcing the original sequence with
243 				// 'order' attribute if it hasn't been assigned. (#3868)
244 				if ( !item.order )
245 					item.order = this.items.length;
246 
247 				this.items.push( item );
248 			},
249 
250 			removeAll : function()
251 			{
252 				this.items = [];
253 			},
254 
255 			show : function( offsetParent, corner, offsetX, offsetY )
256 			{
257 				// Not for sub menu.
258 				if ( !this.parent )
259 				{
260 					this._.onShow();
261 					// Don't menu with zero items.
262 					if ( ! this.items.length )
263 						return;
264 				}
265 
266 				corner = corner || ( this.editor.lang.dir == 'rtl' ? 2 : 1 );
267 
268 				var items = this.items,
269 					editor = this.editor,
270 					panel = this._.panel,
271 					element = this._.element;
272 
273 				// Create the floating panel for this menu.
274 				if ( !panel )
275 				{
276 					panel = this._.panel = new CKEDITOR.ui.floatPanel( this.editor,
277 						CKEDITOR.document.getBody(),
278 						this._.panelDefinition,
279 						this._.level );
280 
281 					panel.onEscape = CKEDITOR.tools.bind( function( keystroke )
282 					{
283 						if ( this._.onEscape( keystroke ) === false )
284 							return false;
285 					},
286 					this );
287 
288 					panel.onHide = CKEDITOR.tools.bind( function()
289 					{
290 						this._.onHide && this._.onHide();
291 					},
292 					this );
293 
294 					// Create an autosize block inside the panel.
295 					var block = panel.addBlock( this.id, this._.panelDefinition.block );
296 					block.autoSize = true;
297 
298 					var keys = block.keys;
299 					keys[ 40 ]	= 'next';					// ARROW-DOWN
300 					keys[ 9 ]	= 'next';					// TAB
301 					keys[ 38 ]	= 'prev';					// ARROW-UP
302 					keys[ CKEDITOR.SHIFT + 9 ]	= 'prev';	// SHIFT + TAB
303 					keys[ ( editor.lang.dir == 'rtl' ? 37 : 39 ) ]= CKEDITOR.env.ie ? 'mouseup' : 'click';  // ARROW-RIGHT/ARROW-LEFT(rtl)
304 					keys[ 32 ]	= CKEDITOR.env.ie ? 'mouseup' : 'click';					// SPACE
305 					CKEDITOR.env.ie && ( keys[ 13 ] = 'mouseup' );		// Manage ENTER, since onclick is blocked in IE (#8041).
306 
307 					element = this._.element = block.element;
308 					element.addClass( editor.skinClass );
309 
310 					var elementDoc = element.getDocument();
311 					elementDoc.getBody().setStyle( 'overflow', 'hidden' );
312 					elementDoc.getElementsByTag( 'html' ).getItem( 0 ).setStyle( 'overflow', 'hidden' );
313 
314 					this._.itemOverFn = CKEDITOR.tools.addFunction( function( index )
315 						{
316 							clearTimeout( this._.showSubTimeout );
317 							this._.showSubTimeout = CKEDITOR.tools.setTimeout( this._.showSubMenu, editor.config.menu_subMenuDelay || 400, this, [ index ] );
318 						},
319 						this );
320 
321 					this._.itemOutFn = CKEDITOR.tools.addFunction( function( index )
322 						{
323 							clearTimeout( this._.showSubTimeout );
324 						},
325 						this );
326 
327 					this._.itemClickFn = CKEDITOR.tools.addFunction( function( index )
328 						{
329 							var item = this.items[ index ];
330 
331 							if ( item.state == CKEDITOR.TRISTATE_DISABLED )
332 							{
333 								this.hide();
334 								return;
335 							}
336 
337 							if ( item.getItems )
338 								this._.showSubMenu( index );
339 							else
340 								this._.onClick( item );
341 						},
342 						this );
343 				}
344 
345 				// Put the items in the right order.
346 				sortItems( items );
347 
348 				var chromeRoot = editor.container.getChild( 1 ),
349 					mixedContentClass = chromeRoot.hasClass( 'cke_mixed_dir_content' ) ? ' cke_mixed_dir_content' : '';
350 
351 				// Build the HTML that composes the menu and its items.
352 				var output = [ '<div class="cke_menu' + mixedContentClass + '" role="presentation">' ];
353 
354 				var length = items.length,
355 					lastGroup = length && items[ 0 ].group;
356 
357 				for ( var i = 0 ; i < length ; i++ )
358 				{
359 					var item = items[ i ];
360 					if ( lastGroup != item.group )
361 					{
362 						output.push( '<div class="cke_menuseparator" role="separator"></div>' );
363 						lastGroup = item.group;
364 					}
365 
366 					item.render( this, i, output );
367 				}
368 
369 				output.push( '</div>' );
370 
371 				// Inject the HTML inside the panel.
372 				element.setHtml( output.join( '' ) );
373 
374 				CKEDITOR.ui.fire( 'ready', this );
375 
376 				// Show the panel.
377 				if ( this.parent )
378 					this.parent._.panel.showAsChild( panel, this.id, offsetParent, corner, offsetX, offsetY );
379 				else
380 					panel.showBlock( this.id, offsetParent, corner, offsetX, offsetY );
381 
382 				editor.fire( 'menuShow', [ panel ] );
383 			},
384 
385 			addListener : function( listenerFn )
386 			{
387 				this._.listeners.push( listenerFn );
388 			},
389 
390 			hide : function( returnFocus )
391 			{
392 				this._.onHide && this._.onHide();
393 				this._.panel && this._.panel.hide( returnFocus );
394 			}
395 		}
396 	});
397 
398 	function sortItems( items )
399 	{
400 		items.sort( function( itemA, itemB )
401 			{
402 				if ( itemA.group < itemB.group )
403 					return -1;
404 				else if ( itemA.group > itemB.group )
405 					return 1;
406 
407 				return itemA.order < itemB.order ? -1 :
408 					itemA.order > itemB.order ? 1 :
409 					0;
410 			});
411 	}
412 	CKEDITOR.menuItem = CKEDITOR.tools.createClass(
413 	{
414 		$ : function( editor, name, definition )
415 		{
416 			CKEDITOR.tools.extend( this, definition,
417 				// Defaults
418 				{
419 					order : 0,
420 					className : 'cke_button_' + name
421 				});
422 
423 			// Transform the group name into its order number.
424 			this.group = editor._.menuGroups[ this.group ];
425 
426 			this.editor = editor;
427 			this.name = name;
428 		},
429 
430 		proto :
431 		{
432 			render : function( menu, index, output )
433 			{
434 				var id = menu.id + String( index ),
435 					state = ( typeof this.state == 'undefined' ) ? CKEDITOR.TRISTATE_OFF : this.state;
436 
437 				var classes = ' cke_' + (
438 					state == CKEDITOR.TRISTATE_ON ? 'on' :
439 					state == CKEDITOR.TRISTATE_DISABLED ? 'disabled' :
440 					'off' );
441 
442 				var htmlLabel = this.label;
443 
444 				if ( this.className )
445 					classes += ' ' + this.className;
446 
447 			var hasSubMenu = this.getItems;
448 
449 			output.push(
450 				'<span class="cke_menuitem' + ( this.icon && this.icon.indexOf( '.png' ) == -1 ? ' cke_noalphafix' : '' ) + '">' +
451 				'<a id="', id, '"' +
452 					' class="', classes, '" href="javascript:void(\'', ( this.label || '' ).replace( "'", '' ), '\')"' +
453 					' title="', this.label, '"' +
454 					' tabindex="-1"' +
455 					'_cke_focus=1' +
456 					' hidefocus="true"' +
457 					' role="menuitem"' +
458 					( hasSubMenu ? 'aria-haspopup="true"' : '' ) +
459 					( state == CKEDITOR.TRISTATE_DISABLED ? 'aria-disabled="true"' : '' ) +
460 					( state == CKEDITOR.TRISTATE_ON ? 'aria-pressed="true"' : '' ) );
461 
462 				// Some browsers don't cancel key events in the keydown but in the
463 				// keypress.
464 				// TODO: Check if really needed for Gecko+Mac.
465 				if ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) )
466 				{
467 					output.push(
468 						' onkeypress="return false;"' );
469 				}
470 
471 				// With Firefox, we need to force the button to redraw, otherwise it
472 				// will remain in the focus state.
473 				if ( CKEDITOR.env.gecko )
474 				{
475 					output.push(
476 						' onblur="this.style.cssText = this.style.cssText;"' );
477 				}
478 
479 				var offset = ( this.iconOffset || 0 ) * -16;
480 				output.push(
481 //					' onkeydown="return CKEDITOR.ui.button._.keydown(', index, ', event);"' +
482 					' onmouseover="CKEDITOR.tools.callFunction(', menu._.itemOverFn, ',', index, ');"' +
483 					' onmouseout="CKEDITOR.tools.callFunction(', menu._.itemOutFn, ',', index, ');" ' +
484 					( CKEDITOR.env.ie ? 'onclick="return false;" onmouseup' : 'onclick' ) +		// #188
485 						'="CKEDITOR.tools.callFunction(', menu._.itemClickFn, ',', index, '); return false;"' +
486 					'>' +
487 						'<span class="cke_icon_wrapper"><span class="cke_icon"' +
488 							( this.icon ? ' style="background-image:url(' + CKEDITOR.getUrl( this.icon ) + ');background-position:0 ' + offset + 'px;"'
489 							: '' ) +
490 							'></span></span>' +
491 						'<span class="cke_label">' );
492 
493 			if ( hasSubMenu )
494 			{
495 				output.push(
496 							'<span class="cke_menuarrow">',
497 								'<span>&#',
498 									( this.editor.lang.dir == 'rtl' ?
499 										'9668' :	// BLACK LEFT-POINTING POINTER
500 										'9658' ),	// BLACK RIGHT-POINTING POINTER
501 								';</span>',
502 							'</span>' );
503 			}
504 
505 				output.push(
506 								htmlLabel,
507 							'</span>' +
508 					'</a>' +
509 					'</span>' );
510 		}
511 		}
512 	});
513 
514 })();
515 
516 
517 /**
518  * The amount of time, in milliseconds, the editor waits before displaying submenu
519  * options when moving the mouse over options that contain submenus, like the
520  * "Cell Properties" entry for tables.
521  * @type Number
522  * @default 400
523  * @example
524  * // Remove the submenu delay.
525  * config.menu_subMenuDelay = 0;
526  */
527 
528 /**
529  * A comma separated list of items group names to be displayed in the context
530  * menu. The order of items will reflect the order specified in this list if
531  * no priority was defined in the groups.
532  * @type String
533  * @default 'clipboard,form,tablecell,tablecellproperties,tablerow,tablecolumn,table,anchor,link,image,flash,checkbox,radio,textfield,hiddenfield,imagebutton,button,select,textarea'
534  * @example
535  * config.menu_groups = 'clipboard,table,anchor,link,image';
536  */
537 CKEDITOR.config.menu_groups =
538 	'clipboard,' +
539 	'form,' +
540 	'tablecell,tablecellproperties,tablerow,tablecolumn,table,'+
541 	'anchor,link,image,flash,' +
542 	'checkbox,radio,textfield,hiddenfield,imagebutton,button,select,textarea,div';
543