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