/** * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or http://ckeditor.com/license */ ( function() { CKEDITOR.plugins.add( 'panel', { beforeInit: function( editor ) { editor.ui.addHandler( CKEDITOR.UI_PANEL, CKEDITOR.ui.panel.handler ); } } ); /** * Panel UI element. * * @readonly * @property {String} [='panel'] * @member CKEDITOR */ CKEDITOR.UI_PANEL = 'panel'; /** * @class * @constructor Creates a panel class instance. * @param {CKEDITOR.dom.document} document * @param {Object} definition */ CKEDITOR.ui.panel = function( document, definition ) { // Copy all definition properties to this object. if ( definition ) CKEDITOR.tools.extend( this, definition ); // Set defaults. CKEDITOR.tools.extend( this, { className: '', css: [] } ); this.id = CKEDITOR.tools.getNextId(); this.document = document; this.isFramed = this.forceIFrame || this.css.length; this._ = { blocks: {} }; }; /** * Represents panel handler object. * * @class * @singleton * @extends CKEDITOR.ui.handlerDefinition */ CKEDITOR.ui.panel.handler = { /** * Transforms a panel definition in a {@link CKEDITOR.ui.panel} instance. * * @param {Object} definition * @returns {CKEDITOR.ui.panel} */ create: function( definition ) { return new CKEDITOR.ui.panel( definition ); } }; var panelTpl = CKEDITOR.addTemplate( 'panel', '' ); var frameTpl = CKEDITOR.addTemplate( 'panel-frame', '' ); var frameDocTpl = CKEDITOR.addTemplate( 'panel-frame-inner', '' + '' + '{css}' + '' + '<\/html>' ); /** @class CKEDITOR.ui.panel */ CKEDITOR.ui.panel.prototype = { /** * Renders the combo. * * @param {CKEDITOR.editor} editor The editor instance which this button is * to be used by. * @param {Array} [output] The output array to which append the HTML relative * to this button. */ render: function( editor, output ) { this.getHolderElement = function() { var holder = this._.holder; if ( !holder ) { if ( this.isFramed ) { var iframe = this.document.getById( this.id + '_frame' ), parentDiv = iframe.getParent(), doc = iframe.getFrameDocument(); // Make it scrollable on iOS. (http://dev.ckeditor.com/ticket/8308) CKEDITOR.env.iOS && parentDiv.setStyles( { 'overflow': 'scroll', '-webkit-overflow-scrolling': 'touch' } ); var onLoad = CKEDITOR.tools.addFunction( CKEDITOR.tools.bind( function() { this.isLoaded = true; if ( this.onLoad ) this.onLoad(); }, this ) ); doc.write( frameDocTpl.output( CKEDITOR.tools.extend( { css: CKEDITOR.tools.buildStyleHtml( this.css ), onload: 'window.parent.CKEDITOR.tools.callFunction(' + onLoad + ');' }, data ) ) ); var win = doc.getWindow(); // Register the CKEDITOR global. win.$.CKEDITOR = CKEDITOR; // Arrow keys for scrolling is only preventable with 'keypress' event in Opera (http://dev.ckeditor.com/ticket/4534). doc.on( 'keydown', function( evt ) { var keystroke = evt.data.getKeystroke(), dir = this.document.getById( this.id ).getAttribute( 'dir' ); // Delegate key processing to block. if ( this._.onKeyDown && this._.onKeyDown( keystroke ) === false ) { evt.data.preventDefault(); return; } // ESC/ARROW-LEFT(ltr) OR ARROW-RIGHT(rtl) if ( keystroke == 27 || keystroke == ( dir == 'rtl' ? 39 : 37 ) ) { if ( this.onEscape && this.onEscape( keystroke ) === false ) evt.data.preventDefault(); } }, this ); holder = doc.getBody(); holder.unselectable(); CKEDITOR.env.air && CKEDITOR.tools.callFunction( onLoad ); } else { holder = this.document.getById( this.id ); } this._.holder = holder; } return holder; }; var data = { editorId: editor.id, id: this.id, langCode: editor.langCode, dir: editor.lang.dir, cls: this.className, frame: '', env: CKEDITOR.env.cssClass, 'z-index': editor.config.baseFloatZIndex + 1 }; if ( this.isFramed ) { // With IE, the custom domain has to be taken care at first, // for other browers, the 'src' attribute should be left empty to // trigger iframe's 'load' event. var src = CKEDITOR.env.air ? 'javascript:void(0)' : // jshint ignore:line CKEDITOR.env.ie ? 'javascript:void(function(){' + encodeURIComponent( // jshint ignore:line 'document.open();' + // In IE, the document domain must be set any time we call document.open(). '(' + CKEDITOR.tools.fixDomain + ')();' + 'document.close();' ) + '}())' : ''; data.frame = frameTpl.output( { id: this.id + '_frame', src: src } ); } var html = panelTpl.output( data ); if ( output ) output.push( html ); return html; }, /** * @todo */ addBlock: function( name, block ) { block = this._.blocks[ name ] = block instanceof CKEDITOR.ui.panel.block ? block : new CKEDITOR.ui.panel.block( this.getHolderElement(), block ); if ( !this._.currentBlock ) this.showBlock( name ); return block; }, /** * @todo */ getBlock: function( name ) { return this._.blocks[ name ]; }, /** * @todo */ showBlock: function( name ) { var blocks = this._.blocks, block = blocks[ name ], current = this._.currentBlock; // ARIA role works better in IE on the body element, while on the iframe // for FF. (http://dev.ckeditor.com/ticket/8864) var holder = !this.forceIFrame || CKEDITOR.env.ie ? this._.holder : this.document.getById( this.id + '_frame' ); if ( current ) current.hide(); this._.currentBlock = block; CKEDITOR.fire( 'ariaWidget', holder ); // Reset the focus index, so it will always go into the first one. block._.focusIndex = -1; this._.onKeyDown = block.onKeyDown && CKEDITOR.tools.bind( block.onKeyDown, block ); block.show(); return block; }, /** * @todo */ destroy: function() { this.element && this.element.remove(); } }; /** * @class * * @todo class and all methods */ CKEDITOR.ui.panel.block = CKEDITOR.tools.createClass( { /** * Creates a block class instances. * * @constructor * @todo */ $: function( blockHolder, blockDefinition ) { this.element = blockHolder.append( blockHolder.getDocument().createElement( 'div', { attributes: { 'tabindex': -1, 'class': 'cke_panel_block' }, styles: { display: 'none' } } ) ); // Copy all definition properties to this object. if ( blockDefinition ) CKEDITOR.tools.extend( this, blockDefinition ); // Set the a11y attributes of this element ... this.element.setAttributes( { 'role': this.attributes.role || 'presentation', 'aria-label': this.attributes[ 'aria-label' ], 'title': this.attributes.title || this.attributes[ 'aria-label' ] } ); this.keys = {}; this._.focusIndex = -1; // Disable context menu for panels. this.element.disableContextMenu(); }, _: { /** * Mark the item specified by the index as current activated. */ markItem: function( index ) { if ( index == -1 ) return; var links = this.element.getElementsByTag( 'a' ); var item = links.getItem( this._.focusIndex = index ); // Safari need focus on the iframe window first(http://dev.ckeditor.com/ticket/3389), but we need // lock the blur to avoid hiding the panel. if ( CKEDITOR.env.webkit ) item.getDocument().getWindow().focus(); item.focus(); this.onMark && this.onMark( item ); }, /** * Marks the first visible item or the one whose `aria-selected` attribute is set to `true`. * The latter has priority over the former. * * @private * @param beforeMark function to be executed just before marking. * Used in cases when any preparatory cleanup (like unmarking all items) would simultaneously * destroy the information that is needed to determine the focused item. */ markFirstDisplayed: function( beforeMark ) { var notDisplayed = function( element ) { return element.type == CKEDITOR.NODE_ELEMENT && element.getStyle( 'display' ) == 'none'; }, links = this._.getItems(), item, focused; for ( var i = links.count() - 1; i >= 0; i-- ) { item = links.getItem( i ); if ( !item.getAscendant( notDisplayed ) ) { focused = item; this._.focusIndex = i; } if ( item.getAttribute( 'aria-selected' ) == 'true' ) { focused = item; this._.focusIndex = i; break; } } if ( !focused ) { return; } if ( beforeMark ) { beforeMark(); } if ( CKEDITOR.env.webkit ) focused.getDocument().getWindow().focus(); focused.focus(); this.onMark && this.onMark( focused ); }, /** * Returns a `CKEDITOR.dom.nodeList` of block items. * * @returns {*|CKEDITOR.dom.nodeList} */ getItems: function() { return this.element.getElementsByTag( 'a' ); } }, proto: { show: function() { this.element.setStyle( 'display', '' ); }, hide: function() { if ( !this.onHide || this.onHide.call( this ) !== true ) this.element.setStyle( 'display', 'none' ); }, onKeyDown: function( keystroke, noCycle ) { var keyAction = this.keys[ keystroke ]; switch ( keyAction ) { // Move forward. case 'next': var index = this._.focusIndex, links = this.element.getElementsByTag( 'a' ), link; while ( ( link = links.getItem( ++index ) ) ) { // Move the focus only if the element is marked with // the _cke_focus and it it's visible (check if it has // width). if ( link.getAttribute( '_cke_focus' ) && link.$.offsetWidth ) { this._.focusIndex = index; link.focus(); break; } } // If no link was found, cycle and restart from the top. (http://dev.ckeditor.com/ticket/11125) if ( !link && !noCycle ) { this._.focusIndex = -1; return this.onKeyDown( keystroke, 1 ); } return false; // Move backward. case 'prev': index = this._.focusIndex; links = this.element.getElementsByTag( 'a' ); while ( index > 0 && ( link = links.getItem( --index ) ) ) { // Move the focus only if the element is marked with // the _cke_focus and it it's visible (check if it has // width). if ( link.getAttribute( '_cke_focus' ) && link.$.offsetWidth ) { this._.focusIndex = index; link.focus(); break; } // Make sure link is null when the loop ends and nothing was // found (http://dev.ckeditor.com/ticket/11125). link = null; } // If no link was found, cycle and restart from the bottom. (http://dev.ckeditor.com/ticket/11125) if ( !link && !noCycle ) { this._.focusIndex = links.count(); return this.onKeyDown( keystroke, 1 ); } return false; case 'click': case 'mouseup': index = this._.focusIndex; link = index >= 0 && this.element.getElementsByTag( 'a' ).getItem( index ); if ( link ) link.$[ keyAction ] ? link.$[ keyAction ]() : link.$[ 'on' + keyAction ](); return false; } return true; } } } ); } )(); /** * Fired when a panel is added to the document. * * @event ariaWidget * @member CKEDITOR * @param {Object} data The element wrapping the panel. */