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