1 /*
  2 Copyright (c) 2003-2009, 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 = {};
 12
 13 		for ( var i = 0 ; i < groups.length ; i++ )
 14 			groupsOrder[ groups[ i ] ] = i + 1;
 15
 16 		editor._.menuGroups = groupsOrder;
 17 		editor._.menuItems = {};
 18 	},
 19
 20 	requires : [ 'floatpanel' ]
 21 });
 22
 23 CKEDITOR.tools.extend( CKEDITOR.editor.prototype,
 24 {
 25 	addMenuGroup : function( name, order )
 26 	{
 27 		this._.menuGroups[ name ] = order || 100;
 28 	},
 29
 30 	addMenuItem : function( name, definition )
 31 	{
 32 		if ( this._.menuGroups[ definition.group ] )
 33 			this._.menuItems[ name ] = new CKEDITOR.menuItem( this, name, definition );
 34 	},
 35
 36 	addMenuItems : function( definitions )
 37 	{
 38 		for ( var itemName in definitions )
 39 		{
 40 			this.addMenuItem( itemName, definitions[ itemName ] );
 41 		}
 42 	},
 43
 44 	getMenuItem : function( name )
 45 	{
 46 		return this._.menuItems[ name ];
 47 	}
 48 });
 49
 50 (function()
 51 {
 52 	CKEDITOR.menu = CKEDITOR.tools.createClass(
 53 	{
 54 		$ : function( editor, level )
 55 		{
 56 			this.id = 'cke_' + CKEDITOR.tools.getNextNumber();
 57
 58 			this.editor = editor;
 59 			this.items = [];
 60
 61 			this._.level = level || 1;
 62 		},
 63
 64 		_ :
 65 		{
 66 			showSubMenu : function( index )
 67 			{
 68 				var menu = this._.subMenu,
 69 					item = this.items[ index ],
 70 					subItems = item.getItems && item.getItems();
 71
 72 				// If this item has no subitems, we just hide the submenu, if
 73 				// available, and return back.
 74 				if ( !subItems )
 75 				{
 76 					this._.panel.hideChild();
 77 					return;
 78 				}
 79
 80 				// Create the submenu, if not available, or clean the existing
 81 				// one.
 82 				if ( menu )
 83 					menu.removeAll();
 84 				else
 85 				{
 86 					menu = this._.subMenu = new CKEDITOR.menu( this.editor, this._.level + 1 );
 87 					menu.parent = this;
 88 					menu.onClick = CKEDITOR.tools.bind( this.onClick, this );
 89 				}
 90
 91 				// Add all submenu items to the menu.
 92 				for ( var itemName in subItems )
 93 				{
 94 					menu.add( this.editor.getMenuItem( itemName ) );
 95 				}
 96
 97 				// Get the element representing the current item.
 98 				var element = this._.panel.getBlock( this.id ).element.getDocument().getById( this.id + String( index ) );
 99
100 				// Show the submenu.
101 				menu.show( element, 2 );
102 			}
103 		},
104
105 		proto :
106 		{
107 			add : function( item )
108 			{
109 				this.items.push( item );
110 			},
111
112 			removeAll : function()
113 			{
114 				this.items = [];
115 			},
116
117 			show : function( offsetParent, corner, offsetX, offsetY )
118 			{
119 				var items = this.items,
120 					editor = this.editor,
121 					panel = this._.panel,
122 					element = this._.element;
123
124 				// Create the floating panel for this menu.
125 				if ( !panel )
126 				{
127 					panel = this._.panel = new CKEDITOR.ui.floatPanel( this.editor, CKEDITOR.document.getBody(),
128 						{
129 							css : [ CKEDITOR.getUrl( editor.skinPath + 'editor.css' ) ],
130 							level : this._.level - 1,
131 							className : editor.skinClass + ' cke_contextmenu'
132 						},
133 						this._.level);
134
135 					panel.onEscape = CKEDITOR.tools.bind( function()
136 					{
137 						this.onEscape && this.onEscape();
138 						this.hide();
139 					},
140 					this );
141
142 					panel.onHide = CKEDITOR.tools.bind( function()
143 					{
144 						this.onHide && this.onHide();
145 					},
146 					this );
147
148 					// Create an autosize block inside the panel.
149 					var block = panel.addBlock( this.id );
150 					block.autoSize = true;
151
152 					var keys = block.keys;
153 					keys[ 40 ]	= 'next';					// ARROW-DOWN
154 					keys[ 9 ]	= 'next';					// TAB
155 					keys[ 38 ]	= 'prev';					// ARROW-UP
156 					keys[ CKEDITOR.SHIFT + 9 ]	= 'prev';	// SHIFT + TAB
157 					keys[ 32 ]	= 'click';					// SPACE
158 					keys[ 39 ]	= 'click';					// ARROW-RIGHT
159
160 					element = this._.element = block.element;
161 					element.addClass( editor.skinClass );
162
163 					var elementDoc = element.getDocument();
164 					elementDoc.getBody().setStyle( 'overflow', 'hidden' );
165 					elementDoc.getElementsByTag( 'html' ).getItem( 0 ).setStyle( 'overflow', 'hidden' );
166
167 					this._.itemOverFn = CKEDITOR.tools.addFunction( function( index )
168 						{
169 							clearTimeout( this._.showSubTimeout );
170 							this._.showSubTimeout = CKEDITOR.tools.setTimeout( this._.showSubMenu, editor.config.menu_subMenuDelay, this, [ index ] );
171 						},
172 						this);
173
174 					this._.itemOutFn = CKEDITOR.tools.addFunction( function( index )
175 						{
176 							clearTimeout( this._.showSubTimeout );
177 						},
178 						this);
179
180 					this._.itemClickFn = CKEDITOR.tools.addFunction( function( index )
181 						{
182 							var item = this.items[ index ];
183
184 							if ( item.state == CKEDITOR.TRISTATE_DISABLED )
185 							{
186 								this.hide();
187 								return;
188 							}
189
190 							if ( item.getItems )
191 								this._.showSubMenu( index );
192 							else
193 								this.onClick && this.onClick( item );
194 						},
195 						this);
196 				}
197
198 				// Put the items in the right order.
199 				sortItems( items );
200
201 				// Build the HTML that composes the menu and its items.
202 				var output = [ '<div class="cke_menu">' ];
203
204 				var length = items.length,
205 					lastGroup = length && items[ 0 ].group;
206
207 				for ( var i = 0 ; i < length ; i++ )
208 				{
209 					var item = items[ i ];
210 					if ( lastGroup != item.group )
211 					{
212 						output.push( '<div class="cke_menuseparator"></div>' );
213 						lastGroup = item.group;
214 					}
215
216 					item.render( this, i, output );
217 				}
218
219 				output.push( '</div>' );
220
221 				// Inject the HTML inside the panel.
222 				element.setHtml( output.join( '' ) );
223
224 				// Show the panel.
225 				if ( this.parent )
226 					this.parent._.panel.showAsChild( panel, this.id, offsetParent, corner, offsetX, offsetY );
227 				else
228 					panel.showBlock( this.id, offsetParent, corner, offsetX, offsetY );
229 			},
230
231 			hide : function()
232 			{
233 				this._.panel && this._.panel.hide();
234 			}
235 		}
236 	});
237
238 	function sortItems( items )
239 	{
240 		items.sort( function( itemA, itemB )
241 			{
242 				if ( itemA.group < itemB.group )
243 					return -1;
244 				else if ( itemA.group > itemB.group )
245 					return 1;
246
247 				return itemA.order < itemB.order ? -1 :
248 					itemA.order > itemB.order ? 1 :
249 					0;
250 			});
251 	}
252 })();
253
254 CKEDITOR.menuItem = CKEDITOR.tools.createClass(
255 {
256 	$ : function( editor, name, definition )
257 	{
258 		CKEDITOR.tools.extend( this, definition,
259 			// Defaults
260 			{
261 				order : 0,
262 				className : 'cke_button_' + name
263 			});
264
265 		// Transform the group name into its order number.
266 		this.group = editor._.menuGroups[ this.group ];
267
268 		this.editor = editor;
269 		this.name = name;
270 	},
271
272 	proto :
273 	{
274 		render : function( menu, index, output )
275 		{
276 			var id = menu.id + String( index ),
277 				state = ( typeof this.state == 'undefined' ) ? CKEDITOR.TRISTATE_OFF : this.state;
278
279 			var classes = ' cke_' + (
280 				state == CKEDITOR.TRISTATE_ON ? 'on' :
281 				state == CKEDITOR.TRISTATE_DISABLED ? 'disabled' :
282 				'off' );
283
284 			var htmlLabel = this.label;
285 			if ( state == CKEDITOR.TRISTATE_DISABLED )
286 				htmlLabel = this.editor.lang.common.unavailable.replace( '%1', htmlLabel );
287
288 			if ( this.className )
289 				classes += ' ' + this.className;
290
291 			output.push(
292 				'<span class="cke_menuitem">' +
293 				'<a id="', id, '"' +
294 					' class="', classes, '" href="javascript:void(\'', ( this.label || '' ).replace( "'", '' ), '\')"' +
295 					' title="', this.label, '"' +
296 					' tabindex="-1"' +
297 					'_cke_focus=1' +
298 					' hidefocus="true"' );
299
300 			// Some browsers don't cancel key events in the keydown but in the
301 			// keypress.
302 			// TODO: Check if really needed for Gecko+Mac.
303 			if ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) )
304 			{
305 				output.push(
306 					' onkeypress="return false;"' );
307 			}
308
309 			// With Firefox, we need to force the button to redraw, otherwise it
310 			// will remain in the focus state.
311 			if ( CKEDITOR.env.gecko )
312 			{
313 				output.push(
314 					' onblur="this.style.cssText = this.style.cssText;"' );
315 			}
316
317 			var offset = ( this.iconOffset || 0 ) * -16;
318 			output.push(
319 //					' onkeydown="return CKEDITOR.ui.button._.keydown(', index, ', event);"' +
320 					' onmouseover="CKEDITOR.tools.callFunction(', menu._.itemOverFn, ',', index, ');"' +
321 					' onmouseout="CKEDITOR.tools.callFunction(', menu._.itemOutFn, ',', index, ');"' +
322 					' onclick="CKEDITOR.tools.callFunction(', menu._.itemClickFn, ',', index, '); return false;"' +
323 					'>' +
324 						'<span class="cke_icon_wrapper"><span class="cke_icon"' +
325 							( this.icon ? ' style="background-image:url(' + CKEDITOR.getUrl( this.icon ) + ');background-position:0 ' + offset + 'px;"></span>'
326 							: '' ) +
327 							'></span></span>' +
328 						'<span class="cke_label">' );
329
330 			if ( this.getItems )
331 			{
332 				output.push(
333 							'<span class="cke_menuarrow"></span>' );
334 			}
335
336 			output.push(
337 							htmlLabel,
338 						'</span>' +
339 				'</a>' +
340 				'</span>' );
341 		}
342 	}
343 });
344
345 CKEDITOR.config.menu_subMenuDelay = 400;
346 CKEDITOR.config.menu_groups =
347 	'clipboard,' +
348 	'form,' +
349 	'tablecell,tablecellproperties,tablerow,tablecolumn,table,'+
350 	'anchor,link,image,flash,' +
351 	'checkbox,radio,textfield,hiddenfield,imagebutton,button,select,textarea';
352