/* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. For licensing, see LICENSE.md or http://ckeditor.com/license */ (function(){if(window.CKEDITOR&&window.CKEDITOR.dom)return;/** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview Contains the first and essential part of the {@link CKEDITOR} * object definition. */ // #### Compressed Code // Compressed code in ckeditor.js must be be updated on changes in the script. // The Closure Compiler online service should be used when updating this manually: // http://closure-compiler.appspot.com/ // #### Raw code // ATTENTION: read the above "Compressed Code" notes when changing this code. if ( !window.CKEDITOR ) { /** * This is the API entry point. The entire CKEditor code runs under this object. * @class CKEDITOR * @mixins CKEDITOR.event * @singleton */ window.CKEDITOR = ( function() { var basePathSrcPattern = /(^|.*[\\\/])ckeditor\.js(?:\?.*|;.*)?$/i; var CKEDITOR = { /** * A constant string unique for each release of CKEditor. Its value * is used, by default, to build the URL for all resources loaded * by the editor code, guaranteeing clean cache results when * upgrading. * * **Note:** There is [a known issue where "icons.png" does not include * timestamp](https://dev.ckeditor.com/ticket/10685) and might get cached. * We are working on having it fixed. * * alert( CKEDITOR.timestamp ); // e.g. '87dm' */ // The production implementation contains a fixed timestamp, unique // for each release and generated by the releaser. // (Base 36 value of each component of YYMMDDHH - 4 chars total - e.g. 87bm == 08071122) timestamp: 'J5R9', /** * Contains the CKEditor version number. * * alert( CKEDITOR.version ); // e.g. 'CKEditor 3.4.1' */ version: '4.12.0', /** * Contains the CKEditor revision number. * The revision number is incremented automatically, following each * modification to the CKEditor source code. * * alert( CKEDITOR.revision ); // e.g. '3975' */ revision: '0', /** * A 3-digit random integer, valid for the entire life of the CKEDITOR object. * * alert( CKEDITOR.rnd ); // e.g. 319 * * @property {Number} */ rnd: Math.floor( Math.random() * ( 999 /*Max*/ - 100 /*Min*/ + 1 ) ) + 100 /*Min*/, /** * Private object used to hold core stuff. It should not be used outside of * the API code as properties defined here may change at any time * without notice. * * @private */ _: { pending: [], basePathSrcPattern: basePathSrcPattern }, /** * Indicates the API loading status. The following statuses are available: * * * **unloaded**: the API is not yet loaded. * * **basic_loaded**: the basic API features are available. * * **basic_ready**: the basic API is ready to load the full core code. * * **loaded**: the API can be fully used. * * Example: * * if ( CKEDITOR.status == 'loaded' ) { * // The API can now be fully used. * doSomething(); * } else { * // Wait for the full core to be loaded and fire its loading. * CKEDITOR.on( 'load', doSomething ); * CKEDITOR.loadFullCore && CKEDITOR.loadFullCore(); * } */ status: 'unloaded', /** * The full URL for the CKEditor installation directory. * It is possible to manually provide the base path by setting a * global variable named `CKEDITOR_BASEPATH`. This global variable * must be set **before** the editor script loading. * * alert( CKEDITOR.basePath ); // e.g. 'http://www.example.com/ckeditor/' * * @property {String} */ basePath: ( function() { // Find out the editor directory path, based on its ' ); this.$.write( html ); this.$.close(); }, /** * Wrapper for `querySelectorAll`. Returns a list of elements within this document that match * the specified `selector`. * * **Note:** The returned list is not a live collection (like the result of native `querySelectorAll`). * * @since 4.3.0 * @param {String} selector A valid [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors). * @returns {CKEDITOR.dom.nodeList} */ find: function( selector ) { return new CKEDITOR.dom.nodeList( this.$.querySelectorAll( selector ) ); }, /** * Wrapper for `querySelector`. Returns the first element within this document that matches * the specified `selector`. * * @since 4.3.0 * @param {String} selector A valid [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors). * @returns {CKEDITOR.dom.element} */ findOne: function( selector ) { var el = this.$.querySelector( selector ); return el ? new CKEDITOR.dom.element( el ) : null; }, /** * Internet Explorer 8 only method. It returns a document fragment which has all HTML5 elements enabled. * * @since 4.3.0 * @private * @returns DocumentFragment */ _getHtml5ShivFrag: function() { var $frag = this.getCustomData( 'html5ShivFrag' ); if ( !$frag ) { $frag = this.$.createDocumentFragment(); CKEDITOR.tools.enableHtml5Elements( $frag, true ); this.setCustomData( 'html5ShivFrag', $frag ); } return $frag; } } ); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * Represents a list of {@link CKEDITOR.dom.node} objects. * It is a wrapper for a native nodes list. * * var nodeList = CKEDITOR.document.getBody().getChildren(); * alert( nodeList.count() ); // number [0;N] * * @class * @constructor Creates a document class instance. * @param {Object} nativeList */ CKEDITOR.dom.nodeList = function( nativeList ) { this.$ = nativeList; }; CKEDITOR.dom.nodeList.prototype = { /** * Gets the count of nodes in this list. * * @returns {Number} */ count: function() { return this.$.length; }, /** * Gets the node from the list. * * @returns {CKEDITOR.dom.node} */ getItem: function( index ) { if ( index < 0 || index >= this.$.length ) return null; var $node = this.$[ index ]; return $node ? new CKEDITOR.dom.node( $node ) : null; }, /** * Returns a node list as an array. * * @returns {CKEDITOR.dom.node[]} */ toArray: function() { return CKEDITOR.tools.array.map( this.$, function( nativeEl ) { return new CKEDITOR.dom.node( nativeEl ); } ); } }; /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview Defines the {@link CKEDITOR.dom.element} class, which * represents a DOM element. */ /** * Represents a DOM element. * * // Create a new element. * var element = new CKEDITOR.dom.element( 'span' ); * * // Create an element based on a native DOM element. * var element = new CKEDITOR.dom.element( document.getElementById( 'myId' ) ); * * @class * @extends CKEDITOR.dom.node * @constructor Creates an element class instance. * @param {Object/String} element A native DOM element or the element name for * new elements. * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain * the element in case of element creation. */ CKEDITOR.dom.element = function( element, ownerDocument ) { if ( typeof element == 'string' ) element = ( ownerDocument ? ownerDocument.$ : document ).createElement( element ); // Call the base constructor (we must not call CKEDITOR.dom.node). CKEDITOR.dom.domObject.call( this, element ); }; // PACKAGER_RENAME( CKEDITOR.dom.element ) /** * The the {@link CKEDITOR.dom.element} representing and element. If the * element is a native DOM element, it will be transformed into a valid * CKEDITOR.dom.element object. * * var element = new CKEDITOR.dom.element( 'span' ); * alert( element == CKEDITOR.dom.element.get( element ) ); // true * * var element = document.getElementById( 'myElement' ); * alert( CKEDITOR.dom.element.get( element ).getName() ); // (e.g.) 'p' * * @static * @param {String/Object} element Element's id or name or native DOM element. * @returns {CKEDITOR.dom.element} The transformed element. */ CKEDITOR.dom.element.get = function( element ) { var el = typeof element == 'string' ? document.getElementById( element ) || document.getElementsByName( element )[ 0 ] : element; return el && ( el.$ ? el : new CKEDITOR.dom.element( el ) ); }; CKEDITOR.dom.element.prototype = new CKEDITOR.dom.node(); /** * Creates an instance of the {@link CKEDITOR.dom.element} class based on the * HTML representation of an element. * * var element = CKEDITOR.dom.element.createFromHtml( 'My element' ); * alert( element.getName() ); // 'strong' * * @static * @param {String} html The element HTML. It should define only one element in * the "root" level. The "root" element can have child nodes, but not siblings. * @returns {CKEDITOR.dom.element} The element instance. */ CKEDITOR.dom.element.createFromHtml = function( html, ownerDocument ) { var temp = new CKEDITOR.dom.element( 'div', ownerDocument ); temp.setHtml( html ); // When returning the node, remove it from its parent to detach it. return temp.getFirst().remove(); }; /** * Sets {@link CKEDITOR.dom.element#setCustomData custom data} on an element in a way that it is later * possible to {@link #clearAllMarkers clear all data} set on all elements sharing the same database. * * This mechanism is very useful when processing some portion of DOM. All markers can later be removed * by calling the {@link #clearAllMarkers} method, hence markers will not leak to second pass of this algorithm. * * var database = {}; * CKEDITOR.dom.element.setMarker( database, element1, 'foo', 'bar' ); * CKEDITOR.dom.element.setMarker( database, element2, 'oof', [ 1, 2, 3 ] ); * * element1.getCustomData( 'foo' ); // 'bar' * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ] * * CKEDITOR.dom.element.clearAllMarkers( database ); * * element1.getCustomData( 'foo' ); // null * * @static * @param {Object} database * @param {CKEDITOR.dom.element} element * @param {String} name * @param {Object} value * @returns {CKEDITOR.dom.element} The element. */ CKEDITOR.dom.element.setMarker = function( database, element, name, value ) { var id = element.getCustomData( 'list_marker_id' ) || ( element.setCustomData( 'list_marker_id', CKEDITOR.tools.getNextNumber() ).getCustomData( 'list_marker_id' ) ), markerNames = element.getCustomData( 'list_marker_names' ) || ( element.setCustomData( 'list_marker_names', {} ).getCustomData( 'list_marker_names' ) ); database[ id ] = element; markerNames[ name ] = 1; return element.setCustomData( name, value ); }; /** * Removes all markers added using this database. See the {@link #setMarker} method for more information. * * @param {Object} database * @static */ CKEDITOR.dom.element.clearAllMarkers = function( database ) { for ( var i in database ) CKEDITOR.dom.element.clearMarkers( database, database[ i ], 1 ); }; /** * Removes all markers added to this element and removes it from the database if * `removeFromDatabase` was passed. See the {@link #setMarker} method for more information. * * var database = {}; * CKEDITOR.dom.element.setMarker( database, element1, 'foo', 'bar' ); * CKEDITOR.dom.element.setMarker( database, element2, 'oof', [ 1, 2, 3 ] ); * * element1.getCustomData( 'foo' ); // 'bar' * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ] * * CKEDITOR.dom.element.clearMarkers( database, element1, true ); * * element1.getCustomData( 'foo' ); // null * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ] * * @param {Object} database * @static */ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatabase ) { var names = element.getCustomData( 'list_marker_names' ), id = element.getCustomData( 'list_marker_id' ); for ( var i in names ) element.removeCustomData( i ); element.removeCustomData( 'list_marker_names' ); if ( removeFromDatabase ) { element.removeCustomData( 'list_marker_id' ); delete database[ id ]; } }; ( function() { var elementsClassList = document.createElement( '_' ).classList, supportsClassLists = typeof elementsClassList !== 'undefined' && String( elementsClassList.add ).match( /\[Native code\]/gi ) !== null, rclass = /[\n\t\r]/g; function hasClass( classNames, className ) { // Source: jQuery. return ( ' ' + classNames + ' ' ).replace( rclass, ' ' ).indexOf( ' ' + className + ' ' ) > -1; } CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, { /** * The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}. * * @readonly * @property {Number} [=CKEDITOR.NODE_ELEMENT] */ type: CKEDITOR.NODE_ELEMENT, /** * Adds a CSS class to the element. It appends the class to the * already existing names. * * var element = new CKEDITOR.dom.element( 'div' ); * element.addClass( 'classA' ); //
* element.addClass( 'classB' ); //
* element.addClass( 'classA' ); //
* * **Note:** Since CKEditor 4.5.0 this method cannot be used with multiple classes (`'classA classB'`). * * @chainable * @method addClass * @param {String} className The name of the class to be added. */ addClass: supportsClassLists ? function( className ) { this.$.classList.add( className ); return this; } : function( className ) { var c = this.$.className; if ( c ) { if ( !hasClass( c, className ) ) c += ' ' + className; } this.$.className = c || className; return this; }, /** * Removes a CSS class name from the elements classes. Other classes * remain untouched. * * var element = new CKEDITOR.dom.element( 'div' ); * element.addClass( 'classA' ); //
* element.addClass( 'classB' ); //
* element.removeClass( 'classA' ); //
* element.removeClass( 'classB' ); //
* * @chainable * @method removeClass * @param {String} className The name of the class to remove. */ removeClass: supportsClassLists ? function( className ) { var $ = this.$; $.classList.remove( className ); if ( !$.className ) $.removeAttribute( 'class' ); return this; } : function( className ) { var c = this.getAttribute( 'class' ); if ( c && hasClass( c, className ) ) { c = c .replace( new RegExp( '(?:^|\\s+)' + className + '(?=\\s|$)' ), '' ) .replace( /^\s+/, '' ); if ( c ) this.setAttribute( 'class', c ); else this.removeAttribute( 'class' ); } return this; }, /** * Checks if element has class name. * * @param {String} className * @returns {Boolean} */ hasClass: function( className ) { return hasClass( this.$.className, className ); }, /** * Append a node as a child of this element. * * var p = new CKEDITOR.dom.element( 'p' ); * * var strong = new CKEDITOR.dom.element( 'strong' ); * p.append( strong ); * * var em = p.append( 'em' ); * * // Result: '

' * * @param {CKEDITOR.dom.node/String} node The node or element name to be appended. * @param {Boolean} [toStart=false] Indicates that the element is to be appended at the start. * @returns {CKEDITOR.dom.node} The appended node. */ append: function( node, toStart ) { if ( typeof node == 'string' ) node = this.getDocument().createElement( node ); if ( toStart ) this.$.insertBefore( node.$, this.$.firstChild ); else this.$.appendChild( node.$ ); return node; }, /** * Append HTML as a child(ren) of this element. * * @param {String} html */ appendHtml: function( html ) { if ( !this.$.childNodes.length ) this.setHtml( html ); else { var temp = new CKEDITOR.dom.element( 'div', this.getDocument() ); temp.setHtml( html ); temp.moveChildren( this ); } }, /** * Append text to this element. * * var p = new CKEDITOR.dom.element( 'p' ); * p.appendText( 'This is' ); * p.appendText( ' some text' ); * * // Result: '

This is some text

' * * @param {String} text The text to be appended. */ appendText: function( text ) { // On IE8 it is impossible to append node to script tag, so we use its text. // On the contrary, on Safari the text property is unpredictable in links. (https://dev.ckeditor.com/ticket/13232) if ( this.$.text != null && CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) this.$.text += text; else this.append( new CKEDITOR.dom.text( text ) ); }, /** * Appends a `
` filler element to this element if the filler is not present already. * By default filler is appended only if {@link CKEDITOR.env#needsBrFiller} is `true`, * however when `force` is set to `true` filler will be appended regardless of the environment. * * @param {Boolean} [force] Append filler regardless of the environment. */ appendBogus: function( force ) { if ( !force && !CKEDITOR.env.needsBrFiller ) return; var lastChild = this.getLast(); // Ignore empty/spaces text. while ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.rtrim( lastChild.getText() ) ) lastChild = lastChild.getPrevious(); if ( !lastChild || !lastChild.is || !lastChild.is( 'br' ) ) { var bogus = this.getDocument().createElement( 'br' ); CKEDITOR.env.gecko && bogus.setAttribute( 'type', '_moz' ); this.append( bogus ); } }, /** * Breaks one of the ancestor element in the element position, moving * this element between the broken parts. * * // Before breaking: * // This is some sample test text * // If "element" is and "parent" is : * // This is some sample test text * element.breakParent( parent ); * * // Before breaking: * // This is some sample test text * // If "element" is and "parent" is : * // This is some sample test text * element.breakParent( parent ); * * @param {CKEDITOR.dom.element} parent The anscestor element to get broken. * @param {Boolean} [cloneId=false] Whether to preserve ancestor ID attributes while breaking. */ breakParent: function( parent, cloneId ) { var range = new CKEDITOR.dom.range( this.getDocument() ); // We'll be extracting part of this element, so let's use our // range to get the correct piece. range.setStartAfter( this ); range.setEndAfter( parent ); // Extract it. var docFrag = range.extractContents( false, cloneId || false ), tmpElement, current; // Move the element outside the broken element. range.insertNode( this.remove() ); // In case of Internet Explorer, we must check if there is no background-color // added to the element. In such case, we have to overwrite it to prevent "switching it off" // by a browser (https://dev.ckeditor.com/ticket/14667). if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) { tmpElement = new CKEDITOR.dom.element( 'div' ); while ( current = docFrag.getFirst() ) { if ( current.$.style.backgroundColor ) { // This is a necessary hack to make sure that IE will track backgroundColor CSS property, see // https://dev.ckeditor.com/ticket/14667#comment:8 for more details. current.$.style.backgroundColor = current.$.style.backgroundColor; } tmpElement.append( current ); } // Re-insert the extracted piece after the element. tmpElement.insertAfter( this ); tmpElement.remove( true ); } else { // Re-insert the extracted piece after the element. docFrag.insertAfterNode( this ); } }, /** * Checks if this element contains given node. * * @method * @param {CKEDITOR.dom.node} node * @returns {Boolean} */ contains: !document.compareDocumentPosition ? function( node ) { var $ = this.$; return node.type != CKEDITOR.NODE_ELEMENT ? $.contains( node.getParent().$ ) : $ != node.$ && $.contains( node.$ ); } : function( node ) { return !!( this.$.compareDocumentPosition( node.$ ) & 16 ); }, /** * Moves the selection focus to this element. * * var element = CKEDITOR.document.getById( 'myTextarea' ); * element.focus(); * * @method * @param {Boolean} defer Whether to asynchronously defer the * execution by 100 ms. */ focus: ( function() { function exec() { // IE throws error if the element is not visible. try { this.$.focus(); } catch ( e ) {} } return function( defer ) { if ( defer ) CKEDITOR.tools.setTimeout( exec, 100, this ); else exec.call( this ); }; } )(), /** * Gets the inner HTML of this element. * * var element = CKEDITOR.dom.element.createFromHtml( '
Example
' ); * alert( element.getHtml() ); // 'Example' * * @returns {String} The inner HTML of this element. */ getHtml: function() { var retval = this.$.innerHTML; // Strip tags in IE. (https://dev.ckeditor.com/ticket/3341). return CKEDITOR.env.ie ? retval.replace( /<\?[^>]*>/g, '' ) : retval; }, /** * Gets the outer (inner plus tags) HTML of this element. * * var element = CKEDITOR.dom.element.createFromHtml( '
Example
' ); * alert( element.getOuterHtml() ); // '
Example
' * * @returns {String} The outer HTML of this element. */ getOuterHtml: function() { if ( this.$.outerHTML ) { // IE includes the tag in the outerHTML of // namespaced element. So, we must strip it here. (https://dev.ckeditor.com/ticket/3341) return this.$.outerHTML.replace( /<\?[^>]*>/, '' ); } var tmpDiv = this.$.ownerDocument.createElement( 'div' ); tmpDiv.appendChild( this.$.cloneNode( true ) ); return tmpDiv.innerHTML; }, /** * Retrieve the bounding rectangle of the current element, in pixels, * relative to the upper-left corner of the browser's client area. * * Since 4.10.0 you can pass an additional parameter if the function should return an absolute element position that can be used for * positioning elements inside scrollable areas. * * For example, you can use this function with the {@link CKEDITOR.dom.window#getFrame editor's window frame} to * calculate the absolute rectangle of the visible area of the editor viewport. * The retrieved absolute rectangle can be used to position elements like toolbars or notifications (elements outside the editor) * to always keep them inside the editor viewport independently from the scroll position. * * ```javascript * var frame = editor.window.getFrame(); * frame.getClientRect( true ); * ``` * * @param {Boolean} [isAbsolute=false] The function will retrieve an absolute rectangle of the element, i.e. the position relative * to the upper-left corner of the topmost viewport. This option is available since 4.10.0. * @returns {CKEDITOR.dom.rect} The dimensions of the DOM element. */ getClientRect: function( isAbsolute ) { // http://help.dottoro.com/ljvmcrrn.php var elementRect = CKEDITOR.tools.extend( {}, this.$.getBoundingClientRect() ); !elementRect.width && ( elementRect.width = elementRect.right - elementRect.left ); !elementRect.height && ( elementRect.height = elementRect.bottom - elementRect.top ); if ( !isAbsolute ) { return elementRect; } return CKEDITOR.tools.getAbsoluteRectPosition( this.getWindow(), elementRect ); }, /** * Sets the inner HTML of this element. * * var p = new CKEDITOR.dom.element( 'p' ); * p.setHtml( 'Inner HTML' ); * * // Result: '

Inner HTML

' * * @method * @param {String} html The HTML to be set for this element. * @returns {String} The inserted HTML. */ setHtml: ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) ? // old IEs throws error on HTML manipulation (through the "innerHTML" property) // on the element which resides in an DTD invalid position, e.g.
// fortunately it can be worked around with DOM manipulation. function( html ) { try { var $ = this.$; // Fix the case when setHtml is called on detached element. // HTML5 shiv used for document in which this element was created // won't affect that detached element. So get document fragment with // all HTML5 elements enabled and set innerHTML while this element is appended to it. if ( this.getParent() ) return ( $.innerHTML = html ); else { var $frag = this.getDocument()._getHtml5ShivFrag(); $frag.appendChild( $ ); $.innerHTML = html; $frag.removeChild( $ ); return html; } } catch ( e ) { this.$.innerHTML = ''; var temp = new CKEDITOR.dom.element( 'body', this.getDocument() ); temp.$.innerHTML = html; var children = temp.getChildren(); while ( children.count() ) this.append( children.getItem( 0 ) ); return html; } } : function( html ) { return ( this.$.innerHTML = html ); }, /** * Sets the element contents as plain text. * * var element = new CKEDITOR.dom.element( 'div' ); * element.setText( 'A > B & C < D' ); * alert( element.innerHTML ); // 'A > B & C < D' * * @param {String} text The text to be set. * @returns {String} The inserted text. */ setText: ( function() { var supportsTextContent = document.createElement( 'p' ); supportsTextContent.innerHTML = 'x'; supportsTextContent = supportsTextContent.textContent; return function( text ) { this.$[ supportsTextContent ? 'textContent' : 'innerText' ] = text; }; } )(), /** * Gets the value of an element attribute. * * var element = CKEDITOR.dom.element.createFromHtml( '' ); * alert( element.getAttribute( 'type' ) ); // 'text' * * @method * @param {String} name The attribute name. * @returns {String} The attribute value or null if not defined. */ getAttribute: ( function() { var standard = function( name ) { return this.$.getAttribute( name, 2 ); }; if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) { return function( name ) { switch ( name ) { case 'class': name = 'className'; break; case 'http-equiv': name = 'httpEquiv'; break; case 'name': return this.$.name; case 'tabindex': var tabIndex = standard.call( this, name ); // IE returns tabIndex=0 by default for all // elements. For those elements, // getAtrribute( 'tabindex', 2 ) returns 32768 // instead. So, we must make this check to give a // uniform result among all browsers. if ( tabIndex !== 0 && this.$.tabIndex === 0 ) tabIndex = null; return tabIndex; case 'checked': var attr = this.$.attributes.getNamedItem( name ), attrValue = attr.specified ? attr.nodeValue // For value given by parser. : this.$.checked; // For value created via DOM interface. return attrValue ? 'checked' : null; case 'hspace': case 'value': return this.$[ name ]; case 'style': // IE does not return inline styles via getAttribute(). See https://dev.ckeditor.com/ticket/2947. return this.$.style.cssText; case 'contenteditable': case 'contentEditable': return this.$.attributes.getNamedItem( 'contentEditable' ).specified ? this.$.getAttribute( 'contentEditable' ) : null; } return standard.call( this, name ); }; } else { return standard; } } )(), /** * Gets the values of all element attributes. * * @param {Array} exclude The names of attributes to be excluded from the returned object. * @return {Object} An object containing all element attributes with their values. */ getAttributes: function( exclude ) { var attributes = {}, attrDefs = this.$.attributes, i; exclude = CKEDITOR.tools.isArray( exclude ) ? exclude : []; for ( i = 0; i < attrDefs.length; i++ ) { if ( CKEDITOR.tools.indexOf( exclude, attrDefs[ i ].name ) === -1 ) { attributes[ attrDefs[ i ].name ] = attrDefs[ i ].value; } } return attributes; }, /** * Gets the nodes list containing all children of this element. * * @returns {CKEDITOR.dom.nodeList} */ getChildren: function() { return new CKEDITOR.dom.nodeList( this.$.childNodes ); }, /** * Gets the current computed value of one of the element CSS style * properties. * * var element = new CKEDITOR.dom.element( 'span' ); * alert( element.getComputedStyle( 'display' ) ); // 'inline' * * @method * @param {String} propertyName The style property name. * @returns {String} The property value. */ getComputedStyle: ( document.defaultView && document.defaultView.getComputedStyle ) ? function( propertyName ) { var style = this.getWindow().$.getComputedStyle( this.$, null ); // Firefox may return null if we call the above on a hidden iframe. (https://dev.ckeditor.com/ticket/9117) return style ? style.getPropertyValue( propertyName ) : ''; } : function( propertyName ) { return this.$.currentStyle[ CKEDITOR.tools.cssStyleToDomStyle( propertyName ) ]; }, /** * Gets the DTD entries for this element. * * @returns {Object} An object containing the list of elements accepted * by this element. */ getDtd: function() { var dtd = CKEDITOR.dtd[ this.getName() ]; this.getDtd = function() { return dtd; }; return dtd; }, /** * Gets all this element's descendants having given tag name. * * @method * @param {String} tagName */ getElementsByTag: CKEDITOR.dom.document.prototype.getElementsByTag, /** * Gets the computed tabindex for this element. * * var element = CKEDITOR.document.getById( 'myDiv' ); * alert( element.getTabIndex() ); // (e.g.) '-1' * * @method * @returns {Number} The tabindex value. */ getTabIndex: function() { var tabIndex = this.$.tabIndex; // IE returns tabIndex=0 by default for all elements. In // those cases we must check that the element really has // the tabindex attribute set to zero, or it is one of // those element that should have zero by default. if ( tabIndex === 0 && !CKEDITOR.dtd.$tabIndex[ this.getName() ] && parseInt( this.getAttribute( 'tabindex' ), 10 ) !== 0 ) return -1; return tabIndex; }, /** * Gets the text value of this element. * * Only in IE (which uses innerText), `
` will cause linebreaks, * and sucessive whitespaces (including line breaks) will be reduced to * a single space. This behavior is ok for us, for now. It may change * in the future. * * var element = CKEDITOR.dom.element.createFromHtml( '
Sample text.
' ); * alert( element.getText() ); // 'Sample text.' * * @returns {String} The text value. */ getText: function() { return this.$.textContent || this.$.innerText || ''; }, /** * Gets the window object that contains this element. * * @returns {CKEDITOR.dom.window} The window object. */ getWindow: function() { return this.getDocument().getWindow(); }, /** * Gets the value of the `id` attribute of this element. * * var element = CKEDITOR.dom.element.createFromHtml( '

' ); * alert( element.getId() ); // 'myId' * * @returns {String} The element id, or null if not available. */ getId: function() { return this.$.id || null; }, /** * Gets the value of the `name` attribute of this element. * * var element = CKEDITOR.dom.element.createFromHtml( '' ); * alert( element.getNameAtt() ); // 'myName' * * @returns {String} The element name, or null if not available. */ getNameAtt: function() { return this.$.name || null; }, /** * Gets the element name (tag name). The returned name is guaranteed to * be always full lowercased. * * var element = new CKEDITOR.dom.element( 'span' ); * alert( element.getName() ); // 'span' * * @returns {String} The element name. */ getName: function() { // Cache the lowercased name inside a closure. var nodeName = this.$.nodeName.toLowerCase(); if ( CKEDITOR.env.ie && ( document.documentMode <= 8 ) ) { var scopeName = this.$.scopeName; if ( scopeName != 'HTML' ) nodeName = scopeName.toLowerCase() + ':' + nodeName; } this.getName = function() { return nodeName; }; return this.getName(); }, /** * Gets the value set to this element. This value is usually available * for form field elements. * * @returns {String} The element value. */ getValue: function() { return this.$.value; }, /** * Gets the first child node of this element. * * var element = CKEDITOR.dom.element.createFromHtml( '
Example
' ); * var first = element.getFirst(); * alert( first.getName() ); // 'b' * * @param {Function} evaluator Filtering the result node. * @returns {CKEDITOR.dom.node} The first child node or null if not available. */ getFirst: function( evaluator ) { var first = this.$.firstChild, retval = first && new CKEDITOR.dom.node( first ); if ( retval && evaluator && !evaluator( retval ) ) retval = retval.getNext( evaluator ); return retval; }, /** * See {@link #getFirst}. * * @param {Function} evaluator Filtering the result node. * @returns {CKEDITOR.dom.node} */ getLast: function( evaluator ) { var last = this.$.lastChild, retval = last && new CKEDITOR.dom.node( last ); if ( retval && evaluator && !evaluator( retval ) ) retval = retval.getPrevious( evaluator ); return retval; }, /** * Gets CSS style value. * * @param {String} name The CSS property name. * @returns {String} Style value. */ getStyle: function( name ) { return this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ]; }, /** * Checks if the element name matches the specified criteria. * * var element = new CKEDITOR.element( 'span' ); * alert( element.is( 'span' ) ); // true * alert( element.is( 'p', 'span' ) ); // true * alert( element.is( 'p' ) ); // false * alert( element.is( 'p', 'div' ) ); // false * alert( element.is( { p:1,span:1 } ) ); // true * * @param {String.../Object} name One or more names to be checked, or a {@link CKEDITOR.dtd} object. * @returns {Boolean} `true` if the element name matches any of the names. */ is: function() { var name = this.getName(); // Check against the specified DTD liternal. if ( typeof arguments[ 0 ] == 'object' ) return !!arguments[ 0 ][ name ]; // Check for tag names for ( var i = 0; i < arguments.length; i++ ) { if ( arguments[ i ] == name ) return true; } return false; }, /** * Decide whether one element is able to receive cursor. * * @param {Boolean} [textCursor=true] Only consider element that could receive text child. */ isEditable: function( textCursor ) { var name = this.getName(); if ( this.isReadOnly() || this.getComputedStyle( 'display' ) == 'none' || this.getComputedStyle( 'visibility' ) == 'hidden' || CKEDITOR.dtd.$nonEditable[ name ] || CKEDITOR.dtd.$empty[ name ] || ( this.is( 'a' ) && ( this.data( 'cke-saved-name' ) || this.hasAttribute( 'name' ) ) && !this.getChildCount() ) ) { return false; } if ( textCursor !== false ) { // Get the element DTD (defaults to span for unknown elements). var dtd = CKEDITOR.dtd[ name ] || CKEDITOR.dtd.span; // In the DTD # == text node. return !!( dtd && dtd[ '#' ] ); } return true; }, /** * Compare this element's inner html, tag name, attributes, etc. with other one. * * See [W3C's DOM Level 3 spec - node#isEqualNode](http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-isEqualNode) * for more details. * * @param {CKEDITOR.dom.element} otherElement Element to compare. * @returns {Boolean} */ isIdentical: function( otherElement ) { // do shallow clones, but with IDs var thisEl = this.clone( 0, 1 ), otherEl = otherElement.clone( 0, 1 ); // Remove distractions. thisEl.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] ); otherEl.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] ); // Native comparison available. if ( thisEl.$.isEqualNode ) { // Styles order matters. thisEl.$.style.cssText = CKEDITOR.tools.normalizeCssText( thisEl.$.style.cssText ); otherEl.$.style.cssText = CKEDITOR.tools.normalizeCssText( otherEl.$.style.cssText ); return thisEl.$.isEqualNode( otherEl.$ ); } else { thisEl = thisEl.getOuterHtml(); otherEl = otherEl.getOuterHtml(); // Fix tiny difference between link href in older IEs. if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && this.is( 'a' ) ) { var parent = this.getParent(); if ( parent.type == CKEDITOR.NODE_ELEMENT ) { var el = parent.clone(); el.setHtml( thisEl ), thisEl = el.getHtml(); el.setHtml( otherEl ), otherEl = el.getHtml(); } } return thisEl == otherEl; } }, /** * Checks if this element is visible. May not work if the element is * child of an element with visibility set to `hidden`, but works well * on the great majority of cases. * * @returns {Boolean} True if the element is visible. */ isVisible: function() { var isVisible = ( this.$.offsetHeight || this.$.offsetWidth ) && this.getComputedStyle( 'visibility' ) != 'hidden', elementWindow, elementWindowFrame; // Webkit and Opera report non-zero offsetHeight despite that // element is inside an invisible iframe. (https://dev.ckeditor.com/ticket/4542) if ( isVisible && CKEDITOR.env.webkit ) { elementWindow = this.getWindow(); if ( !elementWindow.equals( CKEDITOR.document.getWindow() ) && ( elementWindowFrame = elementWindow.$.frameElement ) ) isVisible = new CKEDITOR.dom.element( elementWindowFrame ).isVisible(); } return !!isVisible; }, /** * Whether it's an empty inline elements which has no visual impact when removed. * * @returns {Boolean} */ isEmptyInlineRemoveable: function() { if ( !CKEDITOR.dtd.$removeEmpty[ this.getName() ] ) return false; var children = this.getChildren(); for ( var i = 0, count = children.count(); i < count; i++ ) { var child = children.getItem( i ); if ( child.type == CKEDITOR.NODE_ELEMENT && child.data( 'cke-bookmark' ) ) continue; if ( child.type == CKEDITOR.NODE_ELEMENT && !child.isEmptyInlineRemoveable() || child.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( child.getText() ) ) return false; } return true; }, /** * Checks if the element has any defined attributes. * * var element = CKEDITOR.dom.element.createFromHtml( '
Example
' ); * alert( element.hasAttributes() ); // true * * var element = CKEDITOR.dom.element.createFromHtml( '
Example
' ); * alert( element.hasAttributes() ); // false * * @method * @returns {Boolean} True if the element has attributes. */ hasAttributes: CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ? function() { var attributes = this.$.attributes; for ( var i = 0; i < attributes.length; i++ ) { var attribute = attributes[ i ]; switch ( attribute.nodeName ) { case 'class': // IE has a strange bug. If calling removeAttribute('className'), // the attributes collection will still contain the "class" // attribute, which will be marked as "specified", even if the // outerHTML of the element is not displaying the class attribute. // Note : I was not able to reproduce it outside the editor, // but I've faced it while working on the TC of https://dev.ckeditor.com/ticket/1391. if ( this.getAttribute( 'class' ) ) { return true; } // Attributes to be ignored. /* falls through */ case 'data-cke-expando': continue; /* falls through */ default: if ( attribute.specified ) { return true; } } } return false; } : function() { var attrs = this.$.attributes, attrsNum = attrs.length; // The _moz_dirty attribute might get into the element after pasting (https://dev.ckeditor.com/ticket/5455) var execludeAttrs = { 'data-cke-expando': 1, _moz_dirty: 1 }; return attrsNum > 0 && ( attrsNum > 2 || !execludeAttrs[ attrs[ 0 ].nodeName ] || ( attrsNum == 2 && !execludeAttrs[ attrs[ 1 ].nodeName ] ) ); }, /** * Checks if the specified attribute is defined for this element. * * @method * @param {String} name The attribute name. * @returns {Boolean} `true` if the specified attribute is defined. */ hasAttribute: ( function() { function ieHasAttribute( name ) { var $attr = this.$.attributes.getNamedItem( name ); if ( this.getName() == 'input' ) { switch ( name ) { case 'class': return this.$.className.length > 0; case 'checked': return !!this.$.checked; case 'value': var type = this.getAttribute( 'type' ); return type == 'checkbox' || type == 'radio' ? this.$.value != 'on' : !!this.$.value; } } if ( !$attr ) return false; return $attr.specified; } if ( CKEDITOR.env.ie ) { if ( CKEDITOR.env.version < 8 ) { return function( name ) { // On IE < 8 the name attribute cannot be retrieved // right after the element creation and setting the // name with setAttribute. if ( name == 'name' ) return !!this.$.name; return ieHasAttribute.call( this, name ); }; } else { return ieHasAttribute; } } else { return function( name ) { // On other browsers specified property is deprecated and return always true, // but fortunately $.attributes contains only specified attributes. return !!this.$.attributes.getNamedItem( name ); }; } } )(), /** * Hides this element (sets `display: none`). * * var element = CKEDITOR.document.getById( 'myElement' ); * element.hide(); */ hide: function() { this.setStyle( 'display', 'none' ); }, /** * Moves this element's children to the target element. * * @param {CKEDITOR.dom.element} target * @param {Boolean} [toStart=false] Insert moved children at the * beginning of the target element. */ moveChildren: function( target, toStart ) { var $ = this.$; target = target.$; if ( $ == target ) return; var child; if ( toStart ) { while ( ( child = $.lastChild ) ) target.insertBefore( $.removeChild( child ), target.firstChild ); } else { while ( ( child = $.firstChild ) ) target.appendChild( $.removeChild( child ) ); } }, /** * Merges sibling elements that are identical to this one. * * Identical child elements are also merged. For example: * * => * * @method * @param {Boolean} [inlineOnly=true] Allow only inline elements to be merged. */ mergeSiblings: ( function() { function mergeElements( element, sibling, isNext ) { if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) { // Jumping over bookmark nodes and empty inline elements, e.g. , // queuing them to be moved later. (https://dev.ckeditor.com/ticket/5567) var pendingNodes = []; while ( sibling.data( 'cke-bookmark' ) || sibling.isEmptyInlineRemoveable() ) { pendingNodes.push( sibling ); sibling = isNext ? sibling.getNext() : sibling.getPrevious(); if ( !sibling || sibling.type != CKEDITOR.NODE_ELEMENT ) return; } if ( element.isIdentical( sibling ) ) { // Save the last child to be checked too, to merge things like // => var innerSibling = isNext ? element.getLast() : element.getFirst(); // Move pending nodes first into the target element. while ( pendingNodes.length ) pendingNodes.shift().move( element, !isNext ); sibling.moveChildren( element, !isNext ); sibling.remove(); // Now check the last inner child (see two comments above). if ( innerSibling && innerSibling.type == CKEDITOR.NODE_ELEMENT ) innerSibling.mergeSiblings(); } } } return function( inlineOnly ) { // Merge empty links and anchors also. (https://dev.ckeditor.com/ticket/5567) if ( !( inlineOnly === false || CKEDITOR.dtd.$removeEmpty[ this.getName() ] || this.is( 'a' ) ) ) { return; } mergeElements( this, this.getNext(), true ); mergeElements( this, this.getPrevious() ); }; } )(), /** * Shows this element (displays it). * * var element = CKEDITOR.document.getById( 'myElement' ); * element.show(); */ show: function() { this.setStyles( { display: '', visibility: '' } ); }, /** * Sets the value of an element attribute. * * var element = CKEDITOR.document.getById( 'myElement' ); * element.setAttribute( 'class', 'myClass' ); * element.setAttribute( 'title', 'This is an example' ); * * @method * @param {String} name The name of the attribute. * @param {String} value The value to be set to the attribute. * @returns {CKEDITOR.dom.element} This element instance. */ setAttribute: ( function() { var standard = function( name, value ) { this.$.setAttribute( name, value ); return this; }; if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) { return function( name, value ) { if ( name == 'class' ) this.$.className = value; else if ( name == 'style' ) this.$.style.cssText = value; else if ( name == 'tabindex' ) // Case sensitive. this.$.tabIndex = value; else if ( name == 'checked' ) this.$.checked = value; else if ( name == 'contenteditable' ) standard.call( this, 'contentEditable', value ); else standard.apply( this, arguments ); return this; }; } else if ( CKEDITOR.env.ie8Compat && CKEDITOR.env.secure ) { return function( name, value ) { // IE8 throws error when setting src attribute to non-ssl value. (https://dev.ckeditor.com/ticket/7847) if ( name == 'src' && value.match( /^http:\/\// ) ) { try { standard.apply( this, arguments ); } catch ( e ) {} } else { standard.apply( this, arguments ); } return this; }; } else { return standard; } } )(), /** * Sets the value of several element attributes. * * var element = CKEDITOR.document.getById( 'myElement' ); * element.setAttributes( { * 'class': 'myClass', * title: 'This is an example' * } ); * * @chainable * @param {Object} attributesPairs An object containing the names and * values of the attributes. * @returns {CKEDITOR.dom.element} This element instance. */ setAttributes: function( attributesPairs ) { for ( var name in attributesPairs ) this.setAttribute( name, attributesPairs[ name ] ); return this; }, /** * Sets the element value. This function is usually used with form * field element. * * @chainable * @param {String} value The element value. * @returns {CKEDITOR.dom.element} This element instance. */ setValue: function( value ) { this.$.value = value; return this; }, /** * Removes an attribute from the element. * * var element = CKEDITOR.dom.element.createFromHtml( '
' ); * element.removeAttribute( 'class' ); * * @method * @param {String} name The attribute name. */ removeAttribute: ( function() { var standard = function( name ) { this.$.removeAttribute( name ); }; if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) { return function( name ) { if ( name == 'class' ) name = 'className'; else if ( name == 'tabindex' ) name = 'tabIndex'; else if ( name == 'contenteditable' ) name = 'contentEditable'; standard.call( this, name ); }; } else { return standard; } } )(), /** * Removes all element's attributes or just given ones. * * @param {Array} [attributes] The array with attributes names. */ removeAttributes: function( attributes ) { if ( CKEDITOR.tools.isArray( attributes ) ) { for ( var i = 0; i < attributes.length; i++ ) { this.removeAttribute( attributes[ i ] ); } } else { attributes = attributes || this.getAttributes(); for ( var attr in attributes ) { attributes.hasOwnProperty( attr ) && this.removeAttribute( attr ); } } }, /** * Removes a style from the element. * * var element = CKEDITOR.dom.element.createFromHtml( '
' ); * element.removeStyle( 'display' ); * * @method * @param {String} name The style name. */ removeStyle: function( name ) { // Removes the specified property from the current style object. var $ = this.$.style; // "removeProperty" need to be specific on the following styles. if ( !$.removeProperty && ( name == 'border' || name == 'margin' || name == 'padding' ) ) { var names = expandedRules( name ); for ( var i = 0 ; i < names.length ; i++ ) this.removeStyle( names[ i ] ); return; } $.removeProperty ? $.removeProperty( name ) : $.removeAttribute( CKEDITOR.tools.cssStyleToDomStyle( name ) ); // Eventually remove empty style attribute. if ( !this.$.style.cssText ) this.removeAttribute( 'style' ); }, /** * Sets the value of an element style. * * var element = CKEDITOR.document.getById( 'myElement' ); * element.setStyle( 'background-color', '#ff0000' ); * element.setStyle( 'margin-top', '10px' ); * element.setStyle( 'float', 'right' ); * * @param {String} name The name of the style. The CSS naming notation * must be used (e.g. `background-color`). * @param {String} value The value to be set to the style. * @returns {CKEDITOR.dom.element} This element instance. */ setStyle: function( name, value ) { this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ] = value; return this; }, /** * Sets the value of several element styles. * * var element = CKEDITOR.document.getById( 'myElement' ); * element.setStyles( { * position: 'absolute', * float: 'right' * } ); * * @param {Object} stylesPairs An object containing the names and * values of the styles. * @returns {CKEDITOR.dom.element} This element instance. */ setStyles: function( stylesPairs ) { for ( var name in stylesPairs ) this.setStyle( name, stylesPairs[ name ] ); return this; }, /** * Sets the opacity of an element. * * var element = CKEDITOR.document.getById( 'myElement' ); * element.setOpacity( 0.75 ); * * @param {Number} opacity A number within the range `[0.0, 1.0]`. */ setOpacity: function( opacity ) { if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) { opacity = Math.round( opacity * 100 ); this.setStyle( 'filter', opacity >= 100 ? '' : 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + opacity + ')' ); } else { this.setStyle( 'opacity', opacity ); } }, /** * Makes the element and its children unselectable. * * var element = CKEDITOR.document.getById( 'myElement' ); * element.unselectable(); * * @method */ unselectable: function() { // CSS unselectable. this.setStyles( CKEDITOR.tools.cssVendorPrefix( 'user-select', 'none' ) ); // For IE/Opera which doesn't support for the above CSS style, // the unselectable="on" attribute only specifies the selection // process cannot start in the element itself, and it doesn't inherit. if ( CKEDITOR.env.ie ) { this.setAttribute( 'unselectable', 'on' ); var element, elements = this.getElementsByTag( '*' ); for ( var i = 0, count = elements.count() ; i < count ; i++ ) { element = elements.getItem( i ); element.setAttribute( 'unselectable', 'on' ); } } }, /** * Gets closest positioned (`position != static`) ancestor. * * @returns {CKEDITOR.dom.element} Positioned ancestor or `null`. */ getPositionedAncestor: function() { var current = this; while ( current.getName() != 'html' ) { if ( current.getComputedStyle( 'position' ) != 'static' ) return current; current = current.getParent(); } return null; }, /** * Gets this element's position in document. * * @param {CKEDITOR.dom.document} [refDocument] * @returns {Object} Element's position. * @returns {Number} return.x * @returns {Number} return.y * @todo refDocument */ getDocumentPosition: function( refDocument ) { var x = 0, y = 0, doc = this.getDocument(), body = doc.getBody(), quirks = doc.$.compatMode == 'BackCompat'; if ( document.documentElement.getBoundingClientRect && ( CKEDITOR.env.ie ? CKEDITOR.env.version !== 8 : true ) ) { var box = this.$.getBoundingClientRect(), $doc = doc.$, $docElem = $doc.documentElement; var clientTop = $docElem.clientTop || body.$.clientTop || 0, clientLeft = $docElem.clientLeft || body.$.clientLeft || 0, needAdjustScrollAndBorders = true; // https://dev.ckeditor.com/ticket/3804: getBoundingClientRect() works differently on IE and non-IE // browsers, regarding scroll positions. // // On IE, the top position of the element is always 0, no matter // how much you scrolled down. // // On other browsers, the top position of the element is negative // scrollTop. if ( CKEDITOR.env.ie ) { var inDocElem = doc.getDocumentElement().contains( this ), inBody = doc.getBody().contains( this ); needAdjustScrollAndBorders = ( quirks && inBody ) || ( !quirks && inDocElem ); } // https://dev.ckeditor.com/ticket/12747. if ( needAdjustScrollAndBorders ) { var scrollRelativeLeft, scrollRelativeTop; // See https://dev.ckeditor.com/ticket/12758 to know more about document.(documentElement|body).scroll(Left|Top) in Webkit. if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version >= 12 ) ) { scrollRelativeLeft = body.$.scrollLeft || $docElem.scrollLeft; scrollRelativeTop = body.$.scrollTop || $docElem.scrollTop; } else { var scrollRelativeElement = quirks ? body.$ : $docElem; scrollRelativeLeft = scrollRelativeElement.scrollLeft; scrollRelativeTop = scrollRelativeElement.scrollTop; } x = box.left + scrollRelativeLeft - clientLeft; y = box.top + scrollRelativeTop - clientTop; } } else { var current = this, previous = null, offsetParent; while ( current && !( current.getName() == 'body' || current.getName() == 'html' ) ) { x += current.$.offsetLeft - current.$.scrollLeft; y += current.$.offsetTop - current.$.scrollTop; // Opera includes clientTop|Left into offsetTop|Left. if ( !current.equals( this ) ) { x += ( current.$.clientLeft || 0 ); y += ( current.$.clientTop || 0 ); } var scrollElement = previous; while ( scrollElement && !scrollElement.equals( current ) ) { x -= scrollElement.$.scrollLeft; y -= scrollElement.$.scrollTop; scrollElement = scrollElement.getParent(); } previous = current; current = ( offsetParent = current.$.offsetParent ) ? new CKEDITOR.dom.element( offsetParent ) : null; } } if ( refDocument ) { var currentWindow = this.getWindow(), refWindow = refDocument.getWindow(); if ( !currentWindow.equals( refWindow ) && currentWindow.$.frameElement ) { var iframePosition = ( new CKEDITOR.dom.element( currentWindow.$.frameElement ) ).getDocumentPosition( refDocument ); x += iframePosition.x; y += iframePosition.y; } } if ( !document.documentElement.getBoundingClientRect ) { // In Firefox, we'll endup one pixel before the element positions, // so we must add it here. if ( CKEDITOR.env.gecko && !quirks ) { x += this.$.clientLeft ? 1 : 0; y += this.$.clientTop ? 1 : 0; } } return { x: x, y: y }; }, /** * Make any page element visible inside the browser viewport. * * @param {Boolean} [alignToTop=false] */ scrollIntoView: function( alignToTop ) { var parent = this.getParent(); if ( !parent ) return; // Scroll the element into parent container from the inner out. do { // Check ancestors that overflows. var overflowed = parent.$.clientWidth && parent.$.clientWidth < parent.$.scrollWidth || parent.$.clientHeight && parent.$.clientHeight < parent.$.scrollHeight; // Skip body element, which will report wrong clientHeight when containing // floated content. (https://dev.ckeditor.com/ticket/9523) if ( overflowed && !parent.is( 'body' ) ) this.scrollIntoParent( parent, alignToTop, 1 ); // Walk across the frame. if ( parent.is( 'html' ) ) { var win = parent.getWindow(); // Avoid security error. try { var iframe = win.$.frameElement; iframe && ( parent = new CKEDITOR.dom.element( iframe ) ); } catch ( er ) {} } } while ( ( parent = parent.getParent() ) ); }, /** * Make any page element visible inside one of the ancestors by scrolling the parent. * * @param {CKEDITOR.dom.element/CKEDITOR.dom.window} parent The container to scroll into. * @param {Boolean} [alignToTop] Align the element's top side with the container's * when `true` is specified; align the bottom with viewport bottom when * `false` is specified. Otherwise scroll on either side with the minimum * amount to show the element. * @param {Boolean} [hscroll] Whether horizontal overflow should be considered. */ scrollIntoParent: function( parent, alignToTop, hscroll ) { !parent && ( parent = this.getWindow() ); var doc = parent.getDocument(); var isQuirks = doc.$.compatMode == 'BackCompat'; // On window is scrolled while quirks scrolls . if ( parent instanceof CKEDITOR.dom.window ) parent = isQuirks ? doc.getBody() : doc.getDocumentElement(); // Scroll the parent by the specified amount. function scrollBy( x, y ) { // Webkit doesn't support "scrollTop/scrollLeft" // on documentElement/body element. if ( /body|html/.test( parent.getName() ) ) parent.getWindow().$.scrollBy( x, y ); else { parent.$.scrollLeft += x; parent.$.scrollTop += y; } } // Figure out the element position relative to the specified window. function screenPos( element, refWin ) { var pos = { x: 0, y: 0 }; if ( !( element.is( isQuirks ? 'body' : 'html' ) ) ) { var box = element.$.getBoundingClientRect(); pos.x = box.left, pos.y = box.top; } var win = element.getWindow(); if ( !win.equals( refWin ) ) { var outerPos = screenPos( CKEDITOR.dom.element.get( win.$.frameElement ), refWin ); pos.x += outerPos.x, pos.y += outerPos.y; } return pos; } // calculated margin size. function margin( element, side ) { return parseInt( element.getComputedStyle( 'margin-' + side ) || 0, 10 ) || 0; } // [WebKit] Reset stored scrollTop value to not break scrollIntoView() method flow. // Scrolling breaks when range.select() is used right after element.scrollIntoView(). (https://dev.ckeditor.com/ticket/14659) if ( CKEDITOR.env.webkit ) { var editor = this.getEditor( false ); if ( editor ) { editor._.previousScrollTop = null; } } var win = parent.getWindow(); var thisPos = screenPos( this, win ), parentPos = screenPos( parent, win ), eh = this.$.offsetHeight, ew = this.$.offsetWidth, ch = parent.$.clientHeight, cw = parent.$.clientWidth, lt, br; // Left-top margins. lt = { x: thisPos.x - margin( this, 'left' ) - parentPos.x || 0, y: thisPos.y - margin( this, 'top' ) - parentPos.y || 0 }; // Bottom-right margins. br = { x: thisPos.x + ew + margin( this, 'right' ) - ( ( parentPos.x ) + cw ) || 0, y: thisPos.y + eh + margin( this, 'bottom' ) - ( ( parentPos.y ) + ch ) || 0 }; // 1. Do the specified alignment as much as possible; // 2. Otherwise be smart to scroll only the minimum amount; // 3. Never cut at the top; // 4. DO NOT scroll when already visible. if ( lt.y < 0 || br.y > 0 ) scrollBy( 0, alignToTop === true ? lt.y : alignToTop === false ? br.y : lt.y < 0 ? lt.y : br.y ); if ( hscroll && ( lt.x < 0 || br.x > 0 ) ) scrollBy( lt.x < 0 ? lt.x : br.x, 0 ); }, /** * Switch the `class` attribute to reflect one of the triple states of an * element in one of {@link CKEDITOR#TRISTATE_ON}, {@link CKEDITOR#TRISTATE_OFF} * or {@link CKEDITOR#TRISTATE_DISABLED}. * * link.setState( CKEDITOR.TRISTATE_ON ); * // ... * link.setState( CKEDITOR.TRISTATE_OFF ); * // ... * link.setState( CKEDITOR.TRISTATE_DISABLED ); * // ... * * span.setState( CKEDITOR.TRISTATE_ON, 'cke_button' ); * // ... * * @param {Number} state Indicate the element state. One of {@link CKEDITOR#TRISTATE_ON}, * {@link CKEDITOR#TRISTATE_OFF}, {@link CKEDITOR#TRISTATE_DISABLED}. * @param [base='cke'] The prefix apply to each of the state class name. * @param [useAria=true] Whether toggle the ARIA state attributes besides of class name change. */ setState: function( state, base, useAria ) { base = base || 'cke'; switch ( state ) { case CKEDITOR.TRISTATE_ON: this.addClass( base + '_on' ); this.removeClass( base + '_off' ); this.removeClass( base + '_disabled' ); useAria && this.setAttribute( 'aria-pressed', true ); useAria && this.removeAttribute( 'aria-disabled' ); break; case CKEDITOR.TRISTATE_DISABLED: this.addClass( base + '_disabled' ); this.removeClass( base + '_off' ); this.removeClass( base + '_on' ); useAria && this.setAttribute( 'aria-disabled', true ); useAria && this.removeAttribute( 'aria-pressed' ); break; default: this.addClass( base + '_off' ); this.removeClass( base + '_on' ); this.removeClass( base + '_disabled' ); useAria && this.removeAttribute( 'aria-pressed' ); useAria && this.removeAttribute( 'aria-disabled' ); break; } }, /** * Returns the inner document of this `' ); return html.join( '' ); }; // IE BUG: Parent container does not resize to contain the iframe automatically. dialog.on( 'load', function() { var iframe = CKEDITOR.document.getById( _.frameId ), contentDiv = iframe.getParent(); contentDiv.addClass( 'cke_dialog_ui_input_file' ); } ); CKEDITOR.ui.dialog.labeledElement.call( this, dialog, elementDefinition, htmlList, innerHTML ); }, /** * A button for submitting the file in a file upload input. * * @class CKEDITOR.ui.dialog.fileButton * @extends CKEDITOR.ui.dialog.button * @constructor Creates a fileButton class instance. * @param {CKEDITOR.dialog} dialog Parent dialog window object. * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition * The element definition. Accepted fields: * * * `for` (Required) The file input's page and element ID * to associate with, in a two-item array format: `[ 'page_id', 'element_id' ]`. * * `validate` (Optional) The validation function. * * @param {Array} htmlList List of HTML code to output to. */ fileButton: function( dialog, elementDefinition, htmlList ) { var me = this; if ( arguments.length < 3 ) return; initPrivateObject.call( this, elementDefinition ); if ( elementDefinition.validate ) this.validate = elementDefinition.validate; var myDefinition = CKEDITOR.tools.extend( {}, elementDefinition ); var onClick = myDefinition.onClick; myDefinition.className = ( myDefinition.className ? myDefinition.className + ' ' : '' ) + 'cke_dialog_ui_button'; myDefinition.onClick = function( evt ) { var target = elementDefinition[ 'for' ]; // [ pageId, elementId ] // If exists onClick in elementDefinition, then it is called and checked response type. // If it's possible, then XHR is used, what prevents of using submit. var responseType = onClick ? onClick.call( this, evt ) : false; if ( responseType !== false ) { if ( responseType !== 'xhr' ) { dialog.getContentElement( target[ 0 ], target[ 1 ] ).submit(); } this.disable(); } }; dialog.on( 'load', function() { dialog.getContentElement( elementDefinition[ 'for' ][ 0 ], elementDefinition[ 'for' ][ 1 ] )._.buttons.push( me ); } ); CKEDITOR.ui.dialog.button.call( this, dialog, myDefinition, htmlList ); }, html: ( function() { var myHtmlRe = /^\s*<[\w:]+\s+([^>]*)?>/, theirHtmlRe = /^(\s*<[\w:]+(?:\s+[^>]*)?)((?:.|\r|\n)+)$/, emptyTagRe = /\/$/; /** * A dialog window element made from raw HTML code. * * @class CKEDITOR.ui.dialog.html * @extends CKEDITOR.ui.dialog.uiElement * @constructor Creates a html class instance. * @param {CKEDITOR.dialog} dialog Parent dialog window object. * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition Element definition. * Accepted fields: * * * `html` (Required) HTML code of this element. * * @param {Array} htmlList List of HTML code to be added to the dialog's content area. */ return function( dialog, elementDefinition, htmlList ) { if ( arguments.length < 3 ) return; var myHtmlList = [], myHtml, theirHtml = elementDefinition.html, myMatch, theirMatch; // If the HTML input doesn't contain any tags at the beginning, add a tag around it. if ( theirHtml.charAt( 0 ) != '<' ) theirHtml = '' + theirHtml + ''; // Look for focus function in definition. var focus = elementDefinition.focus; if ( focus ) { var oldFocus = this.focus; this.focus = function() { ( typeof focus == 'function' ? focus : oldFocus ).call( this ); this.fire( 'focus' ); }; if ( elementDefinition.isFocusable ) { var oldIsFocusable = this.isFocusable; this.isFocusable = oldIsFocusable; } this.keyboardFocusable = true; } CKEDITOR.ui.dialog.uiElement.call( this, dialog, elementDefinition, myHtmlList, 'span', null, null, '' ); // Append the attributes created by the uiElement call to the real HTML. myHtml = myHtmlList.join( '' ); myMatch = myHtml.match( myHtmlRe ); theirMatch = theirHtml.match( theirHtmlRe ) || [ '', '', '' ]; if ( emptyTagRe.test( theirMatch[ 1 ] ) ) { theirMatch[ 1 ] = theirMatch[ 1 ].slice( 0, -1 ); theirMatch[ 2 ] = '/' + theirMatch[ 2 ]; } htmlList.push( [ theirMatch[ 1 ], ' ', myMatch[ 1 ] || '', theirMatch[ 2 ] ].join( '' ) ); }; } )(), /** * Form fieldset for grouping dialog UI elements. * * @class CKEDITOR.ui.dialog.fieldset * @extends CKEDITOR.ui.dialog.uiElement * @constructor Creates a fieldset class instance. * @param {CKEDITOR.dialog} dialog Parent dialog window object. * @param {Array} childObjList * Array of {@link CKEDITOR.ui.dialog.uiElement} objects inside this container. * @param {Array} childHtmlList Array of HTML code that corresponds to the HTML output of all the * objects in childObjList. * @param {Array} htmlList Array of HTML code that this element will output to. * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition * The element definition. Accepted fields: * * * `label` (Optional) The legend of the this fieldset. * * `children` (Required) An array of dialog window field definitions which will be grouped inside this fieldset. * */ fieldset: function( dialog, childObjList, childHtmlList, htmlList, elementDefinition ) { var legendLabel = elementDefinition.label; /** @ignore */ var innerHTML = function() { var html = []; legendLabel && html.push( '' + legendLabel + '' ); for ( var i = 0; i < childHtmlList.length; i++ ) html.push( childHtmlList[ i ] ); return html.join( '' ); }; this._ = { children: childObjList }; CKEDITOR.ui.dialog.uiElement.call( this, dialog, elementDefinition, htmlList, 'fieldset', null, null, innerHTML ); } }, true ); CKEDITOR.ui.dialog.html.prototype = new CKEDITOR.ui.dialog.uiElement(); /** @class CKEDITOR.ui.dialog.labeledElement */ CKEDITOR.ui.dialog.labeledElement.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.uiElement(), { /** * Sets the label text of the element. * * @param {String} label The new label text. * @returns {CKEDITOR.ui.dialog.labeledElement} The current labeled element. */ setLabel: function( label ) { var node = CKEDITOR.document.getById( this._.labelId ); if ( node.getChildCount() < 1 ) ( new CKEDITOR.dom.text( label, CKEDITOR.document ) ).appendTo( node ); else node.getChild( 0 ).$.nodeValue = label; return this; }, /** * Retrieves the current label text of the elment. * * @returns {String} The current label text. */ getLabel: function() { var node = CKEDITOR.document.getById( this._.labelId ); if ( !node || node.getChildCount() < 1 ) return ''; else return node.getChild( 0 ).getText(); }, /** * Defines the `onChange` event for UI element definitions. * @property {Object} */ eventProcessors: commonEventProcessors }, true ); /** @class CKEDITOR.ui.dialog.button */ CKEDITOR.ui.dialog.button.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.uiElement(), { /** * Simulates a click to the button. * * @returns {Object} Return value of the `click` event. */ click: function() { if ( !this._.disabled ) return this.fire( 'click', { dialog: this._.dialog } ); return false; }, /** * Enables the button. */ enable: function() { this._.disabled = false; var element = this.getElement(); element && element.removeClass( 'cke_disabled' ); }, /** * Disables the button. */ disable: function() { this._.disabled = true; this.getElement().addClass( 'cke_disabled' ); }, /** * Checks whether a field is visible. * * @returns {Boolean} */ isVisible: function() { return this.getElement().getFirst().isVisible(); }, /** * Checks whether a field is enabled. Fields can be disabled by using the * {@link #disable} method and enabled by using the {@link #enable} method. * * @returns {Boolean} */ isEnabled: function() { return !this._.disabled; }, /** * Defines the `onChange` event and `onClick` for button element definitions. * * @property {Object} */ eventProcessors: CKEDITOR.tools.extend( {}, CKEDITOR.ui.dialog.uiElement.prototype.eventProcessors, { onClick: function( dialog, func ) { this.on( 'click', function() { func.apply( this, arguments ); } ); } }, true ), /** * Handler for the element's access key up event. Simulates a click to * the button. */ accessKeyUp: function() { this.click(); }, /** * Handler for the element's access key down event. Simulates a mouse * down to the button. */ accessKeyDown: function() { this.focus(); }, keyboardFocusable: true }, true ); /** @class CKEDITOR.ui.dialog.textInput */ CKEDITOR.ui.dialog.textInput.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.labeledElement(), { /** * Gets the text input DOM element under this UI object. * * @returns {CKEDITOR.dom.element} The DOM element of the text input. */ getInputElement: function() { return CKEDITOR.document.getById( this._.inputId ); }, /** * Puts focus into the text input. */ focus: function() { var me = this.selectParentTab(); // GECKO BUG: setTimeout() is needed to workaround invisible selections. setTimeout( function() { var element = me.getInputElement(); element && element.$.focus(); }, 0 ); }, /** * Selects all the text in the text input. */ select: function() { var me = this.selectParentTab(); // GECKO BUG: setTimeout() is needed to workaround invisible selections. setTimeout( function() { var e = me.getInputElement(); if ( e ) { e.$.focus(); e.$.select(); } }, 0 ); }, /** * Handler for the text input's access key up event. Makes a `select()` * call to the text input. */ accessKeyUp: function() { this.select(); }, /** * Sets the value of this text input object. * * uiElement.setValue( 'Blamo' ); * * @param {Object} value The new value. * @returns {CKEDITOR.ui.dialog.textInput} The current UI element. */ setValue: function( value ) { if ( this.bidi ) { var marker = value && value.charAt( 0 ), dir = ( marker == '\u202A' ? 'ltr' : marker == '\u202B' ? 'rtl' : null ); if ( dir ) { value = value.slice( 1 ); } // Set the marker or reset it (if dir==null). this.setDirectionMarker( dir ); } if ( !value ) { value = ''; } return CKEDITOR.ui.dialog.uiElement.prototype.setValue.apply( this, arguments ); }, /** * Gets the value of this text input object. * * @returns {String} The value. */ getValue: function() { var value = CKEDITOR.ui.dialog.uiElement.prototype.getValue.call( this ); if ( this.bidi && value ) { var dir = this.getDirectionMarker(); if ( dir ) { value = ( dir == 'ltr' ? '\u202A' : '\u202B' ) + value; } } return value; }, /** * Sets the text direction marker and the `dir` attribute of the input element. * * @since 4.5.0 * @param {String} dir The text direction. Pass `null` to reset. */ setDirectionMarker: function( dir ) { var inputElement = this.getInputElement(); if ( dir ) { inputElement.setAttributes( { dir: dir, 'data-cke-dir-marker': dir } ); // Don't remove the dir attribute if this field hasn't got the marker, // because the dir attribute could be set independently. } else if ( this.getDirectionMarker() ) { inputElement.removeAttributes( [ 'dir', 'data-cke-dir-marker' ] ); } }, /** * Gets the value of the text direction marker. * * @since 4.5.0 * @returns {String} `'ltr'`, `'rtl'` or `null` if the marker is not set. */ getDirectionMarker: function() { return this.getInputElement().data( 'cke-dir-marker' ); }, keyboardFocusable: true }, commonPrototype, true ); CKEDITOR.ui.dialog.textarea.prototype = new CKEDITOR.ui.dialog.textInput(); /** @class CKEDITOR.ui.dialog.select */ CKEDITOR.ui.dialog.select.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.labeledElement(), { /** * Gets the DOM element of the select box. * * @returns {CKEDITOR.dom.element} The `` element of this file input. * * @returns {CKEDITOR.dom.element} The file input element. */ getInputElement: function() { var frameDocument = CKEDITOR.document.getById( this._.frameId ).getFrameDocument(); return frameDocument.$.forms.length > 0 ? new CKEDITOR.dom.element( frameDocument.$.forms[ 0 ].elements[ 0 ] ) : this.getElement(); }, /** * Uploads the file in the file input. * * @returns {CKEDITOR.ui.dialog.file} This object. */ submit: function() { this.getInputElement().getParent().$.submit(); return this; }, /** * Gets the action assigned to the form. * * @returns {String} The value of the action. */ getAction: function() { return this.getInputElement().getParent().$.action; }, /** * The events must be applied to the inner input element, and * this must be done when the iframe and form have been loaded. */ registerEvents: function( definition ) { var regex = /^on([A-Z]\w+)/, match; var registerDomEvent = function( uiElement, dialog, eventName, func ) { uiElement.on( 'formLoaded', function() { uiElement.getInputElement().on( eventName, func, uiElement ); } ); }; for ( var i in definition ) { if ( !( match = i.match( regex ) ) ) continue; if ( this.eventProcessors[ i ] ) this.eventProcessors[ i ].call( this, this._.dialog, definition[ i ] ); else registerDomEvent( this, this._.dialog, match[ 1 ].toLowerCase(), definition[ i ] ); } return this; }, /** * Redraws the file input and resets the file path in the file input. * The redrawing logic is necessary because non-IE browsers tend to clear * the `' ); iframe.appendTo( body.getParent() ); } // Make the Title and Close Button unselectable. title.unselectable(); close.unselectable(); return { element: element, parts: { dialog: element.getChild( 0 ), title: title, close: close, tabs: body.getChild( 2 ), contents: body.getChild( [ 3, 0, 0, 0 ] ), footer: body.getChild( [ 3, 0, 1, 0 ] ) } }; } /** * This is the base class for runtime dialog objects. An instance of this * class represents a single named dialog for a single editor instance. * * var dialogObj = new CKEDITOR.dialog( editor, 'smiley' ); * * @class * @constructor Creates a dialog class instance. * @param {Object} editor The editor which created the dialog. * @param {String} dialogName The dialog's registered name. */ CKEDITOR.dialog = function( editor, dialogName ) { // Load the dialog definition. var definition = CKEDITOR.dialog._.dialogDefinitions[ dialogName ], defaultDefinition = CKEDITOR.tools.clone( defaultDialogDefinition ), buttonsOrder = editor.config.dialog_buttonsOrder || 'OS', dir = editor.lang.dir, tabsToRemove = {}, i, processed, stopPropagation; if ( ( buttonsOrder == 'OS' && CKEDITOR.env.mac ) || // The buttons in MacOS Apps are in reverse order (https://dev.ckeditor.com/ticket/4750) ( buttonsOrder == 'rtl' && dir == 'ltr' ) || ( buttonsOrder == 'ltr' && dir == 'rtl' ) ) defaultDefinition.buttons.reverse(); // Completes the definition with the default values. definition = CKEDITOR.tools.extend( definition( editor ), defaultDefinition ); // Clone a functionally independent copy for this dialog. definition = CKEDITOR.tools.clone( definition ); // Create a complex definition object, extending it with the API // functions. definition = new definitionObject( this, definition ); var themeBuilt = buildDialog( editor ); // Initialize some basic parameters. this._ = { editor: editor, element: themeBuilt.element, name: dialogName, contentSize: { width: 0, height: 0 }, size: { width: 0, height: 0 }, contents: {}, buttons: {}, accessKeyMap: {}, // Initialize the tab and page map. tabs: {}, tabIdList: [], currentTabId: null, currentTabIndex: null, pageCount: 0, lastTab: null, tabBarMode: false, // Initialize the tab order array for input widgets. focusList: [], currentFocusIndex: 0, hasFocus: false }; this.parts = themeBuilt.parts; CKEDITOR.tools.setTimeout( function() { editor.fire( 'ariaWidget', this.parts.contents ); }, 0, this ); // Set the startup styles for the dialog, avoiding it enlarging the // page size on the dialog creation. var startStyles = { position: CKEDITOR.env.ie6Compat ? 'absolute' : 'fixed', top: 0, visibility: 'hidden' }; startStyles[ dir == 'rtl' ? 'right' : 'left' ] = 0; this.parts.dialog.setStyles( startStyles ); // Call the CKEDITOR.event constructor to initialize this instance. CKEDITOR.event.call( this ); // Fire the "dialogDefinition" event, making it possible to customize // the dialog definition. this.definition = definition = CKEDITOR.fire( 'dialogDefinition', { name: dialogName, definition: definition }, editor ).definition; // Cache tabs that should be removed. if ( !( 'removeDialogTabs' in editor._ ) && editor.config.removeDialogTabs ) { var removeContents = editor.config.removeDialogTabs.split( ';' ); for ( i = 0; i < removeContents.length; i++ ) { var parts = removeContents[ i ].split( ':' ); if ( parts.length == 2 ) { var removeDialogName = parts[ 0 ]; if ( !tabsToRemove[ removeDialogName ] ) tabsToRemove[ removeDialogName ] = []; tabsToRemove[ removeDialogName ].push( parts[ 1 ] ); } } editor._.removeDialogTabs = tabsToRemove; } // Remove tabs of this dialog. if ( editor._.removeDialogTabs && ( tabsToRemove = editor._.removeDialogTabs[ dialogName ] ) ) { for ( i = 0; i < tabsToRemove.length; i++ ) definition.removeContents( tabsToRemove[ i ] ); } // Initialize load, show, hide, ok and cancel events. if ( definition.onLoad ) this.on( 'load', definition.onLoad ); if ( definition.onShow ) this.on( 'show', definition.onShow ); if ( definition.onHide ) this.on( 'hide', definition.onHide ); if ( definition.onOk ) { this.on( 'ok', function( evt ) { // Dialog confirm might probably introduce content changes (https://dev.ckeditor.com/ticket/5415). editor.fire( 'saveSnapshot' ); setTimeout( function() { editor.fire( 'saveSnapshot' ); }, 0 ); if ( definition.onOk.call( this, evt ) === false ) evt.data.hide = false; } ); } // Set default dialog state. this.state = CKEDITOR.DIALOG_STATE_IDLE; if ( definition.onCancel ) { this.on( 'cancel', function( evt ) { if ( definition.onCancel.call( this, evt ) === false ) evt.data.hide = false; } ); } var me = this; // Iterates over all items inside all content in the dialog, calling a // function for each of them. var iterContents = function( func ) { var contents = me._.contents, stop = false; for ( var i in contents ) { for ( var j in contents[ i ] ) { stop = func.call( this, contents[ i ][ j ] ); if ( stop ) return; } } }; this.on( 'ok', function( evt ) { iterContents( function( item ) { if ( item.validate ) { var retval = item.validate( this ), invalid = ( typeof retval == 'string' ) || retval === false; if ( invalid ) { evt.data.hide = false; evt.stop(); } handleFieldValidated.call( item, !invalid, typeof retval == 'string' ? retval : undefined ); return invalid; } } ); }, this, null, 0 ); this.on( 'cancel', function( evt ) { iterContents( function( item ) { if ( item.isChanged() ) { if ( !editor.config.dialog_noConfirmCancel && !confirm( editor.lang.common.confirmCancel ) ) // jshint ignore:line evt.data.hide = false; return true; } } ); }, this, null, 0 ); this.parts.close.on( 'click', function( evt ) { if ( this.fire( 'cancel', { hide: true } ).hide !== false ) this.hide(); evt.data.preventDefault(); }, this ); // Sort focus list according to tab order definitions. function setupFocus() { var focusList = me._.focusList; focusList.sort( function( a, b ) { // Mimics browser tab order logics; if ( a.tabIndex != b.tabIndex ) return b.tabIndex - a.tabIndex; // Sort is not stable in some browsers, // fall-back the comparator to 'focusIndex'; else return a.focusIndex - b.focusIndex; } ); var size = focusList.length; for ( var i = 0; i < size; i++ ) focusList[ i ].focusIndex = i; } // Expects 1 or -1 as an offset, meaning direction of the offset change. function changeFocus( offset ) { var focusList = me._.focusList; offset = offset || 0; if ( focusList.length < 1 ) return; var startIndex = me._.currentFocusIndex; if ( me._.tabBarMode && offset < 0 ) { // If we are in tab mode, we need to mimic that we started tabbing back from the first // focusList (so it will go to the last one). startIndex = 0; } // Trigger the 'blur' event of any input element before anything, // since certain UI updates may depend on it. try { focusList[ startIndex ].getInputElement().$.blur(); } catch ( e ) {} var currentIndex = startIndex, hasTabs = me._.pageCount > 1; do { currentIndex = currentIndex + offset; if ( hasTabs && !me._.tabBarMode && ( currentIndex == focusList.length || currentIndex == -1 ) ) { // If the dialog was not in tab mode, then focus the first tab (https://dev.ckeditor.com/ticket/13027). me._.tabBarMode = true; me._.tabs[ me._.currentTabId ][ 0 ].focus(); me._.currentFocusIndex = -1; // Early return, in order to avoid accessing focusList[ -1 ]. return; } currentIndex = ( currentIndex + focusList.length ) % focusList.length; if ( currentIndex == startIndex ) { break; } } while ( offset && !focusList[ currentIndex ].isFocusable() ); focusList[ currentIndex ].focus(); // Select whole field content. if ( focusList[ currentIndex ].type == 'text' ) focusList[ currentIndex ].select(); } this.changeFocus = changeFocus; function keydownHandler( evt ) { // If I'm not the top dialog, ignore. if ( me != CKEDITOR.dialog._.currentTop ) return; var keystroke = evt.data.getKeystroke(), rtl = editor.lang.dir == 'rtl', arrowKeys = [ 37, 38, 39, 40 ], button; processed = stopPropagation = 0; if ( keystroke == 9 || keystroke == CKEDITOR.SHIFT + 9 ) { var shiftPressed = ( keystroke == CKEDITOR.SHIFT + 9 ); changeFocus( shiftPressed ? -1 : 1 ); processed = 1; } else if ( keystroke == CKEDITOR.ALT + 121 && !me._.tabBarMode && me.getPageCount() > 1 ) { // Alt-F10 puts focus into the current tab item in the tab bar. me._.tabBarMode = true; me._.tabs[ me._.currentTabId ][ 0 ].focus(); me._.currentFocusIndex = -1; processed = 1; } else if ( CKEDITOR.tools.indexOf( arrowKeys, keystroke ) != -1 && me._.tabBarMode ) { // Array with key codes that activate previous tab. var prevKeyCodes = [ // Depending on the lang dir: right or left key rtl ? 39 : 37, // Top/bot arrow: actually for both cases it's the same. 38 ], nextId = CKEDITOR.tools.indexOf( prevKeyCodes, keystroke ) != -1 ? getPreviousVisibleTab.call( me ) : getNextVisibleTab.call( me ); me.selectPage( nextId ); me._.tabs[ nextId ][ 0 ].focus(); processed = 1; } else if ( ( keystroke == 13 || keystroke == 32 ) && me._.tabBarMode ) { this.selectPage( this._.currentTabId ); this._.tabBarMode = false; this._.currentFocusIndex = -1; changeFocus( 1 ); processed = 1; } // If user presses enter key in a text box, it implies clicking OK for the dialog. else if ( keystroke == 13 /*ENTER*/ ) { // Don't do that for a target that handles ENTER. var target = evt.data.getTarget(); if ( !target.is( 'a', 'button', 'select', 'textarea' ) && ( !target.is( 'input' ) || target.$.type != 'button' ) ) { button = this.getButton( 'ok' ); button && CKEDITOR.tools.setTimeout( button.click, 0, button ); processed = 1; } stopPropagation = 1; // Always block the propagation (https://dev.ckeditor.com/ticket/4269) } else if ( keystroke == 27 /*ESC*/ ) { button = this.getButton( 'cancel' ); // If there's a Cancel button, click it, else just fire the cancel event and hide the dialog. if ( button ) CKEDITOR.tools.setTimeout( button.click, 0, button ); else { if ( this.fire( 'cancel', { hide: true } ).hide !== false ) this.hide(); } stopPropagation = 1; // Always block the propagation (https://dev.ckeditor.com/ticket/4269) } else { return; } keypressHandler( evt ); } function keypressHandler( evt ) { if ( processed ) evt.data.preventDefault( 1 ); else if ( stopPropagation ) evt.data.stopPropagation(); } var dialogElement = this._.element; editor.focusManager.add( dialogElement, 1 ); // Add the dialog keyboard handlers. this.on( 'show', function() { dialogElement.on( 'keydown', keydownHandler, this ); // Some browsers instead, don't cancel key events in the keydown, but in the // keypress. So we must do a longer trip in those cases. (https://dev.ckeditor.com/ticket/4531,https://dev.ckeditor.com/ticket/8985) if ( CKEDITOR.env.gecko ) dialogElement.on( 'keypress', keypressHandler, this ); } ); this.on( 'hide', function() { dialogElement.removeListener( 'keydown', keydownHandler ); if ( CKEDITOR.env.gecko ) dialogElement.removeListener( 'keypress', keypressHandler ); // Reset fields state when closing dialog. iterContents( function( item ) { resetField.apply( item ); } ); } ); this.on( 'iframeAdded', function( evt ) { var doc = new CKEDITOR.dom.document( evt.data.iframe.$.contentWindow.document ); doc.on( 'keydown', keydownHandler, this, null, 0 ); } ); // Auto-focus logic in dialog. this.on( 'show', function() { // Setup tabIndex on showing the dialog instead of on loading // to allow dynamic tab order happen in dialog definition. setupFocus(); var hasTabs = me._.pageCount > 1; if ( editor.config.dialog_startupFocusTab && hasTabs ) { me._.tabBarMode = true; me._.tabs[ me._.currentTabId ][ 0 ].focus(); me._.currentFocusIndex = -1; } else if ( !this._.hasFocus ) { // https://dev.ckeditor.com/ticket/13114#comment:4. this._.currentFocusIndex = hasTabs ? -1 : this._.focusList.length - 1; // Decide where to put the initial focus. if ( definition.onFocus ) { var initialFocus = definition.onFocus.call( this ); // Focus the field that the user specified. initialFocus && initialFocus.focus(); } // Focus the first field in layout order. else { changeFocus( 1 ); } } }, this, null, 0xffffffff ); // IE6 BUG: Text fields and text areas are only half-rendered the first time the dialog appears in IE6 (https://dev.ckeditor.com/ticket/2661). // This is still needed after [2708] and [2709] because text fields in hidden TR tags are still broken. if ( CKEDITOR.env.ie6Compat ) { this.on( 'load', function() { var outer = this.getElement(), inner = outer.getFirst(); inner.remove(); inner.appendTo( outer ); }, this ); } initDragAndDrop( this ); initResizeHandles( this ); // Insert the title. ( new CKEDITOR.dom.text( definition.title, CKEDITOR.document ) ).appendTo( this.parts.title ); // Insert the tabs and contents. for ( i = 0; i < definition.contents.length; i++ ) { var page = definition.contents[ i ]; page && this.addPage( page ); } this.parts.tabs.on( 'click', function( evt ) { var target = evt.data.getTarget(); // If we aren't inside a tab, bail out. if ( target.hasClass( 'cke_dialog_tab' ) ) { // Get the ID of the tab, without the 'cke_' prefix and the unique number suffix. var id = target.$.id; this.selectPage( id.substring( 4, id.lastIndexOf( '_' ) ) ); if ( this._.tabBarMode ) { this._.tabBarMode = false; this._.currentFocusIndex = -1; changeFocus( 1 ); } evt.data.preventDefault(); } }, this ); // Insert buttons. var buttonsHtml = [], buttons = CKEDITOR.dialog._.uiElementBuilders.hbox.build( this, { type: 'hbox', className: 'cke_dialog_footer_buttons', widths: [], children: definition.buttons }, buttonsHtml ).getChild(); this.parts.footer.setHtml( buttonsHtml.join( '' ) ); for ( i = 0; i < buttons.length; i++ ) this._.buttons[ buttons[ i ].id ] = buttons[ i ]; /** * Current state of the dialog. Use the {@link #setState} method to update it. * See the {@link #event-state} event to know more. * * @readonly * @property {Number} [state=CKEDITOR.DIALOG_STATE_IDLE] */ }; // Focusable interface. Use it via dialog.addFocusable. function Focusable( dialog, element, index ) { this.element = element; this.focusIndex = index; // TODO: support tabIndex for focusables. this.tabIndex = 0; this.isFocusable = function() { return !element.getAttribute( 'disabled' ) && element.isVisible(); }; this.focus = function() { dialog._.currentFocusIndex = this.focusIndex; this.element.focus(); }; // Bind events element.on( 'keydown', function( e ) { if ( e.data.getKeystroke() in { 32: 1, 13: 1 } ) this.fire( 'click' ); } ); element.on( 'focus', function() { this.fire( 'mouseover' ); } ); element.on( 'blur', function() { this.fire( 'mouseout' ); } ); } // Re-layout the dialog on window resize. function resizeWithWindow( dialog ) { var win = CKEDITOR.document.getWindow(); function resizeHandler() { dialog.layout(); } win.on( 'resize', resizeHandler ); dialog.on( 'hide', function() { win.removeListener( 'resize', resizeHandler ); } ); } CKEDITOR.dialog.prototype = { destroy: function() { this.hide(); this._.element.remove(); }, /** * Resizes the dialog. * * dialogObj.resize( 800, 640 ); * * @method * @param {Number} width The width of the dialog in pixels. * @param {Number} height The height of the dialog in pixels. */ resize: ( function() { return function( width, height ) { if ( this._.contentSize && this._.contentSize.width == width && this._.contentSize.height == height ) return; CKEDITOR.dialog.fire( 'resize', { dialog: this, width: width, height: height }, this._.editor ); this.fire( 'resize', { width: width, height: height }, this._.editor ); var contents = this.parts.contents; contents.setStyles( { width: width + 'px', height: height + 'px' } ); // Update dialog position when dimension get changed in RTL. if ( this._.editor.lang.dir == 'rtl' && this._.position ) this._.position.x = CKEDITOR.document.getWindow().getViewPaneSize().width - this._.contentSize.width - parseInt( this._.element.getFirst().getStyle( 'right' ), 10 ); this._.contentSize = { width: width, height: height }; }; } )(), /** * Gets the current size of the dialog in pixels. * * var width = dialogObj.getSize().width; * * @returns {Object} * @returns {Number} return.width * @returns {Number} return.height */ getSize: function() { var element = this._.element.getFirst(); return { width: element.$.offsetWidth || 0, height: element.$.offsetHeight || 0 }; }, /** * Moves the dialog to an `(x, y)` coordinate relative to the window. * * dialogObj.move( 10, 40 ); * * @method * @param {Number} x The target x-coordinate. * @param {Number} y The target y-coordinate. * @param {Boolean} save Flag indicate whether the dialog position should be remembered on next open up. */ move: function( x, y, save ) { // The dialog may be fixed positioned or absolute positioned. Ask the // browser what is the current situation first. var element = this._.element.getFirst(), rtl = this._.editor.lang.dir == 'rtl'; var isFixed = element.getComputedStyle( 'position' ) == 'fixed'; // (https://dev.ckeditor.com/ticket/8888) In some cases of a very small viewport, dialog is incorrectly // positioned in IE7. It also happens that it remains sticky and user cannot // scroll down/up to reveal dialog's content below/above the viewport; this is // cumbersome. // The only way to fix this is to move mouse out of the browser and // go back to see that dialog position is automagically fixed. No events, // no style change - pure magic. This is a IE7 rendering issue, which can be // fixed with dummy style redraw on each move. if ( CKEDITOR.env.ie ) element.setStyle( 'zoom', '100%' ); if ( isFixed && this._.position && this._.position.x == x && this._.position.y == y ) return; // Save the current position. this._.position = { x: x, y: y }; // If not fixed positioned, add scroll position to the coordinates. if ( !isFixed ) { var scrollPosition = CKEDITOR.document.getWindow().getScrollPosition(); x += scrollPosition.x; y += scrollPosition.y; } // Translate coordinate for RTL. if ( rtl ) { var dialogSize = this.getSize(), viewPaneSize = CKEDITOR.document.getWindow().getViewPaneSize(); x = viewPaneSize.width - dialogSize.width - x; } var styles = { 'top': ( y > 0 ? y : 0 ) + 'px' }; styles[ rtl ? 'right' : 'left' ] = ( x > 0 ? x : 0 ) + 'px'; element.setStyles( styles ); save && ( this._.moved = 1 ); }, /** * Gets the dialog's position in the window. * * var dialogX = dialogObj.getPosition().x; * * @returns {Object} * @returns {Number} return.x * @returns {Number} return.y */ getPosition: function() { return CKEDITOR.tools.extend( {}, this._.position ); }, /** * Shows the dialog box. * * dialogObj.show(); */ show: function() { // Insert the dialog's element to the root document. var element = this._.element; var definition = this.definition; if ( !( element.getParent() && element.getParent().equals( CKEDITOR.document.getBody() ) ) ) element.appendTo( CKEDITOR.document.getBody() ); else element.setStyle( 'display', 'block' ); // First, set the dialog to an appropriate size. this.resize( this._.contentSize && this._.contentSize.width || definition.width || definition.minWidth, this._.contentSize && this._.contentSize.height || definition.height || definition.minHeight ); // Reset all inputs back to their default value. this.reset(); // Selects the first tab if no tab is already selected. if ( this._.currentTabId === null ) { this.selectPage( this.definition.contents[ 0 ].id ); } // Set z-index. if ( CKEDITOR.dialog._.currentZIndex === null ) CKEDITOR.dialog._.currentZIndex = this._.editor.config.baseFloatZIndex; this._.element.getFirst().setStyle( 'z-index', CKEDITOR.dialog._.currentZIndex += 10 ); // Maintain the dialog ordering and dialog cover. if ( CKEDITOR.dialog._.currentTop === null ) { CKEDITOR.dialog._.currentTop = this; this._.parentDialog = null; showCover( this._.editor ); } else { this._.parentDialog = CKEDITOR.dialog._.currentTop; var parentElement = this._.parentDialog.getElement().getFirst(); parentElement.$.style.zIndex -= Math.floor( this._.editor.config.baseFloatZIndex / 2 ); CKEDITOR.dialog._.currentTop = this; } element.on( 'keydown', accessKeyDownHandler ); element.on( 'keyup', accessKeyUpHandler ); // Reset the hasFocus state. this._.hasFocus = false; for ( var i in definition.contents ) { if ( !definition.contents[ i ] ) continue; var content = definition.contents[ i ], tab = this._.tabs[ content.id ], requiredContent = content.requiredContent, enableElements = 0; if ( !tab ) continue; for ( var j in this._.contents[ content.id ] ) { var elem = this._.contents[ content.id ][ j ]; if ( elem.type == 'hbox' || elem.type == 'vbox' || !elem.getInputElement() ) continue; if ( elem.requiredContent && !this._.editor.activeFilter.check( elem.requiredContent ) ) elem.disable(); else { elem.enable(); enableElements++; } } if ( !enableElements || ( requiredContent && !this._.editor.activeFilter.check( requiredContent ) ) ) tab[ 0 ].addClass( 'cke_dialog_tab_disabled' ); else tab[ 0 ].removeClass( 'cke_dialog_tab_disabled' ); } CKEDITOR.tools.setTimeout( function() { this.layout(); resizeWithWindow( this ); this.parts.dialog.setStyle( 'visibility', '' ); // Execute onLoad for the first show. this.fireOnce( 'load', {} ); CKEDITOR.ui.fire( 'ready', this ); this.fire( 'show', {} ); this._.editor.fire( 'dialogShow', this ); if ( !this._.parentDialog ) this._.editor.focusManager.lock(); // Save the initial values of the dialog. this.foreach( function( contentObj ) { contentObj.setInitValue && contentObj.setInitValue(); } ); }, 100, this ); }, /** * Rearrange the dialog to its previous position or the middle of the window. * * @since 3.5.0 */ layout: function() { var el = this.parts.dialog; var dialogSize = this.getSize(); var win = CKEDITOR.document.getWindow(), viewSize = win.getViewPaneSize(); var posX = ( viewSize.width - dialogSize.width ) / 2, posY = ( viewSize.height - dialogSize.height ) / 2; // Switch to absolute position when viewport is smaller than dialog size. if ( !CKEDITOR.env.ie6Compat ) { if ( dialogSize.height + ( posY > 0 ? posY : 0 ) > viewSize.height || dialogSize.width + ( posX > 0 ? posX : 0 ) > viewSize.width ) { el.setStyle( 'position', 'absolute' ); } else { el.setStyle( 'position', 'fixed' ); } } this.move( this._.moved ? this._.position.x : posX, this._.moved ? this._.position.y : posY ); }, /** * Executes a function for each UI element. * * @param {Function} fn Function to execute for each UI element. * @returns {CKEDITOR.dialog} The current dialog object. */ foreach: function( fn ) { for ( var i in this._.contents ) { for ( var j in this._.contents[ i ] ) { fn.call( this, this._.contents[i][j] ); } } return this; }, /** * Resets all input values in the dialog. * * dialogObj.reset(); * * @method * @chainable */ reset: ( function() { var fn = function( widget ) { if ( widget.reset ) widget.reset( 1 ); }; return function() { this.foreach( fn ); return this; }; } )(), /** * Calls the {@link CKEDITOR.dialog.definition.uiElement#setup} method of each * of the UI elements, with the arguments passed through it. * It is usually being called when the dialog is opened, to put the initial value inside the field. * * dialogObj.setupContent(); * * var timestamp = ( new Date() ).valueOf(); * dialogObj.setupContent( timestamp ); */ setupContent: function() { var args = arguments; this.foreach( function( widget ) { if ( widget.setup ) widget.setup.apply( widget, args ); } ); }, /** * Calls the {@link CKEDITOR.dialog.definition.uiElement#commit} method of each * of the UI elements, with the arguments passed through it. * It is usually being called when the user confirms the dialog, to process the values. * * dialogObj.commitContent(); * * var timestamp = ( new Date() ).valueOf(); * dialogObj.commitContent( timestamp ); */ commitContent: function() { var args = arguments; this.foreach( function( widget ) { // Make sure IE triggers "change" event on last focused input before closing the dialog. (https://dev.ckeditor.com/ticket/7915) if ( CKEDITOR.env.ie && this._.currentFocusIndex == widget.focusIndex ) widget.getInputElement().$.blur(); if ( widget.commit ) widget.commit.apply( widget, args ); } ); }, /** * Hides the dialog box. * * dialogObj.hide(); */ hide: function() { if ( !this.parts.dialog.isVisible() ) return; this.fire( 'hide', {} ); this._.editor.fire( 'dialogHide', this ); // Reset the tab page. this.selectPage( this._.tabIdList[ 0 ] ); var element = this._.element; element.setStyle( 'display', 'none' ); this.parts.dialog.setStyle( 'visibility', 'hidden' ); // Unregister all access keys associated with this dialog. unregisterAccessKey( this ); // Close any child(top) dialogs first. while ( CKEDITOR.dialog._.currentTop != this ) CKEDITOR.dialog._.currentTop.hide(); // Maintain dialog ordering and remove cover if needed. if ( !this._.parentDialog ) hideCover( this._.editor ); else { var parentElement = this._.parentDialog.getElement().getFirst(); parentElement.setStyle( 'z-index', parseInt( parentElement.$.style.zIndex, 10 ) + Math.floor( this._.editor.config.baseFloatZIndex / 2 ) ); } CKEDITOR.dialog._.currentTop = this._.parentDialog; // Deduct or clear the z-index. if ( !this._.parentDialog ) { CKEDITOR.dialog._.currentZIndex = null; // Remove access key handlers. element.removeListener( 'keydown', accessKeyDownHandler ); element.removeListener( 'keyup', accessKeyUpHandler ); var editor = this._.editor; editor.focus(); // Give a while before unlock, waiting for focus to return to the editable. (https://dev.ckeditor.com/ticket/172) setTimeout( function() { editor.focusManager.unlock(); // Fixed iOS focus issue (https://dev.ckeditor.com/ticket/12381). // Keep in mind that editor.focus() does not work in this case. if ( CKEDITOR.env.iOS ) { editor.window.focus(); } }, 0 ); } else { CKEDITOR.dialog._.currentZIndex -= 10; } delete this._.parentDialog; // Reset the initial values of the dialog. this.foreach( function( contentObj ) { contentObj.resetInitValue && contentObj.resetInitValue(); } ); // Reset dialog state back to IDLE, if busy (https://dev.ckeditor.com/ticket/13213). this.setState( CKEDITOR.DIALOG_STATE_IDLE ); }, /** * Adds a tabbed page into the dialog. * * @param {Object} contents Content definition. */ addPage: function( contents ) { if ( contents.requiredContent && !this._.editor.filter.check( contents.requiredContent ) ) return; var pageHtml = [], titleHtml = contents.label ? ' title="' + CKEDITOR.tools.htmlEncode( contents.label ) + '"' : '', vbox = CKEDITOR.dialog._.uiElementBuilders.vbox.build( this, { type: 'vbox', className: 'cke_dialog_page_contents', children: contents.elements, expand: !!contents.expand, padding: contents.padding, style: contents.style || 'width: 100%;' }, pageHtml ); var contentMap = this._.contents[ contents.id ] = {}, cursor, children = vbox.getChild(), enabledFields = 0; while ( ( cursor = children.shift() ) ) { // Count all allowed fields. if ( !cursor.notAllowed && cursor.type != 'hbox' && cursor.type != 'vbox' ) enabledFields++; contentMap[ cursor.id ] = cursor; if ( typeof cursor.getChild == 'function' ) children.push.apply( children, cursor.getChild() ); } // If all fields are disabled (because they are not allowed) hide this tab. if ( !enabledFields ) contents.hidden = true; // Create the HTML for the tab and the content block. var page = CKEDITOR.dom.element.createFromHtml( pageHtml.join( '' ) ); page.setAttribute( 'role', 'tabpanel' ); var env = CKEDITOR.env; var tabId = 'cke_' + contents.id + '_' + CKEDITOR.tools.getNextNumber(), tab = CKEDITOR.dom.element.createFromHtml( [ ' 0 ? ' cke_last' : 'cke_first' ), titleHtml, ( !!contents.hidden ? ' style="display:none"' : '' ), ' id="', tabId, '"', env.gecko && !env.hc ? '' : ' href="javascript:void(0)"', ' tabIndex="-1"', ' hidefocus="true"', ' role="tab">', contents.label, '' ].join( '' ) ); page.setAttribute( 'aria-labelledby', tabId ); // Take records for the tabs and elements created. this._.tabs[ contents.id ] = [ tab, page ]; this._.tabIdList.push( contents.id ); !contents.hidden && this._.pageCount++; this._.lastTab = tab; this.updateStyle(); // Attach the DOM nodes. page.setAttribute( 'name', contents.id ); page.appendTo( this.parts.contents ); tab.unselectable(); this.parts.tabs.append( tab ); // Add access key handlers if access key is defined. if ( contents.accessKey ) { registerAccessKey( this, this, 'CTRL+' + contents.accessKey, tabAccessKeyDown, tabAccessKeyUp ); this._.accessKeyMap[ 'CTRL+' + contents.accessKey ] = contents.id; } }, /** * Activates a tab page in the dialog by its id. * * dialogObj.selectPage( 'tab_1' ); * * @param {String} id The id of the dialog tab to be activated. */ selectPage: function( id ) { if ( this._.currentTabId == id ) return; if ( this._.tabs[ id ][ 0 ].hasClass( 'cke_dialog_tab_disabled' ) ) return; // If event was canceled - do nothing. if ( this.fire( 'selectPage', { page: id, currentPage: this._.currentTabId } ) === false ) return; // Hide the non-selected tabs and pages. for ( var i in this._.tabs ) { var tab = this._.tabs[ i ][ 0 ], page = this._.tabs[ i ][ 1 ]; if ( i != id ) { tab.removeClass( 'cke_dialog_tab_selected' ); page.hide(); } page.setAttribute( 'aria-hidden', i != id ); } var selected = this._.tabs[ id ]; selected[ 0 ].addClass( 'cke_dialog_tab_selected' ); // [IE] an invisible input[type='text'] will enlarge it's width // if it's value is long when it shows, so we clear it's value // before it shows and then recover it (https://dev.ckeditor.com/ticket/5649) if ( CKEDITOR.env.ie6Compat || CKEDITOR.env.ie7Compat ) { clearOrRecoverTextInputValue( selected[ 1 ] ); selected[ 1 ].show(); setTimeout( function() { clearOrRecoverTextInputValue( selected[ 1 ], 1 ); }, 0 ); } else { selected[ 1 ].show(); } this._.currentTabId = id; this._.currentTabIndex = CKEDITOR.tools.indexOf( this._.tabIdList, id ); }, /** * Dialog state-specific style updates. */ updateStyle: function() { // If only a single page shown, a different style is used in the central pane. this.parts.dialog[ ( this._.pageCount === 1 ? 'add' : 'remove' ) + 'Class' ]( 'cke_single_page' ); }, /** * Hides a page's tab away from the dialog. * * dialog.hidePage( 'tab_3' ); * * @param {String} id The page's Id. */ hidePage: function( id ) { var tab = this._.tabs[ id ] && this._.tabs[ id ][ 0 ]; if ( !tab || this._.pageCount == 1 || !tab.isVisible() ) return; // Switch to other tab first when we're hiding the active tab. else if ( id == this._.currentTabId ) this.selectPage( getPreviousVisibleTab.call( this ) ); tab.hide(); this._.pageCount--; this.updateStyle(); }, /** * Unhides a page's tab. * * dialog.showPage( 'tab_2' ); * * @param {String} id The page's Id. */ showPage: function( id ) { var tab = this._.tabs[ id ] && this._.tabs[ id ][ 0 ]; if ( !tab ) return; tab.show(); this._.pageCount++; this.updateStyle(); }, /** * Gets the root DOM element of the dialog. * * var dialogElement = dialogObj.getElement().getFirst(); * dialogElement.setStyle( 'padding', '5px' ); * * @returns {CKEDITOR.dom.element} The `` element containing this dialog. */ getElement: function() { return this._.element; }, /** * Gets the name of the dialog. * * var dialogName = dialogObj.getName(); * * @returns {String} The name of this dialog. */ getName: function() { return this._.name; }, /** * Gets a dialog UI element object from a dialog page. * * dialogObj.getContentElement( 'tabId', 'elementId' ).setValue( 'Example' ); * * @param {String} pageId id of dialog page. * @param {String} elementId id of UI element. * @returns {CKEDITOR.ui.dialog.uiElement} The dialog UI element. */ getContentElement: function( pageId, elementId ) { var page = this._.contents[ pageId ]; return page && page[ elementId ]; }, /** * Gets the value of a dialog UI element. * * alert( dialogObj.getValueOf( 'tabId', 'elementId' ) ); * * @param {String} pageId id of dialog page. * @param {String} elementId id of UI element. * @returns {Object} The value of the UI element. */ getValueOf: function( pageId, elementId ) { return this.getContentElement( pageId, elementId ).getValue(); }, /** * Sets the value of a dialog UI element. * * dialogObj.setValueOf( 'tabId', 'elementId', 'Example' ); * * @param {String} pageId id of the dialog page. * @param {String} elementId id of the UI element. * @param {Object} value The new value of the UI element. */ setValueOf: function( pageId, elementId, value ) { return this.getContentElement( pageId, elementId ).setValue( value ); }, /** * Gets the UI element of a button in the dialog's button row. * * @returns {CKEDITOR.ui.dialog.button} The button object. * * @param {String} id The id of the button. */ getButton: function( id ) { return this._.buttons[ id ]; }, /** * Simulates a click to a dialog button in the dialog's button row. * * @returns The return value of the dialog's `click` event. * * @param {String} id The id of the button. */ click: function( id ) { return this._.buttons[ id ].click(); }, /** * Disables a dialog button. * * @param {String} id The id of the button. */ disableButton: function( id ) { return this._.buttons[ id ].disable(); }, /** * Enables a dialog button. * * @param {String} id The id of the button. */ enableButton: function( id ) { return this._.buttons[ id ].enable(); }, /** * Gets the number of pages in the dialog. * * @returns {Number} Page count. */ getPageCount: function() { return this._.pageCount; }, /** * Gets the editor instance which opened this dialog. * * @returns {CKEDITOR.editor} Parent editor instances. */ getParentEditor: function() { return this._.editor; }, /** * Gets the element that was selected when opening the dialog, if any. * * @returns {CKEDITOR.dom.element} The element that was selected, or `null`. */ getSelectedElement: function() { return this.getParentEditor().getSelection().getSelectedElement(); }, /** * Adds element to dialog's focusable list. * * @param {CKEDITOR.dom.element} element * @param {Number} [index] */ addFocusable: function( element, index ) { if ( typeof index == 'undefined' ) { index = this._.focusList.length; this._.focusList.push( new Focusable( this, element, index ) ); } else { this._.focusList.splice( index, 0, new Focusable( this, element, index ) ); for ( var i = index + 1; i < this._.focusList.length; i++ ) this._.focusList[ i ].focusIndex++; } }, /** * Sets the dialog {@link #property-state}. * * @since 4.5.0 * @param {Number} state Either {@link CKEDITOR#DIALOG_STATE_IDLE} or {@link CKEDITOR#DIALOG_STATE_BUSY}. */ setState: function( state ) { var oldState = this.state; if ( oldState == state ) { return; } this.state = state; if ( state == CKEDITOR.DIALOG_STATE_BUSY ) { // Insert the spinner on demand. if ( !this.parts.spinner ) { var dir = this.getParentEditor().lang.dir, spinnerDef = { attributes: { 'class': 'cke_dialog_spinner' }, styles: { 'float': dir == 'rtl' ? 'right' : 'left' } }; spinnerDef.styles[ 'margin-' + ( dir == 'rtl' ? 'left' : 'right' ) ] = '8px'; this.parts.spinner = CKEDITOR.document.createElement( 'div', spinnerDef ); this.parts.spinner.setHtml( '⌛' ); this.parts.spinner.appendTo( this.parts.title, 1 ); } // Finally, show the spinner. this.parts.spinner.show(); this.getButton( 'ok' ).disable(); } else if ( state == CKEDITOR.DIALOG_STATE_IDLE ) { // Hide the spinner. But don't do anything if there is no spinner yet. this.parts.spinner && this.parts.spinner.hide(); this.getButton( 'ok' ).enable(); } this.fire( 'state', state ); } }; CKEDITOR.tools.extend( CKEDITOR.dialog, { /** * Registers a dialog. * * // Full sample plugin, which does not only register a dialog window but also adds an item to the context menu. * // To open the dialog window, choose "Open dialog" in the context menu. * CKEDITOR.plugins.add( 'myplugin', { * init: function( editor ) { * editor.addCommand( 'mydialog',new CKEDITOR.dialogCommand( 'mydialog' ) ); * * if ( editor.contextMenu ) { * editor.addMenuGroup( 'mygroup', 10 ); * editor.addMenuItem( 'My Dialog', { * label: 'Open dialog', * command: 'mydialog', * group: 'mygroup' * } ); * editor.contextMenu.addListener( function( element ) { * return { 'My Dialog': CKEDITOR.TRISTATE_OFF }; * } ); * } * * CKEDITOR.dialog.add( 'mydialog', function( api ) { * // CKEDITOR.dialog.definition * var dialogDefinition = { * title: 'Sample dialog', * minWidth: 390, * minHeight: 130, * contents: [ * { * id: 'tab1', * label: 'Label', * title: 'Title', * expand: true, * padding: 0, * elements: [ * { * type: 'html', * html: '

This is some sample HTML content.

' * }, * { * type: 'textarea', * id: 'textareaId', * rows: 4, * cols: 40 * } * ] * } * ], * buttons: [ CKEDITOR.dialog.okButton, CKEDITOR.dialog.cancelButton ], * onOk: function() { * // "this" is now a CKEDITOR.dialog object. * // Accessing dialog elements: * var textareaObj = this.getContentElement( 'tab1', 'textareaId' ); * alert( "You have entered: " + textareaObj.getValue() ); * } * }; * * return dialogDefinition; * } ); * } * } ); * * CKEDITOR.replace( 'editor1', { extraPlugins: 'myplugin' } ); * * @static * @param {String} name The dialog's name. * @param {Function/String} dialogDefinition * A function returning the dialog's definition, or the URL to the `.js` file holding the function. * The function should accept an argument `editor` which is the current editor instance, and * return an object conforming to {@link CKEDITOR.dialog.definition}. * @see CKEDITOR.dialog.definition */ add: function( name, dialogDefinition ) { // Avoid path registration from multiple instances override definition. if ( !this._.dialogDefinitions[ name ] || typeof dialogDefinition == 'function' ) this._.dialogDefinitions[ name ] = dialogDefinition; }, /** * @static * @todo */ exists: function( name ) { return !!this._.dialogDefinitions[ name ]; }, /** * @static * @todo */ getCurrent: function() { return CKEDITOR.dialog._.currentTop; }, /** * Check whether tab wasn't removed by {@link CKEDITOR.config#removeDialogTabs}. * * @since 4.1.0 * @static * @param {CKEDITOR.editor} editor * @param {String} dialogName * @param {String} tabName * @returns {Boolean} */ isTabEnabled: function( editor, dialogName, tabName ) { var cfg = editor.config.removeDialogTabs; return !( cfg && cfg.match( new RegExp( '(?:^|;)' + dialogName + ':' + tabName + '(?:$|;)', 'i' ) ) ); }, /** * The default OK button for dialogs. Fires the `ok` event and closes the dialog if the event succeeds. * * @static * @method */ okButton: ( function() { var retval = function( editor, override ) { override = override || {}; return CKEDITOR.tools.extend( { id: 'ok', type: 'button', label: editor.lang.common.ok, 'class': 'cke_dialog_ui_button_ok', onClick: function( evt ) { var dialog = evt.data.dialog; if ( dialog.fire( 'ok', { hide: true } ).hide !== false ) dialog.hide(); } }, override, true ); }; retval.type = 'button'; retval.override = function( override ) { return CKEDITOR.tools.extend( function( editor ) { return retval( editor, override ); }, { type: 'button' }, true ); }; return retval; } )(), /** * The default cancel button for dialogs. Fires the `cancel` event and * closes the dialog if no UI element value changed. * * @static * @method */ cancelButton: ( function() { var retval = function( editor, override ) { override = override || {}; return CKEDITOR.tools.extend( { id: 'cancel', type: 'button', label: editor.lang.common.cancel, 'class': 'cke_dialog_ui_button_cancel', onClick: function( evt ) { var dialog = evt.data.dialog; if ( dialog.fire( 'cancel', { hide: true } ).hide !== false ) dialog.hide(); } }, override, true ); }; retval.type = 'button'; retval.override = function( override ) { return CKEDITOR.tools.extend( function( editor ) { return retval( editor, override ); }, { type: 'button' }, true ); }; return retval; } )(), /** * Registers a dialog UI element. * * @static * @param {String} typeName The name of the UI element. * @param {Function} builder The function to build the UI element. */ addUIElement: function( typeName, builder ) { this._.uiElementBuilders[ typeName ] = builder; } } ); CKEDITOR.dialog._ = { uiElementBuilders: {}, dialogDefinitions: {}, currentTop: null, currentZIndex: null }; // "Inherit" (copy actually) from CKEDITOR.event. CKEDITOR.event.implementOn( CKEDITOR.dialog ); CKEDITOR.event.implementOn( CKEDITOR.dialog.prototype ); var defaultDialogDefinition = { resizable: CKEDITOR.DIALOG_RESIZE_BOTH, minWidth: 600, minHeight: 400, buttons: [ CKEDITOR.dialog.okButton, CKEDITOR.dialog.cancelButton ] }; // Tool function used to return an item from an array based on its id // property. var getById = function( array, id, recurse ) { for ( var i = 0, item; ( item = array[ i ] ); i++ ) { if ( item.id == id ) return item; if ( recurse && item[ recurse ] ) { var retval = getById( item[ recurse ], id, recurse ); if ( retval ) return retval; } } return null; }; // Tool function used to add an item into an array. var addById = function( array, newItem, nextSiblingId, recurse, nullIfNotFound ) { if ( nextSiblingId ) { for ( var i = 0, item; ( item = array[ i ] ); i++ ) { if ( item.id == nextSiblingId ) { array.splice( i, 0, newItem ); return newItem; } if ( recurse && item[ recurse ] ) { var retval = addById( item[ recurse ], newItem, nextSiblingId, recurse, true ); if ( retval ) return retval; } } if ( nullIfNotFound ) return null; } array.push( newItem ); return newItem; }; // Tool function used to remove an item from an array based on its id. var removeById = function( array, id, recurse ) { for ( var i = 0, item; ( item = array[ i ] ); i++ ) { if ( item.id == id ) return array.splice( i, 1 ); if ( recurse && item[ recurse ] ) { var retval = removeById( item[ recurse ], id, recurse ); if ( retval ) return retval; } } return null; }; /** * This class is not really part of the API. It is the `definition` property value * passed to `dialogDefinition` event handlers. * * CKEDITOR.on( 'dialogDefinition', function( evt ) { * var definition = evt.data.definition; * var content = definition.getContents( 'page1' ); * // ... * } ); * * @private * @class CKEDITOR.dialog.definitionObject * @extends CKEDITOR.dialog.definition * @constructor Creates a definitionObject class instance. */ var definitionObject = function( dialog, dialogDefinition ) { // TODO : Check if needed. this.dialog = dialog; // Transform the contents entries in contentObjects. var contents = dialogDefinition.contents; for ( var i = 0, content; ( content = contents[ i ] ); i++ ) contents[ i ] = content && new contentObject( dialog, content ); CKEDITOR.tools.extend( this, dialogDefinition ); }; definitionObject.prototype = { /** * Gets a content definition. * * @param {String} id The id of the content definition. * @returns {CKEDITOR.dialog.definition.content} The content definition matching id. */ getContents: function( id ) { return getById( this.contents, id ); }, /** * Gets a button definition. * * @param {String} id The id of the button definition. * @returns {CKEDITOR.dialog.definition.button} The button definition matching id. */ getButton: function( id ) { return getById( this.buttons, id ); }, /** * Adds a content definition object under this dialog definition. * * @param {CKEDITOR.dialog.definition.content} contentDefinition The * content definition. * @param {String} [nextSiblingId] The id of an existing content * definition which the new content definition will be inserted * before. Omit if the new content definition is to be inserted as * the last item. * @returns {CKEDITOR.dialog.definition.content} The inserted content definition. */ addContents: function( contentDefinition, nextSiblingId ) { return addById( this.contents, contentDefinition, nextSiblingId ); }, /** * Adds a button definition object under this dialog definition. * * @param {CKEDITOR.dialog.definition.button} buttonDefinition The * button definition. * @param {String} [nextSiblingId] The id of an existing button * definition which the new button definition will be inserted * before. Omit if the new button definition is to be inserted as * the last item. * @returns {CKEDITOR.dialog.definition.button} The inserted button definition. */ addButton: function( buttonDefinition, nextSiblingId ) { return addById( this.buttons, buttonDefinition, nextSiblingId ); }, /** * Removes a content definition from this dialog definition. * * @param {String} id The id of the content definition to be removed. * @returns {CKEDITOR.dialog.definition.content} The removed content definition. */ removeContents: function( id ) { removeById( this.contents, id ); }, /** * Removes a button definition from the dialog definition. * * @param {String} id The id of the button definition to be removed. * @returns {CKEDITOR.dialog.definition.button} The removed button definition. */ removeButton: function( id ) { removeById( this.buttons, id ); } }; /** * This class is not really part of the API. It is the template of the * objects representing content pages inside the * CKEDITOR.dialog.definitionObject. * * CKEDITOR.on( 'dialogDefinition', function( evt ) { * var definition = evt.data.definition; * var content = definition.getContents( 'page1' ); * content.remove( 'textInput1' ); * // ... * } ); * * @private * @class CKEDITOR.dialog.definition.contentObject * @constructor Creates a contentObject class instance. */ function contentObject( dialog, contentDefinition ) { this._ = { dialog: dialog }; CKEDITOR.tools.extend( this, contentDefinition ); } contentObject.prototype = { /** * Gets a UI element definition under the content definition. * * @param {String} id The id of the UI element definition. * @returns {CKEDITOR.dialog.definition.uiElement} */ get: function( id ) { return getById( this.elements, id, 'children' ); }, /** * Adds a UI element definition to the content definition. * * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition The * UI elemnet definition to be added. * @param {String} nextSiblingId The id of an existing UI element * definition which the new UI element definition will be inserted * before. Omit if the new button definition is to be inserted as * the last item. * @returns {CKEDITOR.dialog.definition.uiElement} The element definition inserted. */ add: function( elementDefinition, nextSiblingId ) { return addById( this.elements, elementDefinition, nextSiblingId, 'children' ); }, /** * Removes a UI element definition from the content definition. * * @param {String} id The id of the UI element definition to be removed. * @returns {CKEDITOR.dialog.definition.uiElement} The element definition removed. */ remove: function( id ) { removeById( this.elements, id, 'children' ); } }; function initDragAndDrop( dialog ) { var lastCoords = null, abstractDialogCoords = null, editor = dialog.getParentEditor(), magnetDistance = editor.config.dialog_magnetDistance, margins = CKEDITOR.skin.margins || [ 0, 0, 0, 0 ]; if ( typeof magnetDistance == 'undefined' ) magnetDistance = 20; function mouseMoveHandler( evt ) { var dialogSize = dialog.getSize(), viewPaneSize = CKEDITOR.document.getWindow().getViewPaneSize(), x = evt.data.$.screenX, y = evt.data.$.screenY, dx = x - lastCoords.x, dy = y - lastCoords.y, realX, realY; lastCoords = { x: x, y: y }; abstractDialogCoords.x += dx; abstractDialogCoords.y += dy; if ( abstractDialogCoords.x + margins[ 3 ] < magnetDistance ) realX = -margins[ 3 ]; else if ( abstractDialogCoords.x - margins[ 1 ] > viewPaneSize.width - dialogSize.width - magnetDistance ) realX = viewPaneSize.width - dialogSize.width + ( editor.lang.dir == 'rtl' ? 0 : margins[ 1 ] ); else realX = abstractDialogCoords.x; if ( abstractDialogCoords.y + margins[ 0 ] < magnetDistance ) realY = -margins[ 0 ]; else if ( abstractDialogCoords.y - margins[ 2 ] > viewPaneSize.height - dialogSize.height - magnetDistance ) realY = viewPaneSize.height - dialogSize.height + margins[ 2 ]; else realY = abstractDialogCoords.y; dialog.move( realX, realY, 1 ); evt.data.preventDefault(); } function mouseUpHandler() { CKEDITOR.document.removeListener( 'mousemove', mouseMoveHandler ); CKEDITOR.document.removeListener( 'mouseup', mouseUpHandler ); if ( CKEDITOR.env.ie6Compat ) { var coverDoc = currentCover.getChild( 0 ).getFrameDocument(); coverDoc.removeListener( 'mousemove', mouseMoveHandler ); coverDoc.removeListener( 'mouseup', mouseUpHandler ); } } dialog.parts.title.on( 'mousedown', function( evt ) { lastCoords = { x: evt.data.$.screenX, y: evt.data.$.screenY }; CKEDITOR.document.on( 'mousemove', mouseMoveHandler ); CKEDITOR.document.on( 'mouseup', mouseUpHandler ); abstractDialogCoords = dialog.getPosition(); if ( CKEDITOR.env.ie6Compat ) { var coverDoc = currentCover.getChild( 0 ).getFrameDocument(); coverDoc.on( 'mousemove', mouseMoveHandler ); coverDoc.on( 'mouseup', mouseUpHandler ); } evt.data.preventDefault(); }, dialog ); } function initResizeHandles( dialog ) { var def = dialog.definition, resizable = def.resizable; if ( resizable == CKEDITOR.DIALOG_RESIZE_NONE ) return; var editor = dialog.getParentEditor(); var wrapperWidth, wrapperHeight, viewSize, origin, startSize, dialogCover; var mouseDownFn = CKEDITOR.tools.addFunction( function( $event ) { startSize = dialog.getSize(); var content = dialog.parts.contents, iframeDialog = content.$.getElementsByTagName( 'iframe' ).length; // Shim to help capturing "mousemove" over iframe. if ( iframeDialog ) { dialogCover = CKEDITOR.dom.element.createFromHtml( '
' ); content.append( dialogCover ); } // Calculate the offset between content and chrome size. wrapperHeight = startSize.height - dialog.parts.contents.getSize( 'height', !( CKEDITOR.env.gecko || CKEDITOR.env.ie && CKEDITOR.env.quirks ) ); wrapperWidth = startSize.width - dialog.parts.contents.getSize( 'width', 1 ); origin = { x: $event.screenX, y: $event.screenY }; viewSize = CKEDITOR.document.getWindow().getViewPaneSize(); CKEDITOR.document.on( 'mousemove', mouseMoveHandler ); CKEDITOR.document.on( 'mouseup', mouseUpHandler ); if ( CKEDITOR.env.ie6Compat ) { var coverDoc = currentCover.getChild( 0 ).getFrameDocument(); coverDoc.on( 'mousemove', mouseMoveHandler ); coverDoc.on( 'mouseup', mouseUpHandler ); } $event.preventDefault && $event.preventDefault(); } ); // Prepend the grip to the dialog. dialog.on( 'load', function() { var direction = ''; if ( resizable == CKEDITOR.DIALOG_RESIZE_WIDTH ) direction = ' cke_resizer_horizontal'; else if ( resizable == CKEDITOR.DIALOG_RESIZE_HEIGHT ) direction = ' cke_resizer_vertical'; var resizer = CKEDITOR.dom.element.createFromHtml( '' + // BLACK LOWER RIGHT TRIANGLE (ltr) // BLACK LOWER LEFT TRIANGLE (rtl) ( editor.lang.dir == 'ltr' ? '\u25E2' : '\u25E3' ) + '
' ); dialog.parts.footer.append( resizer, 1 ); } ); editor.on( 'destroy', function() { CKEDITOR.tools.removeFunction( mouseDownFn ); } ); function mouseMoveHandler( evt ) { var rtl = editor.lang.dir == 'rtl', dx = ( evt.data.$.screenX - origin.x ) * ( rtl ? -1 : 1 ), dy = evt.data.$.screenY - origin.y, width = startSize.width, height = startSize.height, internalWidth = width + dx * ( dialog._.moved ? 1 : 2 ), internalHeight = height + dy * ( dialog._.moved ? 1 : 2 ), element = dialog._.element.getFirst(), right = rtl && element.getComputedStyle( 'right' ), position = dialog.getPosition(); if ( position.y + internalHeight > viewSize.height ) internalHeight = viewSize.height - position.y; if ( ( rtl ? right : position.x ) + internalWidth > viewSize.width ) internalWidth = viewSize.width - ( rtl ? right : position.x ); // Make sure the dialog will not be resized to the wrong side when it's in the leftmost position for RTL. if ( ( resizable == CKEDITOR.DIALOG_RESIZE_WIDTH || resizable == CKEDITOR.DIALOG_RESIZE_BOTH ) ) width = Math.max( def.minWidth || 0, internalWidth - wrapperWidth ); if ( resizable == CKEDITOR.DIALOG_RESIZE_HEIGHT || resizable == CKEDITOR.DIALOG_RESIZE_BOTH ) height = Math.max( def.minHeight || 0, internalHeight - wrapperHeight ); dialog.resize( width, height ); if ( !dialog._.moved ) dialog.layout(); evt.data.preventDefault(); } function mouseUpHandler() { CKEDITOR.document.removeListener( 'mouseup', mouseUpHandler ); CKEDITOR.document.removeListener( 'mousemove', mouseMoveHandler ); if ( dialogCover ) { dialogCover.remove(); dialogCover = null; } if ( CKEDITOR.env.ie6Compat ) { var coverDoc = currentCover.getChild( 0 ).getFrameDocument(); coverDoc.removeListener( 'mouseup', mouseUpHandler ); coverDoc.removeListener( 'mousemove', mouseMoveHandler ); } } } var resizeCover; // Caching resuable covers and allowing only one cover // on screen. var covers = {}, currentCover; function cancelEvent( ev ) { ev.data.preventDefault( 1 ); } function showCover( editor ) { var win = CKEDITOR.document.getWindow(), config = editor.config, skinName = ( CKEDITOR.skinName || editor.config.skin ), backgroundColorStyle = config.dialog_backgroundCoverColor || ( skinName == 'moono-lisa' ? 'black' : 'white' ), backgroundCoverOpacity = config.dialog_backgroundCoverOpacity, baseFloatZIndex = config.baseFloatZIndex, coverKey = CKEDITOR.tools.genKey( backgroundColorStyle, backgroundCoverOpacity, baseFloatZIndex ), coverElement = covers[ coverKey ]; if ( !coverElement ) { var html = [ '
' ]; if ( CKEDITOR.env.ie6Compat ) { // Support for custom document.domain in IE. var iframeHtml = ''; html.push( '' + '' ); } html.push( '
' ); coverElement = CKEDITOR.dom.element.createFromHtml( html.join( '' ) ); coverElement.setOpacity( backgroundCoverOpacity !== undefined ? backgroundCoverOpacity : 0.5 ); coverElement.on( 'keydown', cancelEvent ); coverElement.on( 'keypress', cancelEvent ); coverElement.on( 'keyup', cancelEvent ); coverElement.appendTo( CKEDITOR.document.getBody() ); covers[ coverKey ] = coverElement; } else { coverElement.show(); } // Makes the dialog cover a focus holder as well. editor.focusManager.add( coverElement ); currentCover = coverElement; var resizeFunc = function() { var size = win.getViewPaneSize(); coverElement.setStyles( { width: size.width + 'px', height: size.height + 'px' } ); }; var scrollFunc = function() { var pos = win.getScrollPosition(), cursor = CKEDITOR.dialog._.currentTop; coverElement.setStyles( { left: pos.x + 'px', top: pos.y + 'px' } ); if ( cursor ) { do { var dialogPos = cursor.getPosition(); cursor.move( dialogPos.x, dialogPos.y ); } while ( ( cursor = cursor._.parentDialog ) ); } }; resizeCover = resizeFunc; win.on( 'resize', resizeFunc ); resizeFunc(); // Using Safari/Mac, focus must be kept where it is (https://dev.ckeditor.com/ticket/7027) if ( !( CKEDITOR.env.mac && CKEDITOR.env.webkit ) ) coverElement.focus(); if ( CKEDITOR.env.ie6Compat ) { // IE BUG: win.$.onscroll assignment doesn't work.. it must be window.onscroll. // So we need to invent a really funny way to make it work. var myScrollHandler = function() { scrollFunc(); myScrollHandler.prevScrollHandler.apply( this, arguments ); }; win.$.setTimeout( function() { myScrollHandler.prevScrollHandler = window.onscroll || function() {}; window.onscroll = myScrollHandler; }, 0 ); scrollFunc(); } } function hideCover( editor ) { if ( !currentCover ) return; editor.focusManager.remove( currentCover ); var win = CKEDITOR.document.getWindow(); currentCover.hide(); // Remove the current cover reference once the cover is removed (#589). currentCover = null; win.removeListener( 'resize', resizeCover ); if ( CKEDITOR.env.ie6Compat ) { win.$.setTimeout( function() { var prevScrollHandler = window.onscroll && window.onscroll.prevScrollHandler; window.onscroll = prevScrollHandler || null; }, 0 ); } resizeCover = null; } function removeCovers() { for ( var coverId in covers ) covers[ coverId ].remove(); covers = {}; } var accessKeyProcessors = {}; var accessKeyDownHandler = function( evt ) { var ctrl = evt.data.$.ctrlKey || evt.data.$.metaKey, alt = evt.data.$.altKey, shift = evt.data.$.shiftKey, key = String.fromCharCode( evt.data.$.keyCode ), keyProcessor = accessKeyProcessors[ ( ctrl ? 'CTRL+' : '' ) + ( alt ? 'ALT+' : '' ) + ( shift ? 'SHIFT+' : '' ) + key ]; if ( !keyProcessor || !keyProcessor.length ) return; keyProcessor = keyProcessor[ keyProcessor.length - 1 ]; keyProcessor.keydown && keyProcessor.keydown.call( keyProcessor.uiElement, keyProcessor.dialog, keyProcessor.key ); evt.data.preventDefault(); }; var accessKeyUpHandler = function( evt ) { var ctrl = evt.data.$.ctrlKey || evt.data.$.metaKey, alt = evt.data.$.altKey, shift = evt.data.$.shiftKey, key = String.fromCharCode( evt.data.$.keyCode ), keyProcessor = accessKeyProcessors[ ( ctrl ? 'CTRL+' : '' ) + ( alt ? 'ALT+' : '' ) + ( shift ? 'SHIFT+' : '' ) + key ]; if ( !keyProcessor || !keyProcessor.length ) return; keyProcessor = keyProcessor[ keyProcessor.length - 1 ]; if ( keyProcessor.keyup ) { keyProcessor.keyup.call( keyProcessor.uiElement, keyProcessor.dialog, keyProcessor.key ); evt.data.preventDefault(); } }; var registerAccessKey = function( uiElement, dialog, key, downFunc, upFunc ) { var procList = accessKeyProcessors[ key ] || ( accessKeyProcessors[ key ] = [] ); procList.push( { uiElement: uiElement, dialog: dialog, key: key, keyup: upFunc || uiElement.accessKeyUp, keydown: downFunc || uiElement.accessKeyDown } ); }; var unregisterAccessKey = function( obj ) { for ( var i in accessKeyProcessors ) { var list = accessKeyProcessors[ i ]; for ( var j = list.length - 1; j >= 0; j-- ) { if ( list[ j ].dialog == obj || list[ j ].uiElement == obj ) list.splice( j, 1 ); } if ( list.length === 0 ) delete accessKeyProcessors[ i ]; } }; var tabAccessKeyUp = function( dialog, key ) { if ( dialog._.accessKeyMap[ key ] ) dialog.selectPage( dialog._.accessKeyMap[ key ] ); }; var tabAccessKeyDown = function() {}; ( function() { CKEDITOR.ui.dialog = { /** * The base class of all dialog UI elements. * * @class CKEDITOR.ui.dialog.uiElement * @constructor Creates a uiElement class instance. * @param {CKEDITOR.dialog} dialog Parent dialog object. * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition Element * definition. * * Accepted fields: * * * `id` (Required) The id of the UI element. See {@link CKEDITOR.dialog#getContentElement}. * * `type` (Required) The type of the UI element. The * value to this field specifies which UI element class will be used to * generate the final widget. * * `title` (Optional) The popup tooltip for the UI * element. * * `hidden` (Optional) A flag that tells if the element * should be initially visible. * * `className` (Optional) Additional CSS class names * to add to the UI element. Separated by space. * * `style` (Optional) Additional CSS inline styles * to add to the UI element. A semicolon (;) is required after the last * style declaration. * * `accessKey` (Optional) The alphanumeric access key * for this element. Access keys are automatically prefixed by CTRL. * * `on*` (Optional) Any UI element definition field that * starts with `on` followed immediately by a capital letter and * probably more letters is an event handler. Event handlers may be further * divided into registered event handlers and DOM event handlers. Please * refer to {@link CKEDITOR.ui.dialog.uiElement#registerEvents} and * {@link CKEDITOR.ui.dialog.uiElement#eventProcessors} for more information. * * @param {Array} htmlList * List of HTML code to be added to the dialog's content area. * @param {Function/String} [nodeNameArg='div'] * A function returning a string, or a simple string for the node name for * the root DOM node. * @param {Function/Object} [stylesArg={}] * A function returning an object, or a simple object for CSS styles applied * to the DOM node. * @param {Function/Object} [attributesArg={}] * A fucntion returning an object, or a simple object for attributes applied * to the DOM node. * @param {Function/String} [contentsArg=''] * A function returning a string, or a simple string for the HTML code inside * the root DOM node. Default is empty string. */ uiElement: function( dialog, elementDefinition, htmlList, nodeNameArg, stylesArg, attributesArg, contentsArg ) { if ( arguments.length < 4 ) return; var nodeName = ( nodeNameArg.call ? nodeNameArg( elementDefinition ) : nodeNameArg ) || 'div', html = [ '<', nodeName, ' ' ], styles = ( stylesArg && stylesArg.call ? stylesArg( elementDefinition ) : stylesArg ) || {}, attributes = ( attributesArg && attributesArg.call ? attributesArg( elementDefinition ) : attributesArg ) || {}, innerHTML = ( contentsArg && contentsArg.call ? contentsArg.call( this, dialog, elementDefinition ) : contentsArg ) || '', domId = this.domId = attributes.id || CKEDITOR.tools.getNextId() + '_uiElement', i; if ( elementDefinition.requiredContent && !dialog.getParentEditor().filter.check( elementDefinition.requiredContent ) ) { styles.display = 'none'; this.notAllowed = true; } // Set the id, a unique id is required for getElement() to work. attributes.id = domId; // Set the type and definition CSS class names. var classes = {}; if ( elementDefinition.type ) classes[ 'cke_dialog_ui_' + elementDefinition.type ] = 1; if ( elementDefinition.className ) classes[ elementDefinition.className ] = 1; if ( elementDefinition.disabled ) classes.cke_disabled = 1; var attributeClasses = ( attributes[ 'class' ] && attributes[ 'class' ].split ) ? attributes[ 'class' ].split( ' ' ) : []; for ( i = 0; i < attributeClasses.length; i++ ) { if ( attributeClasses[ i ] ) classes[ attributeClasses[ i ] ] = 1; } var finalClasses = []; for ( i in classes ) finalClasses.push( i ); attributes[ 'class' ] = finalClasses.join( ' ' ); // Set the popup tooltop. if ( elementDefinition.title ) attributes.title = elementDefinition.title; // Write the inline CSS styles. var styleStr = ( elementDefinition.style || '' ).split( ';' ); // Element alignment support. if ( elementDefinition.align ) { var align = elementDefinition.align; styles[ 'margin-left' ] = align == 'left' ? 0 : 'auto'; styles[ 'margin-right' ] = align == 'right' ? 0 : 'auto'; } for ( i in styles ) styleStr.push( i + ':' + styles[ i ] ); if ( elementDefinition.hidden ) styleStr.push( 'display:none' ); for ( i = styleStr.length - 1; i >= 0; i-- ) { if ( styleStr[ i ] === '' ) styleStr.splice( i, 1 ); } if ( styleStr.length > 0 ) attributes.style = ( attributes.style ? ( attributes.style + '; ' ) : '' ) + styleStr.join( '; ' ); // Write the attributes. for ( i in attributes ) html.push( i + '="' + CKEDITOR.tools.htmlEncode( attributes[ i ] ) + '" ' ); // Write the content HTML. html.push( '>', innerHTML, '' ); // Add contents to the parent HTML array. htmlList.push( html.join( '' ) ); ( this._ || ( this._ = {} ) ).dialog = dialog; // Override isChanged if it is defined in element definition. if ( typeof elementDefinition.isChanged == 'boolean' ) this.isChanged = function() { return elementDefinition.isChanged; }; if ( typeof elementDefinition.isChanged == 'function' ) this.isChanged = elementDefinition.isChanged; // Overload 'get(set)Value' on definition. if ( typeof elementDefinition.setValue == 'function' ) { this.setValue = CKEDITOR.tools.override( this.setValue, function( org ) { return function( val ) { org.call( this, elementDefinition.setValue.call( this, val ) ); }; } ); } if ( typeof elementDefinition.getValue == 'function' ) { this.getValue = CKEDITOR.tools.override( this.getValue, function( org ) { return function() { return elementDefinition.getValue.call( this, org.call( this ) ); }; } ); } // Add events. CKEDITOR.event.implementOn( this ); this.registerEvents( elementDefinition ); if ( this.accessKeyUp && this.accessKeyDown && elementDefinition.accessKey ) registerAccessKey( this, dialog, 'CTRL+' + elementDefinition.accessKey ); var me = this; dialog.on( 'load', function() { var input = me.getInputElement(); if ( input ) { var focusClass = me.type in { 'checkbox': 1, 'ratio': 1 } && CKEDITOR.env.ie && CKEDITOR.env.version < 8 ? 'cke_dialog_ui_focused' : ''; input.on( 'focus', function() { dialog._.tabBarMode = false; dialog._.hasFocus = true; me.fire( 'focus' ); focusClass && this.addClass( focusClass ); } ); input.on( 'blur', function() { me.fire( 'blur' ); focusClass && this.removeClass( focusClass ); } ); } } ); // Completes this object with everything we have in the // definition. CKEDITOR.tools.extend( this, elementDefinition ); // Register the object as a tab focus if it can be included. if ( this.keyboardFocusable ) { this.tabIndex = elementDefinition.tabIndex || 0; this.focusIndex = dialog._.focusList.push( this ) - 1; this.on( 'focus', function() { dialog._.currentFocusIndex = me.focusIndex; } ); } }, /** * Horizontal layout box for dialog UI elements, auto-expends to available width of container. * * @class CKEDITOR.ui.dialog.hbox * @extends CKEDITOR.ui.dialog.uiElement * @constructor Creates a hbox class instance. * @param {CKEDITOR.dialog} dialog Parent dialog object. * @param {Array} childObjList * Array of {@link CKEDITOR.ui.dialog.uiElement} objects inside this container. * @param {Array} childHtmlList * Array of HTML code that correspond to the HTML output of all the * objects in childObjList. * @param {Array} htmlList * Array of HTML code that this element will output to. * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition * The element definition. Accepted fields: * * * `widths` (Optional) The widths of child cells. * * `height` (Optional) The height of the layout. * * `padding` (Optional) The padding width inside child cells. * * `align` (Optional) The alignment of the whole layout. */ hbox: function( dialog, childObjList, childHtmlList, htmlList, elementDefinition ) { if ( arguments.length < 4 ) return; this._ || ( this._ = {} ); var children = this._.children = childObjList, widths = elementDefinition && elementDefinition.widths || null, height = elementDefinition && elementDefinition.height || null, styles = {}, i; /** @ignore */ var innerHTML = function() { var html = [ '' ]; for ( i = 0; i < childHtmlList.length; i++ ) { var className = 'cke_dialog_ui_hbox_child', styles = []; if ( i === 0 ) { className = 'cke_dialog_ui_hbox_first'; } if ( i == childHtmlList.length - 1 ) { className = 'cke_dialog_ui_hbox_last'; } html.push( ' 0 ) { html.push( 'style="' + styles.join( '; ' ) + '" ' ); } html.push( '>', childHtmlList[ i ], '' ); } html.push( '' ); return html.join( '' ); }; var attribs = { role: 'presentation' }; elementDefinition && elementDefinition.align && ( attribs.align = elementDefinition.align ); CKEDITOR.ui.dialog.uiElement.call( this, dialog, elementDefinition || { type: 'hbox' }, htmlList, 'table', styles, attribs, innerHTML ); }, /** * Vertical layout box for dialog UI elements. * * @class CKEDITOR.ui.dialog.vbox * @extends CKEDITOR.ui.dialog.hbox * @constructor Creates a vbox class instance. * @param {CKEDITOR.dialog} dialog Parent dialog object. * @param {Array} childObjList * Array of {@link CKEDITOR.ui.dialog.uiElement} objects inside this container. * @param {Array} childHtmlList * Array of HTML code that correspond to the HTML output of all the * objects in childObjList. * @param {Array} htmlList Array of HTML code that this element will output to. * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition * The element definition. Accepted fields: * * * `width` (Optional) The width of the layout. * * `heights` (Optional) The heights of individual cells. * * `align` (Optional) The alignment of the layout. * * `padding` (Optional) The padding width inside child cells. * * `expand` (Optional) Whether the layout should expand * vertically to fill its container. */ vbox: function( dialog, childObjList, childHtmlList, htmlList, elementDefinition ) { if ( arguments.length < 3 ) return; this._ || ( this._ = {} ); var children = this._.children = childObjList, width = elementDefinition && elementDefinition.width || null, heights = elementDefinition && elementDefinition.heights || null; /** @ignore */ var innerHTML = function() { var html = [ '' ); for ( var i = 0; i < childHtmlList.length; i++ ) { var styles = []; html.push( '' ); } html.push( '
0 ) html.push( 'style="', styles.join( '; ' ), '" ' ); html.push( ' class="cke_dialog_ui_vbox_child">', childHtmlList[ i ], '
' ); return html.join( '' ); }; CKEDITOR.ui.dialog.uiElement.call( this, dialog, elementDefinition || { type: 'vbox' }, htmlList, 'div', null, { role: 'presentation' }, innerHTML ); } }; } )(); /** @class CKEDITOR.ui.dialog.uiElement */ CKEDITOR.ui.dialog.uiElement.prototype = { /** * Gets the root DOM element of this dialog UI object. * * uiElement.getElement().hide(); * * @returns {CKEDITOR.dom.element} Root DOM element of UI object. */ getElement: function() { return CKEDITOR.document.getById( this.domId ); }, /** * Gets the DOM element that the user inputs values. * * This function is used by {@link #setValue}, {@link #getValue} and {@link #focus}. It should * be overrided in child classes where the input element isn't the root * element. * * var rawValue = textInput.getInputElement().$.value; * * @returns {CKEDITOR.dom.element} The element where the user input values. */ getInputElement: function() { return this.getElement(); }, /** * Gets the parent dialog object containing this UI element. * * var dialog = uiElement.getDialog(); * * @returns {CKEDITOR.dialog} Parent dialog object. */ getDialog: function() { return this._.dialog; }, /** * Sets the value of this dialog UI object. * * uiElement.setValue( 'Dingo' ); * * @chainable * @param {Object} value The new value. * @param {Boolean} noChangeEvent Internal commit, to supress `change` event on this element. */ setValue: function( value, noChangeEvent ) { this.getInputElement().setValue( value ); !noChangeEvent && this.fire( 'change', { value: value } ); return this; }, /** * Gets the current value of this dialog UI object. * * var myValue = uiElement.getValue(); * * @returns {Object} The current value. */ getValue: function() { return this.getInputElement().getValue(); }, /** * Tells whether the UI object's value has changed. * * if ( uiElement.isChanged() ) * confirm( 'Value changed! Continue?' ); * * @returns {Boolean} `true` if changed, `false` if not changed. */ isChanged: function() { // Override in input classes. return false; }, /** * Selects the parent tab of this element. Usually called by focus() or overridden focus() methods. * * focus : function() { * this.selectParentTab(); * // do something else. * } * * @chainable */ selectParentTab: function() { var element = this.getInputElement(), cursor = element, tabId; while ( ( cursor = cursor.getParent() ) && cursor.$.className.search( 'cke_dialog_page_contents' ) == -1 ) { } // Some widgets don't have parent tabs (e.g. OK and Cancel buttons). if ( !cursor ) return this; tabId = cursor.getAttribute( 'name' ); // Avoid duplicate select. if ( this._.dialog._.currentTabId != tabId ) this._.dialog.selectPage( tabId ); return this; }, /** * Puts the focus to the UI object. Switches tabs if the UI object isn't in the active tab page. * * uiElement.focus(); * * @chainable */ focus: function() { this.selectParentTab().getInputElement().focus(); return this; }, /** * Registers the `on*` event handlers defined in the element definition. * * The default behavior of this function is: * * 1. If the on* event is defined in the class's eventProcesors list, * then the registration is delegated to the corresponding function * in the eventProcessors list. * 2. If the on* event is not defined in the eventProcessors list, then * register the event handler under the corresponding DOM event of * the UI element's input DOM element (as defined by the return value * of {@link #getInputElement}). * * This function is only called at UI element instantiation, but can * be overridded in child classes if they require more flexibility. * * @chainable * @param {CKEDITOR.dialog.definition.uiElement} definition The UI element * definition. */ registerEvents: function( definition ) { var regex = /^on([A-Z]\w+)/, match; var registerDomEvent = function( uiElement, dialog, eventName, func ) { dialog.on( 'load', function() { uiElement.getInputElement().on( eventName, func, uiElement ); } ); }; for ( var i in definition ) { if ( !( match = i.match( regex ) ) ) continue; if ( this.eventProcessors[ i ] ) this.eventProcessors[ i ].call( this, this._.dialog, definition[ i ] ); else registerDomEvent( this, this._.dialog, match[ 1 ].toLowerCase(), definition[ i ] ); } return this; }, /** * The event processor list used by * {@link CKEDITOR.ui.dialog.uiElement#getInputElement} at UI element * instantiation. The default list defines three `on*` events: * * 1. `onLoad` - Called when the element's parent dialog opens for the * first time. * 2. `onShow` - Called whenever the element's parent dialog opens. * 3. `onHide` - Called whenever the element's parent dialog closes. * * // This connects the 'click' event in CKEDITOR.ui.dialog.button to onClick * // handlers in the UI element's definitions. * CKEDITOR.ui.dialog.button.eventProcessors = CKEDITOR.tools.extend( {}, * CKEDITOR.ui.dialog.uiElement.prototype.eventProcessors, * { onClick : function( dialog, func ) { this.on( 'click', func ); } }, * true * ); * * @property {Object} */ eventProcessors: { onLoad: function( dialog, func ) { dialog.on( 'load', func, this ); }, onShow: function( dialog, func ) { dialog.on( 'show', func, this ); }, onHide: function( dialog, func ) { dialog.on( 'hide', func, this ); } }, /** * The default handler for a UI element's access key down event, which * tries to put focus to the UI element. * * Can be overridded in child classes for more sophisticaed behavior. * * @param {CKEDITOR.dialog} dialog The parent dialog object. * @param {String} key The key combination pressed. Since access keys * are defined to always include the `CTRL` key, its value should always * include a `'CTRL+'` prefix. */ accessKeyDown: function() { this.focus(); }, /** * The default handler for a UI element's access key up event, which * does nothing. * * Can be overridded in child classes for more sophisticated behavior. * * @param {CKEDITOR.dialog} dialog The parent dialog object. * @param {String} key The key combination pressed. Since access keys * are defined to always include the `CTRL` key, its value should always * include a `'CTRL+'` prefix. */ accessKeyUp: function() {}, /** * Disables a UI element. */ disable: function() { var element = this.getElement(), input = this.getInputElement(); input.setAttribute( 'disabled', 'true' ); element.addClass( 'cke_disabled' ); }, /** * Enables a UI element. */ enable: function() { var element = this.getElement(), input = this.getInputElement(); input.removeAttribute( 'disabled' ); element.removeClass( 'cke_disabled' ); }, /** * Determines whether an UI element is enabled or not. * * @returns {Boolean} Whether the UI element is enabled. */ isEnabled: function() { return !this.getElement().hasClass( 'cke_disabled' ); }, /** * Determines whether an UI element is visible or not. * * @returns {Boolean} Whether the UI element is visible. */ isVisible: function() { return this.getInputElement().isVisible(); }, /** * Determines whether an UI element is focus-able or not. * Focus-able is defined as being both visible and enabled. * * @returns {Boolean} Whether the UI element can be focused. */ isFocusable: function() { if ( !this.isEnabled() || !this.isVisible() ) return false; return true; } }; /** @class CKEDITOR.ui.dialog.hbox */ CKEDITOR.ui.dialog.hbox.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.uiElement(), { /** * Gets a child UI element inside this container. * * var checkbox = hbox.getChild( [0,1] ); * checkbox.setValue( true ); * * @param {Array/Number} indices An array or a single number to indicate the child's * position in the container's descendant tree. Omit to get all the children in an array. * @returns {Array/CKEDITOR.ui.dialog.uiElement} Array of all UI elements in the container * if no argument given, or the specified UI element if indices is given. */ getChild: function( indices ) { // If no arguments, return a clone of the children array. if ( arguments.length < 1 ) return this._.children.concat(); // If indices isn't array, make it one. if ( !indices.splice ) indices = [ indices ]; // Retrieve the child element according to tree position. if ( indices.length < 2 ) return this._.children[ indices[ 0 ] ]; else return ( this._.children[ indices[ 0 ] ] && this._.children[ indices[ 0 ] ].getChild ) ? this._.children[ indices[ 0 ] ].getChild( indices.slice( 1, indices.length ) ) : null; } }, true ); CKEDITOR.ui.dialog.vbox.prototype = new CKEDITOR.ui.dialog.hbox(); ( function() { var commonBuilder = { build: function( dialog, elementDefinition, output ) { var children = elementDefinition.children, child, childHtmlList = [], childObjList = []; for ( var i = 0; ( i < children.length && ( child = children[ i ] ) ); i++ ) { var childHtml = []; childHtmlList.push( childHtml ); childObjList.push( CKEDITOR.dialog._.uiElementBuilders[ child.type ].build( dialog, child, childHtml ) ); } return new CKEDITOR.ui.dialog[ elementDefinition.type ]( dialog, childObjList, childHtmlList, output, elementDefinition ); } }; CKEDITOR.dialog.addUIElement( 'hbox', commonBuilder ); CKEDITOR.dialog.addUIElement( 'vbox', commonBuilder ); } )(); /** * Generic dialog command. It opens a specific dialog when executed. * * // Register the "link" command which opens the "link" dialog. * editor.addCommand( 'link', new CKEDITOR.dialogCommand( 'link' ) ); * * @class * @constructor Creates a dialogCommand class instance. * @extends CKEDITOR.commandDefinition * @param {String} dialogName The name of the dialog to open when executing * this command. * @param {Object} [ext] Additional command definition's properties. * @param {String} [ext.tabId] You can provide additional property (`tabId`) if you wish to open the dialog on a specific tabId. * * // Open the dialog on the 'keystroke' tabId. * editor.addCommand( 'keystroke', new CKEDITOR.dialogCommand( 'a11yHelp', { tabId: 'keystroke' } ) ); */ CKEDITOR.dialogCommand = function( dialogName, ext ) { this.dialogName = dialogName; CKEDITOR.tools.extend( this, ext, true ); }; CKEDITOR.dialogCommand.prototype = { exec: function( editor ) { var tabId = this.tabId; editor.openDialog( this.dialogName, function( dialog ) { // Select different tab if it's provided (#830). if ( tabId ) { dialog.selectPage( tabId ); } } ); }, // Dialog commands just open a dialog ui, thus require no undo logic, // undo support should dedicate to specific dialog implementation. canUndo: false, editorFocus: 1 }; ( function() { var notEmptyRegex = /^([a]|[^a])+$/, integerRegex = /^\d*$/, numberRegex = /^\d*(?:\.\d+)?$/, htmlLengthRegex = /^(((\d*(\.\d+))|(\d*))(px|\%)?)?$/, cssLengthRegex = /^(((\d*(\.\d+))|(\d*))(px|em|ex|in|cm|mm|pt|pc|\%)?)?$/i, inlineStyleRegex = /^(\s*[\w-]+\s*:\s*[^:;]+(?:;|$))*$/; /** * {@link CKEDITOR.dialog Dialog} `OR` logical value indicates the * relation between validation functions. * * @readonly * @property {Number} [=1] * @member CKEDITOR */ CKEDITOR.VALIDATE_OR = 1; /** * {@link CKEDITOR.dialog Dialog} `AND` logical value indicates the * relation between validation functions. * * @readonly * @property {Number} [=2] * @member CKEDITOR */ CKEDITOR.VALIDATE_AND = 2; /** * The namespace with dialog helper validation functions. * * @class * @singleton */ CKEDITOR.dialog.validate = { /** * Performs validation functions composition. * * ```javascript * CKEDITOR.dialog.validate.functions( * CKEDITOR.dialog.validate.notEmpty( 'Value is required.' ), * CKEDITOR.dialog.validate.number( 'Value is not a number.' ), * 'error!' * ); * ``` * * @param {Function...} validators Validation functions which will be composed into a single validator. * @param {String} [msg] Error message returned by the composed validation function. * @param {Number} [relation=CKEDITOR.VALIDATE_OR] Indicates a relation between validation functions. * Use {@link CKEDITOR#VALIDATE_OR} or {@link CKEDITOR#VALIDATE_AND}. * * @returns {Function} Composed validation function. */ functions: function() { var args = arguments; return function() { // It's important for validate functions to be able to accept the value // as argument in addition to this.getValue(), so that it is possible to // combine validate functions together to make more sophisticated // validators. var value = this && this.getValue ? this.getValue() : args[ 0 ]; var msg, relation = CKEDITOR.VALIDATE_AND, functions = [], i; for ( i = 0; i < args.length; i++ ) { if ( typeof args[ i ] == 'function' ) functions.push( args[ i ] ); else break; } if ( i < args.length && typeof args[ i ] == 'string' ) { msg = args[ i ]; i++; } if ( i < args.length && typeof args[ i ] == 'number' ) relation = args[ i ]; var passed = ( relation == CKEDITOR.VALIDATE_AND ? true : false ); for ( i = 0; i < functions.length; i++ ) { if ( relation == CKEDITOR.VALIDATE_AND ) passed = passed && functions[ i ]( value ); else passed = passed || functions[ i ]( value ); } return !passed ? msg : true; }; }, /** * Checks if a dialog UI element value meets the regex condition. * * ```javascript * CKEDITOR.dialog.validate.regex( 'error!', /^\d*$/ )( '123' ) // true * CKEDITOR.dialog.validate.regex( 'error!' )( '123.321' ) // error! * ``` * * @param {RegExp} regex Regular expression used to validate the value. * @param {String} msg Validator error message. * @returns {Function} Validation function. */ regex: function( regex, msg ) { /* * Can be greatly shortened by deriving from functions validator if code size * turns out to be more important than performance. */ return function() { var value = this && this.getValue ? this.getValue() : arguments[ 0 ]; return !regex.test( value ) ? msg : true; }; }, /** * Checks if a dialog UI element value is not an empty string. * * ```javascript * CKEDITOR.dialog.validate.notEmpty( 'error!' )( 'test' ) // true * CKEDITOR.dialog.validate.notEmpty( 'error!' )( ' ' ) // error! * ``` * * @param {String} msg Validator error message. * @returns {Function} Validation function. */ notEmpty: function( msg ) { return this.regex( notEmptyRegex, msg ); }, /** * Checks if a dialog UI element value is an Integer. * * ```javascript * CKEDITOR.dialog.validate.integer( 'error!' )( '123' ) // true * CKEDITOR.dialog.validate.integer( 'error!' )( '123.321' ) // error! * ``` * * @param {String} msg Validator error message. * @returns {Function} Validation function. */ integer: function( msg ) { return this.regex( integerRegex, msg ); }, /** * Checks if a dialog UI element value is a Number. * * ```javascript * CKEDITOR.dialog.validate.number( 'error!' )( '123' ) // true * CKEDITOR.dialog.validate.number( 'error!' )( 'test' ) // error! * ``` * * @param {String} msg Validator error message. * @returns {Function} Validation function. */ 'number': function( msg ) { return this.regex( numberRegex, msg ); }, /** * Checks if a dialog UI element value is a correct CSS length value. * * It allows `px`, `em`, `ex`, `in`, `cm`, `mm`, `pt`, `pc` units. * * ```javascript * CKEDITOR.dialog.validate.cssLength( 'error!' )( '10pt' ) // true * CKEDITOR.dialog.validate.cssLength( 'error!' )( 'solid' ) // error! * ``` * * @param {String} msg Validator error message. * @returns {Function} Validation function. */ 'cssLength': function( msg ) { return this.functions( function( val ) { return cssLengthRegex.test( CKEDITOR.tools.trim( val ) ); }, msg ); }, /** * Checks if a dialog UI element value is a correct HTML length value. * * It allows `px` units. * * ```javascript * CKEDITOR.dialog.validate.htmlLength( 'error!' )( '10px' ) // true * CKEDITOR.dialog.validate.htmlLength( 'error!' )( 'solid' ) // error! * ``` * * @param {String} msg Validator error message. * @returns {Function} Validation function. */ 'htmlLength': function( msg ) { return this.functions( function( val ) { return htmlLengthRegex.test( CKEDITOR.tools.trim( val ) ); }, msg ); }, /** * Checks if a dialog UI element value is a correct CSS inline style. * * ```javascript * CKEDITOR.dialog.validate.inlineStyle( 'error!' )( 'height: 10px; width: 20px;' ) // true * CKEDITOR.dialog.validate.inlineStyle( 'error!' )( 'test' ) // error! * ``` * * @param {String} msg Validator error message. * @returns {Function} Validation function. */ 'inlineStyle': function( msg ) { return this.functions( function( val ) { return inlineStyleRegex.test( CKEDITOR.tools.trim( val ) ); }, msg ); }, /** * Checks if a dialog UI element value and the given value are equal. * * ```javascript * CKEDITOR.dialog.validate.equals( 'foo', 'error!' )( 'foo' ) // true * CKEDITOR.dialog.validate.equals( 'foo', 'error!' )( 'baz' ) // error! * ``` * * @param {String} value The value to compare. * @param {String} msg Validator error message. * @returns {Function} Validation function. */ equals: function( value, msg ) { return this.functions( function( val ) { return val == value; }, msg ); }, /** * Checks if a dialog UI element value and the given value are not equal. * * ```javascript * CKEDITOR.dialog.validate.notEqual( 'foo', 'error!' )( 'baz' ) // true * CKEDITOR.dialog.validate.notEqual( 'foo', 'error!' )( 'foo' ) // error! * ``` * * @param {String} value The value to compare. * @param {String} msg Validator error message. * @returns {Function} Validation function. */ notEqual: function( value, msg ) { return this.functions( function( val ) { return val != value; }, msg ); } }; CKEDITOR.on( 'instanceDestroyed', function( evt ) { // Remove dialog cover on last instance destroy. if ( CKEDITOR.tools.isEmpty( CKEDITOR.instances ) ) { var currentTopDialog; while ( ( currentTopDialog = CKEDITOR.dialog._.currentTop ) ) currentTopDialog.hide(); removeCovers(); } var dialogs = evt.editor._.storedDialogs; for ( var name in dialogs ) dialogs[ name ].destroy(); } ); } )(); // Extend the CKEDITOR.editor class with dialog specific functions. CKEDITOR.tools.extend( CKEDITOR.editor.prototype, { /** * Loads and opens a registered dialog. * * CKEDITOR.instances.editor1.openDialog( 'smiley' ); * * @member CKEDITOR.editor * @param {String} dialogName The registered name of the dialog. * @param {Function} callback The function to be invoked after dialog instance created. * @returns {CKEDITOR.dialog} The dialog object corresponding to the dialog displayed. * `null` if the dialog name is not registered. * @see CKEDITOR.dialog#add */ openDialog: function( dialogName, callback ) { var dialog = null, dialogDefinitions = CKEDITOR.dialog._.dialogDefinitions[ dialogName ]; if ( CKEDITOR.dialog._.currentTop === null ) showCover( this ); // If the dialogDefinition is already loaded, open it immediately. if ( typeof dialogDefinitions == 'function' ) { var storedDialogs = this._.storedDialogs || ( this._.storedDialogs = {} ); dialog = storedDialogs[ dialogName ] || ( storedDialogs[ dialogName ] = new CKEDITOR.dialog( this, dialogName ) ); callback && callback.call( dialog, dialog ); dialog.show(); } else if ( dialogDefinitions == 'failed' ) { hideCover( this ); throw new Error( '[CKEDITOR.dialog.openDialog] Dialog "' + dialogName + '" failed when loading definition.' ); } else if ( typeof dialogDefinitions == 'string' ) { CKEDITOR.scriptLoader.load( CKEDITOR.getUrl( dialogDefinitions ), function() { var dialogDefinition = CKEDITOR.dialog._.dialogDefinitions[ dialogName ]; // In case of plugin error, mark it as loading failed. if ( typeof dialogDefinition != 'function' ) CKEDITOR.dialog._.dialogDefinitions[ dialogName ] = 'failed'; this.openDialog( dialogName, callback ); }, this, 0, 1 ); } CKEDITOR.skin.loadPart( 'dialog' ); return dialog; } } ); } )(); CKEDITOR.plugins.add( 'dialog', { requires: 'dialogui', init: function( editor ) { editor.on( 'doubleclick', function( evt ) { if ( evt.data.dialog ) editor.openDialog( evt.data.dialog ); }, null, null, 999 ); } } ); // Dialog related configurations. /** * The color of the dialog background cover. It should be a valid CSS color string. * * config.dialog_backgroundCoverColor = 'rgb(255, 254, 253)'; * * @cfg {String} [dialog_backgroundCoverColor='white'] * @member CKEDITOR.config */ /** * The opacity of the dialog background cover. It should be a number within the * range `[0.0, 1.0]`. * * config.dialog_backgroundCoverOpacity = 0.7; * * @cfg {Number} [dialog_backgroundCoverOpacity=0.5] * @member CKEDITOR.config */ /** * If the dialog has more than one tab, put focus into the first tab as soon as dialog is opened. * * config.dialog_startupFocusTab = true; * * @cfg {Boolean} [dialog_startupFocusTab=false] * @member CKEDITOR.config */ /** * The distance of magnetic borders used in moving and resizing dialogs, * measured in pixels. * * config.dialog_magnetDistance = 30; * * @cfg {Number} [dialog_magnetDistance=20] * @member CKEDITOR.config */ /** * The guideline to follow when generating the dialog buttons. There are 3 possible options: * * * `'OS'` - the buttons will be displayed in the default order of the user's OS; * * `'ltr'` - for Left-To-Right order; * * `'rtl'` - for Right-To-Left order. * * Example: * * config.dialog_buttonsOrder = 'rtl'; * * @since 3.5.0 * @cfg {String} [dialog_buttonsOrder='OS'] * @member CKEDITOR.config */ /** * The dialog contents to removed. It's a string composed by dialog name and tab name with a colon between them. * * Separate each pair with semicolon (see example). * * **Note:** All names are case-sensitive. * * **Note:** Be cautious when specifying dialog tabs that are mandatory, * like `'info'`, dialog functionality might be broken because of this! * * config.removeDialogTabs = 'flash:advanced;image:Link'; * * @since 3.5.0 * @cfg {String} [removeDialogTabs=''] * @member CKEDITOR.config */ /** * Tells if user should not be asked to confirm close, if any dialog field was modified. * By default it is set to `false` meaning that the confirmation dialog will be shown. * * config.dialog_noConfirmCancel = true; * * @since 4.3.0 * @cfg {Boolean} [dialog_noConfirmCancel=false] * @member CKEDITOR.config */ /** * Event fired when a dialog definition is about to be used to create a dialog into * an editor instance. This event makes it possible to customize the definition * before creating it. * * Note that this event is called only the first time a specific dialog is * opened. Successive openings will use the cached dialog, and this event will * not get fired. * * @event dialogDefinition * @member CKEDITOR * @param {CKEDITOR.dialog.definition} data The dialog defination that * is being loaded. * @param {CKEDITOR.editor} editor The editor instance that will use the dialog. */ /** * Event fired when a tab is going to be selected in a dialog. * * @event selectPage * @member CKEDITOR.dialog * @param data * @param {String} data.page The id of the page that it's gonna be selected. * @param {String} data.currentPage The id of the current page. */ /** * Event fired when the user tries to dismiss a dialog. * * @event cancel * @member CKEDITOR.dialog * @param data * @param {Boolean} data.hide Whether the event should proceed or not. */ /** * Event fired when the user tries to confirm a dialog. * * @event ok * @member CKEDITOR.dialog * @param data * @param {Boolean} data.hide Whether the event should proceed or not. */ /** * Event fired when a dialog is shown. * * @event show * @member CKEDITOR.dialog */ /** * Event fired when a dialog is shown. * * @event dialogShow * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param {CKEDITOR.dialog} data The opened dialog instance. */ /** * Event fired when a dialog is hidden. * * @event hide * @member CKEDITOR.dialog */ /** * Event fired when a dialog is hidden. * * @event dialogHide * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param {CKEDITOR.dialog} data The hidden dialog instance. */ /** * Event fired when a dialog is being resized. The event is fired on * both the {@link CKEDITOR.dialog} object and the dialog instance * since 3.5.3, previously it was only available in the global object. * * @static * @event resize * @member CKEDITOR.dialog * @param data * @param {CKEDITOR.dialog} data.dialog The dialog being resized (if * it is fired on the dialog itself, this parameter is not sent). * @param {String} data.skin The skin name. * @param {Number} data.width The new width. * @param {Number} data.height The new height. */ /** * Event fired when a dialog is being resized. The event is fired on * both the {@link CKEDITOR.dialog} object and the dialog instance * since 3.5.3, previously it was only available in the global object. * * @since 3.5.0 * @event resize * @member CKEDITOR.dialog * @param data * @param {Number} data.width The new width. * @param {Number} data.height The new height. */ /** * Event fired when the dialog state changes, usually by {@link CKEDITOR.dialog#setState}. * * @since 4.5.0 * @event state * @member CKEDITOR.dialog * @param data * @param {Number} data The new state. Either {@link CKEDITOR#DIALOG_STATE_IDLE} or {@link CKEDITOR#DIALOG_STATE_BUSY}. */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview Plugin definition for the a11yhelp, which provides a dialog * with accessibility related help. */ ( function() { var pluginName = 'a11yhelp', commandName = 'a11yHelp'; CKEDITOR.plugins.add( pluginName, { requires: 'dialog', // List of available localizations. // jscs:disable availableLangs: { af:1,ar:1,az:1,bg:1,ca:1,cs:1,cy:1,da:1,de:1,'de-ch':1,el:1,en:1,'en-au':1,'en-gb':1,eo:1,es:1,'es-mx':1,et:1,eu:1,fa:1,fi:1,fo:1,fr:1,'fr-ca':1,gl:1,gu:1,he:1,hi:1,hr:1,hu:1,id:1,it:1,ja:1,km:1,ko:1,ku:1,lt:1,lv:1,mk:1,mn:1,nb:1,nl:1,no:1,oc:1,pl:1,pt:1,'pt-br':1,ro:1,ru:1,si:1,sk:1,sl:1,sq:1,sr:1,'sr-latn':1,sv:1,th:1,tr:1,tt:1,ug:1,uk:1,vi:1,zh:1,'zh-cn':1 }, // jscs:enable init: function( editor ) { var plugin = this; editor.addCommand( commandName, { exec: function() { var langCode = editor.langCode; langCode = plugin.availableLangs[ langCode ] ? langCode : plugin.availableLangs[ langCode.replace( /-.*/, '' ) ] ? langCode.replace( /-.*/, '' ) : 'en'; CKEDITOR.scriptLoader.load( CKEDITOR.getUrl( plugin.path + 'dialogs/lang/' + langCode + '.js' ), function() { editor.lang.a11yhelp = plugin.langEntries[ langCode ]; editor.openDialog( commandName ); } ); }, modes: { wysiwyg: 1, source: 1 }, readOnly: 1, canUndo: false } ); editor.setKeystroke( CKEDITOR.ALT + 48 /*0*/, 'a11yHelp' ); CKEDITOR.dialog.add( commandName, this.path + 'dialogs/a11yhelp.js' ); editor.on( 'ariaEditorHelpLabel', function( evt ) { evt.data.label = editor.lang.common.editorHelp; } ); } } ); } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'about', { requires: 'dialog', // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { var command = editor.addCommand( 'about', new CKEDITOR.dialogCommand( 'about' ) ); command.modes = { wysiwyg: 1, source: 1 }; command.canUndo = false; command.readOnly = 1; editor.ui.addButton && editor.ui.addButton( 'About', { label: editor.lang.about.dlgTitle, command: 'about', toolbar: 'about' } ); CKEDITOR.dialog.add( 'about', this.path + 'dialogs/about.js' ); } } ); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'basicstyles', { // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { var order = 0; // All buttons use the same code to register. So, to avoid // duplications, let's use this tool function. var addButtonCommand = function( buttonName, buttonLabel, commandName, styleDefiniton ) { // Disable the command if no definition is configured. if ( !styleDefiniton ) return; var style = new CKEDITOR.style( styleDefiniton ), forms = contentForms[ commandName ]; // Put the style as the most important form. forms.unshift( style ); // Listen to contextual style activation. editor.attachStyleStateChange( style, function( state ) { !editor.readOnly && editor.getCommand( commandName ).setState( state ); } ); // Create the command that can be used to apply the style. editor.addCommand( commandName, new CKEDITOR.styleCommand( style, { contentForms: forms } ) ); // Register the button, if the button plugin is loaded. if ( editor.ui.addButton ) { editor.ui.addButton( buttonName, { label: buttonLabel, command: commandName, toolbar: 'basicstyles,' + ( order += 10 ) } ); } }; var contentForms = { bold: [ 'strong', 'b', [ 'span', function( el ) { var fw = el.styles[ 'font-weight' ]; return fw == 'bold' || +fw >= 700; } ] ], italic: [ 'em', 'i', [ 'span', function( el ) { return el.styles[ 'font-style' ] == 'italic'; } ] ], underline: [ 'u', [ 'span', function( el ) { return el.styles[ 'text-decoration' ] == 'underline'; } ] ], strike: [ 's', 'strike', [ 'span', function( el ) { return el.styles[ 'text-decoration' ] == 'line-through'; } ] ], subscript: [ 'sub' ], superscript: [ 'sup' ] }, config = editor.config, lang = editor.lang.basicstyles; addButtonCommand( 'Bold', lang.bold, 'bold', config.coreStyles_bold ); addButtonCommand( 'Italic', lang.italic, 'italic', config.coreStyles_italic ); addButtonCommand( 'Underline', lang.underline, 'underline', config.coreStyles_underline ); addButtonCommand( 'Strike', lang.strike, 'strike', config.coreStyles_strike ); addButtonCommand( 'Subscript', lang.subscript, 'subscript', config.coreStyles_subscript ); addButtonCommand( 'Superscript', lang.superscript, 'superscript', config.coreStyles_superscript ); editor.setKeystroke( [ [ CKEDITOR.CTRL + 66 /*B*/, 'bold' ], [ CKEDITOR.CTRL + 73 /*I*/, 'italic' ], [ CKEDITOR.CTRL + 85 /*U*/, 'underline' ] ] ); } } ); // Basic Inline Styles. /** * The style definition that applies the **bold** style to the text. * * Read more in the {@glink features/basicstyles documentation} * and see the {@glink examples/basicstyles example}. * * config.coreStyles_bold = { element: 'b', overrides: 'strong' }; * * config.coreStyles_bold = { * element: 'span', * attributes: { 'class': 'Bold' } * }; * * @cfg * @member CKEDITOR.config */ CKEDITOR.config.coreStyles_bold = { element: 'strong', overrides: 'b' }; /** * The style definition that applies the *italics* style to the text. * * Read more in the {@glink features/basicstyles documentation} * and see the {@glink examples/basicstyles example}. * * config.coreStyles_italic = { element: 'i', overrides: 'em' }; * * CKEDITOR.config.coreStyles_italic = { * element: 'span', * attributes: { 'class': 'Italic' } * }; * * @cfg * @member CKEDITOR.config */ CKEDITOR.config.coreStyles_italic = { element: 'em', overrides: 'i' }; /** * The style definition that applies the underline style to the text. * * Read more in the {@glink features/basicstyles documentation} * and see the {@glink examples/basicstyles example}. * * CKEDITOR.config.coreStyles_underline = { * element: 'span', * attributes: { 'class': 'Underline' } * }; * * @cfg * @member CKEDITOR.config */ CKEDITOR.config.coreStyles_underline = { element: 'u' }; /** * The style definition that applies the strikethrough style to the text. * * Read more in the {@glink features/basicstyles documentation} * and see the {@glink examples/basicstyles example}. * * CKEDITOR.config.coreStyles_strike = { * element: 'span', * attributes: { 'class': 'Strikethrough' }, * overrides: 'strike' * }; * * @cfg * @member CKEDITOR.config */ CKEDITOR.config.coreStyles_strike = { element: 's', overrides: 'strike' }; /** * The style definition that applies the subscript style to the text. * * Read more in the {@glink features/basicstyles documentation} * and see the {@glink examples/basicstyles example}. * * CKEDITOR.config.coreStyles_subscript = { * element: 'span', * attributes: { 'class': 'Subscript' }, * overrides: 'sub' * }; * * @cfg * @member CKEDITOR.config */ CKEDITOR.config.coreStyles_subscript = { element: 'sub' }; /** * The style definition that applies the superscript style to the text. * * Read more in the {@glink features/basicstyles documentation} * and see the {@glink examples/basicstyles example}. * * CKEDITOR.config.coreStyles_superscript = { * element: 'span', * attributes: { 'class': 'Superscript' }, * overrides: 'sup' * }; * * @cfg * @member CKEDITOR.config */ CKEDITOR.config.coreStyles_superscript = { element: 'sup' }; /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ ( function() { function noBlockLeft( bqBlock ) { for ( var i = 0, length = bqBlock.getChildCount(), child; i < length && ( child = bqBlock.getChild( i ) ); i++ ) { if ( child.type == CKEDITOR.NODE_ELEMENT && child.isBlockBoundary() ) return false; } return true; } var commandObject = { exec: function( editor ) { var state = editor.getCommand( 'blockquote' ).state, selection = editor.getSelection(), range = selection && selection.getRanges()[ 0 ]; if ( !range ) return; var bookmarks = selection.createBookmarks(); // Kludge for https://dev.ckeditor.com/ticket/1592: if the bookmark nodes are in the beginning of // blockquote, then move them to the nearest block element in the // blockquote. if ( CKEDITOR.env.ie ) { var bookmarkStart = bookmarks[ 0 ].startNode, bookmarkEnd = bookmarks[ 0 ].endNode, cursor; if ( bookmarkStart && bookmarkStart.getParent().getName() == 'blockquote' ) { cursor = bookmarkStart; while ( ( cursor = cursor.getNext() ) ) { if ( cursor.type == CKEDITOR.NODE_ELEMENT && cursor.isBlockBoundary() ) { bookmarkStart.move( cursor, true ); break; } } } if ( bookmarkEnd && bookmarkEnd.getParent().getName() == 'blockquote' ) { cursor = bookmarkEnd; while ( ( cursor = cursor.getPrevious() ) ) { if ( cursor.type == CKEDITOR.NODE_ELEMENT && cursor.isBlockBoundary() ) { bookmarkEnd.move( cursor ); break; } } } } var iterator = range.createIterator(), block; iterator.enlargeBr = editor.config.enterMode != CKEDITOR.ENTER_BR; if ( state == CKEDITOR.TRISTATE_OFF ) { var paragraphs = []; while ( ( block = iterator.getNextParagraph() ) ) paragraphs.push( block ); // If no paragraphs, create one from the current selection position. if ( paragraphs.length < 1 ) { var para = editor.document.createElement( editor.config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ), firstBookmark = bookmarks.shift(); range.insertNode( para ); para.append( new CKEDITOR.dom.text( '\ufeff', editor.document ) ); range.moveToBookmark( firstBookmark ); range.selectNodeContents( para ); range.collapse( true ); firstBookmark = range.createBookmark(); paragraphs.push( para ); bookmarks.unshift( firstBookmark ); } // Make sure all paragraphs have the same parent. var commonParent = paragraphs[ 0 ].getParent(), tmp = []; for ( var i = 0; i < paragraphs.length; i++ ) { block = paragraphs[ i ]; commonParent = commonParent.getCommonAncestor( block.getParent() ); } // The common parent must not be the following tags: table, tbody, tr, ol, ul. var denyTags = { table: 1, tbody: 1, tr: 1, ol: 1, ul: 1 }; while ( denyTags[ commonParent.getName() ] ) commonParent = commonParent.getParent(); // Reconstruct the block list to be processed such that all resulting blocks // satisfy parentNode.equals( commonParent ). var lastBlock = null; while ( paragraphs.length > 0 ) { block = paragraphs.shift(); while ( !block.getParent().equals( commonParent ) ) block = block.getParent(); if ( !block.equals( lastBlock ) ) tmp.push( block ); lastBlock = block; } // If any of the selected blocks is a blockquote, remove it to prevent // nested blockquotes. while ( tmp.length > 0 ) { block = tmp.shift(); if ( block.getName() == 'blockquote' ) { var docFrag = new CKEDITOR.dom.documentFragment( editor.document ); while ( block.getFirst() ) { docFrag.append( block.getFirst().remove() ); paragraphs.push( docFrag.getLast() ); } docFrag.replace( block ); } else { paragraphs.push( block ); } } // Now we have all the blocks to be included in a new blockquote node. var bqBlock = editor.document.createElement( 'blockquote' ); bqBlock.insertBefore( paragraphs[ 0 ] ); while ( paragraphs.length > 0 ) { block = paragraphs.shift(); bqBlock.append( block ); } } else if ( state == CKEDITOR.TRISTATE_ON ) { var moveOutNodes = [], database = {}; while ( ( block = iterator.getNextParagraph() ) ) { var bqParent = null, bqChild = null; while ( block.getParent() ) { if ( block.getParent().getName() == 'blockquote' ) { bqParent = block.getParent(); bqChild = block; break; } block = block.getParent(); } // Remember the blocks that were recorded down in the moveOutNodes array // to prevent duplicates. if ( bqParent && bqChild && !bqChild.getCustomData( 'blockquote_moveout' ) ) { moveOutNodes.push( bqChild ); CKEDITOR.dom.element.setMarker( database, bqChild, 'blockquote_moveout', true ); } } CKEDITOR.dom.element.clearAllMarkers( database ); var movedNodes = [], processedBlockquoteBlocks = []; database = {}; while ( moveOutNodes.length > 0 ) { var node = moveOutNodes.shift(); bqBlock = node.getParent(); // If the node is located at the beginning or the end, just take it out // without splitting. Otherwise, split the blockquote node and move the // paragraph in between the two blockquote nodes. if ( !node.getPrevious() ) node.remove().insertBefore( bqBlock ); else if ( !node.getNext() ) node.remove().insertAfter( bqBlock ); else { node.breakParent( node.getParent() ); processedBlockquoteBlocks.push( node.getNext() ); } // Remember the blockquote node so we can clear it later (if it becomes empty). if ( !bqBlock.getCustomData( 'blockquote_processed' ) ) { processedBlockquoteBlocks.push( bqBlock ); CKEDITOR.dom.element.setMarker( database, bqBlock, 'blockquote_processed', true ); } movedNodes.push( node ); } CKEDITOR.dom.element.clearAllMarkers( database ); // Clear blockquote nodes that have become empty. for ( i = processedBlockquoteBlocks.length - 1; i >= 0; i-- ) { bqBlock = processedBlockquoteBlocks[ i ]; if ( noBlockLeft( bqBlock ) ) bqBlock.remove(); } if ( editor.config.enterMode == CKEDITOR.ENTER_BR ) { var firstTime = true; while ( movedNodes.length ) { node = movedNodes.shift(); if ( node.getName() == 'div' ) { docFrag = new CKEDITOR.dom.documentFragment( editor.document ); var needBeginBr = firstTime && node.getPrevious() && !( node.getPrevious().type == CKEDITOR.NODE_ELEMENT && node.getPrevious().isBlockBoundary() ); if ( needBeginBr ) docFrag.append( editor.document.createElement( 'br' ) ); var needEndBr = node.getNext() && !( node.getNext().type == CKEDITOR.NODE_ELEMENT && node.getNext().isBlockBoundary() ); while ( node.getFirst() ) node.getFirst().remove().appendTo( docFrag ); if ( needEndBr ) docFrag.append( editor.document.createElement( 'br' ) ); docFrag.replace( node ); firstTime = false; } } } } selection.selectBookmarks( bookmarks ); editor.focus(); }, refresh: function( editor, path ) { // Check if inside of blockquote. var firstBlock = path.block || path.blockLimit; this.setState( editor.elementPath( firstBlock ).contains( 'blockquote', 1 ) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF ); }, context: 'blockquote', allowedContent: 'blockquote', requiredContent: 'blockquote' }; CKEDITOR.plugins.add( 'blockquote', { // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { if ( editor.blockless ) return; editor.addCommand( 'blockquote', commandObject ); editor.ui.addButton && editor.ui.addButton( 'Blockquote', { label: editor.lang.blockquote.toolbar, command: 'blockquote', toolbar: 'blocks,10' } ); } } ); } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview The "Notification" plugin. * */ 'use strict'; ( function() { CKEDITOR.plugins.add( 'notification', { init: function( editor ) { editor._.notificationArea = new Area( editor ); // Overwrites default `editor.showNotification`. editor.showNotification = function( message, type, progressOrDuration ) { var progress, duration; if ( type == 'progress' ) { progress = progressOrDuration; } else { duration = progressOrDuration; } var notification = new CKEDITOR.plugins.notification( editor, { message: message, type: type, progress: progress, duration: duration } ); notification.show(); return notification; }; // Close the last notification on ESC. editor.on( 'key', function( evt ) { if ( evt.data.keyCode == 27 ) { /* ESC */ var notifications = editor._.notificationArea.notifications; if ( !notifications.length ) { return; } // As long as this is not a common practice to inform screen-reader users about actions, in this case // this is the best solution (unfortunately there is no standard for accessibility for notifications). // Notification has an `alert` aria role what means that it does not get a focus nor is needed to be // closed (unlike `alertdialog`). However notification will capture ESC key so we need to inform user // why it does not do other actions. say( editor.lang.notification.closed ); // Hide last. notifications[ notifications.length - 1 ].hide(); evt.cancel(); } } ); // Send the message to the screen readers. function say( text ) { var message = new CKEDITOR.dom.element( 'div' ); message.setStyles( { position: 'fixed', 'margin-left': '-9999px' } ); message.setAttributes( { 'aria-live': 'assertive', 'aria-atomic': 'true' } ); message.setText( text ); CKEDITOR.document.getBody().append( message ); setTimeout( function() { message.remove(); }, 100 ); } } } ); /** * Notification class. Notifications are used to display short messages to the user. They might be used to show the result of * asynchronous actions or information about changes in the editor content. It is recommended to use them instead of * alert dialogs. They should **not** be used if a user response is required nor with dialog windows (e.g. in dialog validation). * * There are four types of notifications available, see the {@link #type} property. * * Note that the notification constructor only creates a notification instance. To show it, use the {@link #show} method: * * var notification = new CKEDITOR.plugins.notification( editor, { message: 'Foo' } ); * notification.show(); * * You can also use the {@link CKEDITOR.editor#showNotification} method: * * editor.showNotification( 'Foo' ); * * All of the notification actions: ({@link #show}, {@link #update} and {@link #hide}) fire cancelable events * on the related {@link CKEDITOR.editor} instance so you can integrate editor notifications with your website notifications. * * Refer to the [Notifications](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_notifications.html) article for more information about this feature. * * @since 4.5.0 * @class CKEDITOR.plugins.notification * @constructor Create a notification object. Call {@link #show} to show the created notification. * @param {CKEDITOR.editor} editor The editor instance. * @param {Object} options * @param {String} options.message The message displayed in the notification. * @param {String} [options.type='info'] Notification type, see {@link #type}. * @param {Number} [options.progress=0] If the type is `progress` this may be a progress from 0 to 1. * @param {Number} [options.duration] How long the notification will be visible, see {@link #duration}. */ function Notification( editor, options ) { CKEDITOR.tools.extend( this, options, { editor: editor, id: 'cke-' + CKEDITOR.tools.getUniqueId(), area: editor._.notificationArea } ); if ( !options.type ) { this.type = 'info'; } this.element = this._createElement(); // Don't allow dragging on notification (https://dev.ckeditor.com/ticket/13184). editor.plugins.clipboard && CKEDITOR.plugins.clipboard.preventDefaultDropOnElement( this.element ); } /** * The editor instance. * * @readonly * @property {CKEDITOR.editor} editor */ /** * Message displayed in the notification. * * @readonly * @property {String} message */ /** * Notification type. There are four types available: * * * `info` (default) – Information for the user (e.g. "File is uploading.", "ACF modified content."), * * `warning` – Warning or error message (e.g. "This type of file is not supported.", * "You cannot paste the script."), * * `success` – Information that an operation finished successfully (e.g. "File uploaded.", "Data imported."). * * `progress` – Information about the progress of an operation. When the operation is done, the notification * type should be changed to `success`. * * @readonly * @property {String} type */ /** * If the notification {@link #type} is `'progress'`, this is the progress from `0` to `1`. * * @readonly * @property {Number} progress */ /** * Notification duration. Determines after how many milliseconds the notification should close automatically. * `0` means that the notification will not close automatically and that the user needs to close it manually. * The default value for `warning` and `progress` notifications is `0`. For `info` and `success` the value can * either be set through the {@link CKEDITOR.config#notification_duration} configuration option or equals `5000` * if the configuration option is not set. * * @readonly * @property {Number} duration */ /** * Unique notification ID. * * @readonly * @property {Number} id */ /** * Notification DOM element. There is one element per notification. It is created when the notification is created, * even if it is not shown. If the notification is hidden, the element is detached from the document but not deleted. * It will be reused if the notification is shown again. * * @readonly * @property {CKEDITOR.dom.element} element */ /** * {@link CKEDITOR.plugins.notification.area Notification area} reference. * * @readonly * @property {CKEDITOR.plugins.notification.area} area */ Notification.prototype = { /** * Adds the notification element to the notification area. The notification will be hidden automatically if * {@link #duration} is set. * * Fires the {@link CKEDITOR.editor#notificationShow} event. */ show: function() { if ( this.editor.fire( 'notificationShow', { notification: this } ) === false ) { return; } this.area.add( this ); this._hideAfterTimeout(); }, /** * Updates the notification object and element. * * Fires the {@link CKEDITOR.editor#notificationUpdate} event. * * @param {Object} options * @param {String} [options.message] {@link #message} * @param {String} [options.type] {@link #type} * @param {Number} [options.progress] {@link #progress} * @param {Number} [options.duration] {@link #duration} * @param {Boolean} [options.important=false] If the update is important, the notification will be shown * if it was hidden and read by screen readers. */ update: function( options ) { var show = true; if ( this.editor.fire( 'notificationUpdate', { notification: this, options: options } ) === false ) { // The idea of cancelable event is to let user create his own way of displaying notification, so if // `notificationUpdate` event will be canceled there will be no interaction with notification area, but on // the other hand the logic should work anyway so object will be updated (including `element` property). // Note: we can safely update the element's attributes below, because this element is created inside // the constructor. If the notificatinShow event was canceled as well, the element is detached from DOM. show = false; } var element = this.element, messageElement = element.findOne( '.cke_notification_message' ), progressElement = element.findOne( '.cke_notification_progress' ), type = options.type; element.removeAttribute( 'role' ); // Change type to progress if `options.progress` is set. if ( options.progress && this.type != 'progress' ) { type = 'progress'; } if ( type ) { element.removeClass( this._getClass() ); element.removeAttribute( 'aria-label' ); this.type = type; element.addClass( this._getClass() ); element.setAttribute( 'aria-label', this.type ); if ( this.type == 'progress' && !progressElement ) { progressElement = this._createProgressElement(); progressElement.insertBefore( messageElement ); } else if ( this.type != 'progress' && progressElement ) { progressElement.remove(); } } if ( options.message !== undefined ) { this.message = options.message; messageElement.setHtml( this.message ); } if ( options.progress !== undefined ) { this.progress = options.progress; if ( progressElement ) { progressElement.setStyle( 'width', this._getPercentageProgress() ); } } if ( show && options.important ) { element.setAttribute( 'role', 'alert' ); if ( !this.isVisible() ) { this.area.add( this ); } } // Overwrite even if it is undefined. this.duration = options.duration; this._hideAfterTimeout(); }, /** * Removes the notification element from the notification area. * * Fires the {@link CKEDITOR.editor#notificationHide} event. */ hide: function() { if ( this.editor.fire( 'notificationHide', { notification: this } ) === false ) { return; } this.area.remove( this ); }, /** * Returns `true` if the notification is in the notification area. * * @returns {Boolean} `true` if the notification is in the notification area. */ isVisible: function() { return CKEDITOR.tools.indexOf( this.area.notifications, this ) >= 0; }, /** * Creates the notification DOM element. * * @private * @returns {CKEDITOR.dom.element} Notification DOM element. */ _createElement: function() { var notification = this, notificationElement, notificationMessageElement, notificationCloseElement, close = this.editor.lang.common.close; notificationElement = new CKEDITOR.dom.element( 'div' ); notificationElement.addClass( 'cke_notification' ); notificationElement.addClass( this._getClass() ); notificationElement.setAttributes( { id: this.id, role: 'alert', 'aria-label': this.type } ); if ( this.type == 'progress' ) notificationElement.append( this._createProgressElement() ); notificationMessageElement = new CKEDITOR.dom.element( 'p' ); notificationMessageElement.addClass( 'cke_notification_message' ); notificationMessageElement.setHtml( this.message ); notificationElement.append( notificationMessageElement ); notificationCloseElement = CKEDITOR.dom.element.createFromHtml( '' + 'X' + '' ); notificationElement.append( notificationCloseElement ); notificationCloseElement.on( 'click', function() { // Focus editor on close (https://dev.ckeditor.com/ticket/12865) notification.editor.focus(); notification.hide(); } ); return notificationElement; }, /** * Gets the notification CSS class. * * @private * @returns {String} Notification CSS class. */ _getClass: function() { return ( this.type == 'progress' ) ? 'cke_notification_info' : ( 'cke_notification_' + this.type ); }, /** * Creates a progress element for the notification element. * * @private * @returns {CKEDITOR.dom.element} Progress element for the notification element. */ _createProgressElement: function() { var element = new CKEDITOR.dom.element( 'span' ); element.addClass( 'cke_notification_progress' ); element.setStyle( 'width', this._getPercentageProgress() ); return element; }, /** * Gets the progress as a percentage (ex. `0.3` -> `30%`). * * @private * @returns {String} Progress as a percentage. */ _getPercentageProgress: function() { return Math.round( ( this.progress || 0 ) * 100 ) + '%'; }, /** * Hides the notification after a timeout. * * @private */ _hideAfterTimeout: function() { var notification = this, duration; if ( this._hideTimeoutId ) { clearTimeout( this._hideTimeoutId ); } if ( typeof this.duration == 'number' ) { duration = this.duration; } else if ( this.type == 'info' || this.type == 'success' ) { duration = ( typeof this.editor.config.notification_duration == 'number' ) ? this.editor.config.notification_duration : 5000; } if ( duration ) { notification._hideTimeoutId = setTimeout( function() { notification.hide(); }, duration ); } } }; /** * Notification area is an area where all notifications are put. The area is laid out dynamically. * When the first notification is added, the area is shown and all listeners are added. * When the last notification is removed, the area is hidden and all listeners are removed. * * @since 4.5.0 * @private * @class CKEDITOR.plugins.notification.area * @constructor * @param {CKEDITOR.editor} editor The editor instance. */ function Area( editor ) { var that = this; this.editor = editor; this.notifications = []; this.element = this._createElement(); this._uiBuffer = CKEDITOR.tools.eventsBuffer( 10, this._layout, this ); this._changeBuffer = CKEDITOR.tools.eventsBuffer( 500, this._layout, this ); editor.on( 'destroy', function() { that._removeListeners(); that.element.remove(); } ); } /** * The editor instance. * * @readonly * @property {CKEDITOR.editor} editor */ /** * The array of added notifications. * * @readonly * @property {Array} notifications */ /** * Notification area DOM element. This element is created when the area object is created. It will be attached to the document * when the first notification is added and removed when the last notification is removed. * * @readonly * @property {CKEDITOR.dom.element} element */ /** * Notification width. Cached for performance reasons. * * @private * @property {CKEDITOR.dom.element} _notificationWidth */ /** * Notification margin. Cached for performance reasons. * * @private * @property {CKEDITOR.dom.element} _notificationMargin */ /** * Event buffer object for UI events to optimize performance. * * @private * @property {Object} _uiBuffer */ /** * Event buffer object for editor change events to optimize performance. * * @private * @property {Object} _changeBuffer */ Area.prototype = { /** * Adds the notification to the notification area. If it is the first notification, the area will also be attached to * the document and listeners will be attached. * * Note that the proper way to show a notification is to call the {@link CKEDITOR.plugins.notification#show} method. * * @param {CKEDITOR.plugins.notification} notification Notification to add. */ add: function( notification ) { this.notifications.push( notification ); this.element.append( notification.element ); if ( this.element.getChildCount() == 1 ) { CKEDITOR.document.getBody().append( this.element ); this._attachListeners(); } this._layout(); }, /** * Removes the notification from the notification area. If it is the last notification, the area will also be * detached from the document and listeners will be detached. * * Note that the proper way to hide a notification is to call the {@link CKEDITOR.plugins.notification#hide} method. * * @param {CKEDITOR.plugins.notification} notification Notification to remove. */ remove: function( notification ) { var i = CKEDITOR.tools.indexOf( this.notifications, notification ); if ( i < 0 ) { return; } this.notifications.splice( i, 1 ); notification.element.remove(); if ( !this.element.getChildCount() ) { this._removeListeners(); this.element.remove(); } }, /** * Creates the notification area element. * * @private * @returns {CKEDITOR.dom.element} Notification area element. */ _createElement: function() { var editor = this.editor, config = editor.config, notificationArea = new CKEDITOR.dom.element( 'div' ); notificationArea.addClass( 'cke_notifications_area' ); notificationArea.setAttribute( 'id', 'cke_notifications_area_' + editor.name ); notificationArea.setStyle( 'z-index', config.baseFloatZIndex - 2 ); return notificationArea; }, /** * Attaches listeners to the notification area. * * @private */ _attachListeners: function() { var win = CKEDITOR.document.getWindow(), editor = this.editor; win.on( 'scroll', this._uiBuffer.input ); win.on( 'resize', this._uiBuffer.input ); editor.on( 'change', this._changeBuffer.input ); editor.on( 'floatingSpaceLayout', this._layout, this, null, 20 ); editor.on( 'blur', this._layout, this, null, 20 ); }, /** * Detaches listeners from the notification area. * * @private */ _removeListeners: function() { var win = CKEDITOR.document.getWindow(), editor = this.editor; win.removeListener( 'scroll', this._uiBuffer.input ); win.removeListener( 'resize', this._uiBuffer.input ); editor.removeListener( 'change', this._changeBuffer.input ); editor.removeListener( 'floatingSpaceLayout', this._layout ); editor.removeListener( 'blur', this._layout ); }, /** * Sets the position of the notification area based on the editor content, toolbar as well as * viewport position and dimensions. * * @private */ _layout: function() { var area = this.element, editor = this.editor, contentsRect = editor.ui.contentsElement.getClientRect(), contentsPos = editor.ui.contentsElement.getDocumentPosition(), top, topRect, areaRect = area.getClientRect(), notification, notificationWidth = this._notificationWidth, notificationMargin = this._notificationMargin, win = CKEDITOR.document.getWindow(), scrollPos = win.getScrollPosition(), viewRect = win.getViewPaneSize(), body = CKEDITOR.document.getBody(), bodyPos = body.getDocumentPosition(), cssLength = CKEDITOR.tools.cssLength; // Cache for optimization if ( !notificationWidth || !notificationMargin ) { notification = this.element.getChild( 0 ); notificationWidth = this._notificationWidth = notification.getClientRect().width; notificationMargin = this._notificationMargin = parseInt( notification.getComputedStyle( 'margin-left' ), 10 ) + parseInt( notification.getComputedStyle( 'margin-right' ), 10 ); } // Check if toolbar exist and if so, then assign values to it (#491). if ( editor.toolbar ) { top = editor.ui.space( 'top' ); topRect = top.getClientRect(); } // --------------------------------------- Horizontal layout ---------------------------------------- // +---Viewport-------------------------------+ +---Viewport-------------------------------+ // | | | | // | +---Toolbar----------------------------+ | | +---Content----------------------------+ | // | | | | | | | | // | +---Content----------------------------+ | | | | | // | | | | | +---Toolbar----------------------+ | | // | | +------Notification------+ | | | | | | | // | | | | OR | +--------------------------------+ | | // | | | | | | | | // | | | | | | +------Notification------+ | | // | | | | | | | | // | | | | | | | | // | +--------------------------------------+ | | +--------------------------------------+ | // +------------------------------------------+ +------------------------------------------+ if ( top && top.isVisible() && topRect.bottom > contentsRect.top && topRect.bottom < contentsRect.bottom - areaRect.height ) { setBelowToolbar(); // +---Viewport-------------------------------+ // | | // | +---Content----------------------------+ | // | | | | // | | +------Notification------+ | | // | | | | // | | | | // | | | | // | +--------------------------------------+ | // | | // +------------------------------------------+ } else if ( contentsRect.top > 0 ) { setTopStandard(); // +---Content----------------------------+ // | | // +---Viewport-------------------------------+ // | | | | // | | +------Notification------+ | | // | | | | // | | | | // | | | | // | +--------------------------------------+ | // | | // +------------------------------------------+ } else if ( contentsPos.y + contentsRect.height - areaRect.height > scrollPos.y ) { setTopFixed(); // +---Content----------------------------+ +---Content----------------------------+ // | | | | // | | | | // | | | +------Notification------+ | // | | | | // | | OR +--------------------------------------+ // +---Viewport-------------------------------+ // | | +------Notification------+ | | +---Viewport-------------------------------+ // | | | | | | // | +--------------------------------------+ | | | // | | | | // +------------------------------------------+ +------------------------------------------+ } else { setBottom(); } function setTopStandard() { area.setStyles( { position: 'absolute', top: cssLength( contentsPos.y ) } ); } function setBelowToolbar() { area.setStyles( { position: 'fixed', top: cssLength( topRect.bottom ) } ); } function setTopFixed() { area.setStyles( { position: 'fixed', top: 0 } ); } function setBottom() { area.setStyles( { position: 'absolute', top: cssLength( contentsPos.y + contentsRect.height - areaRect.height ) } ); } // ---------------------------------------- Vertical layout ----------------------------------------- var leftBase = area.getStyle( 'position' ) == 'fixed' ? contentsRect.left : body.getComputedStyle( 'position' ) != 'static' ? contentsPos.x - bodyPos.x : contentsPos.x; // Content is narrower than notification if ( contentsRect.width < notificationWidth + notificationMargin ) { // +---Viewport-------------------------------+ // | | // | +---Content------------+ | // | | | | // | +------Notification------+ | | // | | | | // | +----------------------+ | // | | // +------------------------------------------+ if ( contentsPos.x + notificationWidth + notificationMargin > scrollPos.x + viewRect.width ) { setRight(); // +---Viewport-------------------------------+ +---Viewport--------------------------+ // | | | | // | +---Content------------+ | +---Content------------+ | // | | | | | | | | // | | +------Notification------+ | OR | +------Notification------+ | // | | | | | | | | // | +----------------------+ | +----------------------+ | // | | | | // +------------------------------------------+ +-------------------------------------+ } else { setLeft(); } // Content is wider than notification. } else { // +--+Viewport+------------------------+ // | | // | +---Content-----------------------------------------+ // | | | | // | | +-----+Notification+-----+ | // | | | | // | | | | // | | | | // | +---------------------------------------------------+ // | | // +------------------------------------+ if ( contentsPos.x + notificationWidth + notificationMargin > scrollPos.x + viewRect.width ) { setLeft(); // +---Viewport-------------------------+ // | | // | +---Content----------------------------------------------+ // | | | | // | | +------Notification------+ | | // | | | | // | | | | // | +--------------------------------------------------------+ // | | // +------------------------------------+ } else if ( contentsPos.x + contentsRect.width / 2 + notificationWidth / 2 + notificationMargin > scrollPos.x + viewRect.width ) { setRightFixed(); // +---Viewport-------------------------+ // | | // +---Content----------------------------+ | // | | | | // | +------Notification------+ | | // | | | | // | | | | // +--------------------------------------+ | // | | // +------------------------------------+ } else if ( contentsRect.left + contentsRect.width - notificationWidth - notificationMargin < 0 ) { setRight(); // +---Viewport-------------------------+ // | | // +---Content---------------------------------------------+ | // | | | | // | | +------Notification------+ | | // | | | | // | | | | // +-------------------------------------------------------+ | // | | // +------------------------------------+ } else if ( contentsRect.left + contentsRect.width / 2 - notificationWidth / 2 < 0 ) { setLeftFixed(); // +---Viewport-------------------------+ // | | // | +---Content----------------------+ | // | | | | // | | +-----Notification-----+ | | // | | | | // | | | | // | +--------------------------------+ | // | | // +------------------------------------+ } else { setCenter(); } } function setLeft() { area.setStyle( 'left', cssLength( leftBase ) ); } function setLeftFixed() { area.setStyle( 'left', cssLength( leftBase - contentsPos.x + scrollPos.x ) ); } function setCenter() { area.setStyle( 'left', cssLength( leftBase + contentsRect.width / 2 - notificationWidth / 2 - notificationMargin / 2 ) ); } function setRight() { area.setStyle( 'left', cssLength( leftBase + contentsRect.width - notificationWidth - notificationMargin ) ); } function setRightFixed() { area.setStyle( 'left', cssLength( leftBase - contentsPos.x + scrollPos.x + viewRect.width - notificationWidth - notificationMargin ) ); } } }; CKEDITOR.plugins.notification = Notification; /** * After how many milliseconds the notification of the `info` and `success` * {@link CKEDITOR.plugins.notification#type type} should close automatically. * `0` means that notifications will not close automatically. * Note that `warning` and `progress` notifications will never close automatically. * * Refer to the [Notifications](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_notifications.html) article * for more information about this feature. * * @since 4.5.0 * @cfg {Number} [notification_duration=5000] * @member CKEDITOR.config */ /** * Event fired when the {@link CKEDITOR.plugins.notification#show} method is called, before the * notification is shown. If this event is canceled, the notification will not be shown. * * Using this event allows you to fully customize how a notification will be shown. It may be used to integrate * the CKEditor notification system with your web page notifications. * * @since 4.5.0 * @event notificationShow * @member CKEDITOR.editor * @param data * @param {CKEDITOR.plugins.notification} data.notification Notification which will be shown. * @param {CKEDITOR.editor} editor The editor instance. */ /** * Event fired when the {@link CKEDITOR.plugins.notification#update} method is called, before the * notification is updated. If this event is canceled, the notification will not be shown even if the update was important, * but the object will be updated anyway. Note that canceling this event does not prevent updating {@link #element} * attributes, but if {@link #notificationShow} was canceled as well, this element is detached from the DOM. * * Using this event allows you to fully customize how a notification will be updated. It may be used to integrate * the CKEditor notification system with your web page notifications. * * @since 4.5.0 * @event notificationUpdate * @member CKEDITOR.editor * @param data * @param {CKEDITOR.plugins.notification} data.notification Notification which will be updated. * Note that it contains the data that has not been updated yet. * @param {Object} data.options Update options, see {@link CKEDITOR.plugins.notification#update}. * @param {CKEDITOR.editor} editor The editor instance. */ /** * Event fired when the {@link CKEDITOR.plugins.notification#hide} method is called, before the * notification is hidden. If this event is canceled, the notification will not be hidden. * * Using this event allows you to fully customize how a notification will be hidden. It may be used to integrate * the CKEditor notification system with your web page notifications. * * @since 4.5.0 * @event notificationHide * @member CKEDITOR.editor * @param data * @param {CKEDITOR.plugins.notification} data.notification Notification which will be hidden. * @param {CKEDITOR.editor} editor The editor instance. */ } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ ( function() { var template = '' + '{label}' + '{ariaShortcut}' + '{arrowHtml}' + ''; var templateArrow = '' + // BLACK DOWN-POINTING TRIANGLE ( CKEDITOR.env.hc ? '▼' : '' ) + ''; var btnArrowTpl = CKEDITOR.addTemplate( 'buttonArrow', templateArrow ), btnTpl = CKEDITOR.addTemplate( 'button', template ); CKEDITOR.plugins.add( 'button', { beforeInit: function( editor ) { editor.ui.addHandler( CKEDITOR.UI_BUTTON, CKEDITOR.ui.button.handler ); } } ); /** * Button UI element. * * @readonly * @property {String} [='button'] * @member CKEDITOR */ CKEDITOR.UI_BUTTON = 'button'; /** * Represents a button UI element. This class should not be called directly. To * create new buttons use {@link CKEDITOR.ui#addButton} instead. * * @class * @constructor Creates a button class instance. * @param {Object} definition The button definition. */ CKEDITOR.ui.button = function( definition ) { CKEDITOR.tools.extend( this, definition, // Set defaults. { title: definition.label, click: definition.click || function( editor ) { editor.execCommand( definition.command ); } } ); this._ = {}; }; /** * Represents the button handler object. * * @class * @singleton * @extends CKEDITOR.ui.handlerDefinition */ CKEDITOR.ui.button.handler = { /** * Transforms a button definition into a {@link CKEDITOR.ui.button} instance. * * @member CKEDITOR.ui.button.handler * @param {Object} definition * @returns {CKEDITOR.ui.button} */ create: function( definition ) { return new CKEDITOR.ui.button( definition ); } }; /** @class CKEDITOR.ui.button */ CKEDITOR.ui.button.prototype = { /** * Renders the button. * * @param {CKEDITOR.editor} editor The editor instance which this button is * to be used by. * @param {Array} output The output array to which the HTML code related to * this button should be appended. */ render: function( editor, output ) { var modeStates = null; function updateState() { // "this" is a CKEDITOR.ui.button instance. var mode = editor.mode; if ( mode ) { // Restore saved button state. var state = this.modes[ mode ] ? modeStates[ mode ] !== undefined ? modeStates[ mode ] : CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED; state = editor.readOnly && !this.readOnly ? CKEDITOR.TRISTATE_DISABLED : state; this.setState( state ); // Let plugin to disable button. if ( this.refresh ) this.refresh(); } } var env = CKEDITOR.env, id = this._.id = CKEDITOR.tools.getNextId(), stateName = '', command = this.command, // Get the command name. clickFn, keystroke, shortcut; this._.editor = editor; var instance = { id: id, button: this, editor: editor, focus: function() { var element = CKEDITOR.document.getById( id ); element.focus(); }, execute: function() { this.button.click( editor ); }, attach: function( editor ) { this.button.attach( editor ); } }; var keydownFn = CKEDITOR.tools.addFunction( function( ev ) { if ( instance.onkey ) { ev = new CKEDITOR.dom.event( ev ); return ( instance.onkey( instance, ev.getKeystroke() ) !== false ); } } ); var focusFn = CKEDITOR.tools.addFunction( function( ev ) { var retVal; if ( instance.onfocus ) retVal = ( instance.onfocus( instance, new CKEDITOR.dom.event( ev ) ) !== false ); return retVal; } ); var selLocked = 0; instance.clickFn = clickFn = CKEDITOR.tools.addFunction( function() { // Restore locked selection in Opera. if ( selLocked ) { editor.unlockSelection( 1 ); selLocked = 0; } instance.execute(); // Fixed iOS focus issue when your press disabled button (https://dev.ckeditor.com/ticket/12381). if ( env.iOS ) { editor.focus(); } } ); // Indicate a mode sensitive button. if ( this.modes ) { modeStates = {}; editor.on( 'beforeModeUnload', function() { if ( editor.mode && this._.state != CKEDITOR.TRISTATE_DISABLED ) modeStates[ editor.mode ] = this._.state; }, this ); // Update status when activeFilter, mode or readOnly changes. editor.on( 'activeFilterChange', updateState, this ); editor.on( 'mode', updateState, this ); // If this button is sensitive to readOnly state, update it accordingly. !this.readOnly && editor.on( 'readOnly', updateState, this ); } else if ( command ) { // Get the command instance. command = editor.getCommand( command ); if ( command ) { command.on( 'state', function() { this.setState( command.state ); }, this ); stateName += ( command.state == CKEDITOR.TRISTATE_ON ? 'on' : command.state == CKEDITOR.TRISTATE_DISABLED ? 'disabled' : 'off' ); } } var iconName; // For button that has text-direction awareness on selection path. if ( this.directional ) { editor.on( 'contentDirChanged', function( evt ) { var el = CKEDITOR.document.getById( this._.id ), icon = el.getFirst(); var pathDir = evt.data; // Make a minor direction change to become style-able for the skin icon. if ( pathDir != editor.lang.dir ) el.addClass( 'cke_' + pathDir ); else el.removeClass( 'cke_ltr' ).removeClass( 'cke_rtl' ); // Inline style update for the plugin icon. icon.setAttribute( 'style', CKEDITOR.skin.getIconStyle( iconName, pathDir == 'rtl', this.icon, this.iconOffset ) ); }, this ); } if ( !command ) { stateName += 'off'; } else { keystroke = editor.getCommandKeystroke( command ); if ( keystroke ) { shortcut = CKEDITOR.tools.keystrokeToString( editor.lang.common.keyboard, keystroke ); } } var name = this.name || this.command, iconPath = null, overridePath = this.icon; iconName = name; // Check if we're pointing to an icon defined by another command. (https://dev.ckeditor.com/ticket/9555) if ( this.icon && !( /\./ ).test( this.icon ) ) { iconName = this.icon; overridePath = null; } else { // Register and use custom icon for button (#1530). if ( this.icon ) { iconPath = this.icon; } if ( CKEDITOR.env.hidpi && this.iconHiDpi ) { iconPath = this.iconHiDpi; } } if ( iconPath ) { CKEDITOR.skin.addIcon( iconPath, iconPath ); overridePath = null; } else { iconPath = iconName; } var params = { id: id, name: name, iconName: iconName, label: this.label, // .cke_button_expandable enables additional styling for popup buttons (#2483). cls: ( this.hasArrow ? 'cke_button_expandable ' : '' ) + ( this.className || '' ), state: stateName, ariaDisabled: stateName == 'disabled' ? 'true' : 'false', title: this.title + ( shortcut ? ' (' + shortcut.display + ')' : '' ), ariaShortcut: shortcut ? editor.lang.common.keyboardShortcut + ' ' + shortcut.aria : '', titleJs: env.gecko && !env.hc ? '' : ( this.title || '' ).replace( "'", '' ), hasArrow: typeof this.hasArrow === 'string' && this.hasArrow || ( this.hasArrow ? 'true' : 'false' ), keydownFn: keydownFn, focusFn: focusFn, clickFn: clickFn, style: CKEDITOR.skin.getIconStyle( iconPath, ( editor.lang.dir == 'rtl' ), overridePath, this.iconOffset ), arrowHtml: this.hasArrow ? btnArrowTpl.output() : '' }; btnTpl.output( params, output ); if ( this.onRender ) this.onRender(); return instance; }, /** * Sets the button state. * * @param {Number} state Indicates the button state. One of {@link CKEDITOR#TRISTATE_ON}, * {@link CKEDITOR#TRISTATE_OFF}, or {@link CKEDITOR#TRISTATE_DISABLED}. */ setState: function( state ) { if ( this._.state == state ) return false; this._.state = state; var element = CKEDITOR.document.getById( this._.id ); if ( element ) { element.setState( state, 'cke_button' ); element.setAttribute( 'aria-disabled', state == CKEDITOR.TRISTATE_DISABLED ); if ( !this.hasArrow ) { // Note: aria-pressed attribute should not be added to menuButton instances. (https://dev.ckeditor.com/ticket/11331) if ( state === CKEDITOR.TRISTATE_ON ) { element.setAttribute( 'aria-pressed', true ); } else { element.removeAttribute( 'aria-pressed' ); } } else { // Indicates that menu button is opened (#421). element.setAttribute( 'aria-expanded', state == CKEDITOR.TRISTATE_ON ); } return true; } else { return false; } }, /** * Gets the button state. * * @returns {Number} The button state. One of {@link CKEDITOR#TRISTATE_ON}, * {@link CKEDITOR#TRISTATE_OFF}, or {@link CKEDITOR#TRISTATE_DISABLED}. */ getState: function() { return this._.state; }, /** * Returns this button's {@link CKEDITOR.feature} instance. * * It may be this button instance if it has at least one of * `allowedContent` and `requiredContent` properties. Otherwise, * if a command is bound to this button by the `command` property, then * that command will be returned. * * This method implements the {@link CKEDITOR.feature#toFeature} interface method. * * @since 4.1.0 * @param {CKEDITOR.editor} Editor instance. * @returns {CKEDITOR.feature} The feature. */ toFeature: function( editor ) { if ( this._.feature ) return this._.feature; var feature = this; // If button isn't a feature, return command if is bound. if ( !this.allowedContent && !this.requiredContent && this.command ) feature = editor.getCommand( this.command ) || feature; return this._.feature = feature; } }; /** * Adds a button definition to the UI elements list. * * editorInstance.ui.addButton( 'MyBold', { * label: 'My Bold', * command: 'bold', * toolbar: 'basicstyles,1' * } ); * * @member CKEDITOR.ui * @param {String} name The button name. * @param {Object} definition The button definition. * @param {String} definition.label The textual part of the button (if visible) and its tooltip. * @param {String} definition.command The command to be executed once the button is activated. * @param {String} definition.toolbar The {@link CKEDITOR.config#toolbarGroups toolbar group} into which * the button will be added. An optional index value (separated by a comma) determines the button position within the group. * @param {String} definition.icon The path to a custom icon or icon name registered by another plugin. Custom icon paths * are supported since the **4.9.0** version. * * To use icon registered by another plugin, icon parameter should be used like: * * editor.ui.addButton( 'my_button', { * icon: 'Link' // Uses link icon from Link plugin. * } ); * * If the plugin provides a HiDPI version of an icon, it will be used for HiDPI displays (so defining `iconHiDpi` is not needed * in this case). * * To use a custom icon, the path to the icon should be provided: * * editor.ui.addButton( 'my_button', { * icon: 'assets/icons/my_button.png' * } ) * * This icon will be used for both standard and HiDPI displays unless `iconHiDpi` is explicitly defined. * **Important**: CKEditor will resolve relative paths based on {@link CKEDITOR#basePath}. * @param {String} definition.iconHiDpi The path to the custom HiDPI icon version. Supported since **4.9.0** version. * It will be used only in HiDPI environments. The usage is similar to the `icon` parameter: * * editor.ui.addButton( 'my_button', { * iconHiDpi: 'assets/icons/my_button.hidpi.png' * } ) * @param {String/Boolean} definition.hasArrow If Boolean, it indicates whether the button should have a dropdown. If a string, it acts * as a value of the button's `aria-haspopup` attribute. Since **4.11.0** it supports the string as a value. */ CKEDITOR.ui.prototype.addButton = function( name, definition ) { this.add( name, CKEDITOR.UI_BUTTON, definition ); }; } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview The "toolbar" plugin. Renders the default toolbar interface in * the editor. */ ( function() { var toolbox = function() { this.toolbars = []; this.focusCommandExecuted = false; }; toolbox.prototype.focus = function() { for ( var t = 0, toolbar; toolbar = this.toolbars[ t++ ]; ) { for ( var i = 0, item; item = toolbar.items[ i++ ]; ) { if ( item.focus ) { item.focus(); return; } } } }; var commands = { toolbarFocus: { modes: { wysiwyg: 1, source: 1 }, readOnly: 1, exec: function( editor ) { if ( editor.toolbox ) { editor.toolbox.focusCommandExecuted = true; // Make the first button focus accessible for IE. (https://dev.ckeditor.com/ticket/3417) // Adobe AIR instead need while of delay. if ( CKEDITOR.env.ie || CKEDITOR.env.air ) { setTimeout( function() { editor.toolbox.focus(); }, 100 ); } else { editor.toolbox.focus(); } } } } }; CKEDITOR.plugins.add( 'toolbar', { requires: 'button', // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { var endFlag; var itemKeystroke = function( item, keystroke ) { var next, toolbar; var rtl = editor.lang.dir == 'rtl', toolbarGroupCycling = editor.config.toolbarGroupCycling, // Picking right/left key codes. rightKeyCode = rtl ? 37 : 39, leftKeyCode = rtl ? 39 : 37; toolbarGroupCycling = toolbarGroupCycling === undefined || toolbarGroupCycling; switch ( keystroke ) { case 9: // TAB case CKEDITOR.SHIFT + 9: // SHIFT + TAB // Cycle through the toolbars, starting from the one // closest to the current item. while ( !toolbar || !toolbar.items.length ) { if ( keystroke == 9 ) { toolbar = ( ( toolbar ? toolbar.next : item.toolbar.next ) || editor.toolbox.toolbars[ 0 ] ); } else { toolbar = ( ( toolbar ? toolbar.previous : item.toolbar.previous ) || editor.toolbox.toolbars[ editor.toolbox.toolbars.length - 1 ] ); } // Look for the first item that accepts focus. if ( toolbar.items.length ) { item = toolbar.items[ endFlag ? ( toolbar.items.length - 1 ) : 0 ]; while ( item && !item.focus ) { item = endFlag ? item.previous : item.next; if ( !item ) toolbar = 0; } } } if ( item ) item.focus(); return false; case rightKeyCode: next = item; do { // Look for the next item in the toolbar. next = next.next; // If it's the last item, cycle to the first one. if ( !next && toolbarGroupCycling ) next = item.toolbar.items[ 0 ]; } while ( next && !next.focus ); // If available, just focus it, otherwise focus the // first one. if ( next ) next.focus(); else // Send a TAB. itemKeystroke( item, 9 ); return false; case 40: // DOWN-ARROW if ( item.button && item.button.hasArrow ) { item.execute(); } else { // Send left arrow key. itemKeystroke( item, keystroke == 40 ? rightKeyCode : leftKeyCode ); } return false; case leftKeyCode: case 38: // UP-ARROW next = item; do { // Look for the previous item in the toolbar. next = next.previous; // If it's the first item, cycle to the last one. if ( !next && toolbarGroupCycling ) next = item.toolbar.items[ item.toolbar.items.length - 1 ]; } while ( next && !next.focus ); // If available, just focus it, otherwise focus the // last one. if ( next ) next.focus(); else { endFlag = 1; // Send a SHIFT + TAB. itemKeystroke( item, CKEDITOR.SHIFT + 9 ); endFlag = 0; } return false; case 27: // ESC editor.focus(); return false; case 13: // ENTER case 32: // SPACE item.execute(); return false; } return true; }; editor.on( 'uiSpace', function( event ) { if ( event.data.space != editor.config.toolbarLocation ) return; // Create toolbar only once. event.removeListener(); editor.toolbox = new toolbox(); var labelId = CKEDITOR.tools.getNextId(); var output = [ '', editor.lang.toolbar.toolbars, '', '' ]; var expanded = editor.config.toolbarStartupExpanded !== false, groupStarted, pendingSeparator; // If the toolbar collapser will be available, we'll have // an additional container for all toolbars. if ( editor.config.toolbarCanCollapse && editor.elementMode != CKEDITOR.ELEMENT_MODE_INLINE ) output.push( '' : ' style="display:none">' ) ); var toolbars = editor.toolbox.toolbars, toolbar = getToolbarConfig( editor ), toolbarLength = toolbar.length; for ( var r = 0; r < toolbarLength; r++ ) { var toolbarId, toolbarObj = 0, toolbarName, row = toolbar[ r ], lastToolbarInRow = row !== '/' && ( toolbar[ r + 1 ] === '/' || r == toolbarLength - 1 ), items; // It's better to check if the row object is really // available because it's a common mistake to leave // an extra comma in the toolbar definition // settings, which leads on the editor not loading // at all in IE. (https://dev.ckeditor.com/ticket/3983) if ( !row ) continue; if ( groupStarted ) { output.push( '' ); groupStarted = 0; pendingSeparator = 0; } if ( row === '/' ) { output.push( '' ); continue; } items = row.items || row; // Create all items defined for this toolbar. for ( var i = 0; i < items.length; i++ ) { var item = items[ i ], canGroup; if ( item ) { if ( item.type == CKEDITOR.UI_SEPARATOR ) { // Do not add the separator immediately. Just save // it be included if we already have something in // the toolbar and if a new item is to be added (later). pendingSeparator = groupStarted && item; continue; } canGroup = item.canGroup !== false; // Initialize the toolbar first, if needed. if ( !toolbarObj ) { // Create the basic toolbar object. toolbarId = CKEDITOR.tools.getNextId(); toolbarObj = { id: toolbarId, items: [] }; toolbarName = row.name && ( editor.lang.toolbar.toolbarGroups[ row.name ] || row.name ); // Output the toolbar opener. output.push( '' ); // If a toolbar name is available, send the voice label. toolbarName && output.push( '', toolbarName, '' ); output.push( '' ); // Add the toolbar to the "editor.toolbox.toolbars" // array. var index = toolbars.push( toolbarObj ) - 1; // Create the next/previous reference. if ( index > 0 ) { toolbarObj.previous = toolbars[ index - 1 ]; toolbarObj.previous.next = toolbarObj; } } if ( canGroup ) { if ( !groupStarted ) { output.push( '' ); groupStarted = 1; } } else if ( groupStarted ) { output.push( '' ); groupStarted = 0; } function addItem( item ) { // jshint ignore:line var itemObj = item.render( editor, output ); index = toolbarObj.items.push( itemObj ) - 1; if ( index > 0 ) { itemObj.previous = toolbarObj.items[ index - 1 ]; itemObj.previous.next = itemObj; } itemObj.toolbar = toolbarObj; itemObj.onkey = itemKeystroke; // Fix for https://dev.ckeditor.com/ticket/3052: // Prevent JAWS from focusing the toolbar after document load. itemObj.onfocus = function() { if ( !editor.toolbox.focusCommandExecuted ) editor.focus(); }; } if ( pendingSeparator ) { addItem( pendingSeparator ); pendingSeparator = 0; } addItem( item ); } } if ( groupStarted ) { output.push( '' ); groupStarted = 0; pendingSeparator = 0; } if ( toolbarObj ) output.push( '' ); } if ( editor.config.toolbarCanCollapse ) output.push( '' ); // Not toolbar collapser for inline mode. if ( editor.config.toolbarCanCollapse && editor.elementMode != CKEDITOR.ELEMENT_MODE_INLINE ) { var collapserFn = CKEDITOR.tools.addFunction( function() { editor.execCommand( 'toolbarCollapse' ); } ); editor.on( 'destroy', function() { CKEDITOR.tools.removeFunction( collapserFn ); } ); editor.addCommand( 'toolbarCollapse', { readOnly: 1, exec: function( editor ) { var collapser = editor.ui.space( 'toolbar_collapser' ), toolbox = collapser.getPrevious(), contents = editor.ui.space( 'contents' ), toolboxContainer = toolbox.getParent(), contentHeight = parseInt( contents.$.style.height, 10 ), previousHeight = toolboxContainer.$.offsetHeight, minClass = 'cke_toolbox_collapser_min', collapsed = collapser.hasClass( minClass ); if ( !collapsed ) { toolbox.hide(); collapser.addClass( minClass ); collapser.setAttribute( 'title', editor.lang.toolbar.toolbarExpand ); } else { toolbox.show(); collapser.removeClass( minClass ); collapser.setAttribute( 'title', editor.lang.toolbar.toolbarCollapse ); } // Update collapser symbol. collapser.getFirst().setText( collapsed ? '\u25B2' : // BLACK UP-POINTING TRIANGLE '\u25C0' ); // BLACK LEFT-POINTING TRIANGLE var dy = toolboxContainer.$.offsetHeight - previousHeight; contents.setStyle( 'height', ( contentHeight - dy ) + 'px' ); editor.fire( 'resize', { outerHeight: editor.container.$.offsetHeight, contentsHeight: contents.$.offsetHeight, outerWidth: editor.container.$.offsetWidth } ); }, modes: { wysiwyg: 1, source: 1 } } ); editor.setKeystroke( CKEDITOR.ALT + ( CKEDITOR.env.ie || CKEDITOR.env.webkit ? 189 : 109 ) /*-*/, 'toolbarCollapse' ); output.push( '', '', // BLACK UP-POINTING TRIANGLE '' ); } output.push( '' ); event.data.html += output.join( '' ); } ); editor.on( 'destroy', function() { if ( this.toolbox ) { var toolbars, index = 0, i, items, instance; toolbars = this.toolbox.toolbars; for ( ; index < toolbars.length; index++ ) { items = toolbars[ index ].items; for ( i = 0; i < items.length; i++ ) { instance = items[ i ]; if ( instance.clickFn ) CKEDITOR.tools.removeFunction( instance.clickFn ); if ( instance.keyDownFn ) CKEDITOR.tools.removeFunction( instance.keyDownFn ); } } } } ); // Manage editor focus when navigating the toolbar. editor.on( 'uiReady', function() { var toolbox = editor.ui.space( 'toolbox' ); toolbox && editor.focusManager.add( toolbox, 1 ); } ); editor.addCommand( 'toolbarFocus', commands.toolbarFocus ); editor.setKeystroke( CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' ); editor.ui.add( '-', CKEDITOR.UI_SEPARATOR, {} ); editor.ui.addHandler( CKEDITOR.UI_SEPARATOR, { create: function() { return { render: function( editor, output ) { output.push( '' ); return {}; } }; } } ); } } ); function getToolbarConfig( editor ) { var removeButtons = editor.config.removeButtons; removeButtons = removeButtons && removeButtons.split( ',' ); function buildToolbarConfig() { // Object containing all toolbar groups used by ui items. var lookup = getItemDefinedGroups(); // Take the base for the new toolbar, which is basically a toolbar // definition without items. var toolbar = CKEDITOR.tools.clone( editor.config.toolbarGroups ) || getPrivateToolbarGroups( editor ); // Fill the toolbar groups with the available ui items. for ( var i = 0; i < toolbar.length; i++ ) { var toolbarGroup = toolbar[ i ]; // Skip toolbar break. if ( toolbarGroup == '/' ) continue; // Handle simply group name item. else if ( typeof toolbarGroup == 'string' ) toolbarGroup = toolbar[ i ] = { name: toolbarGroup }; var items, subGroups = toolbarGroup.groups; // Look for items that match sub groups. if ( subGroups ) { for ( var j = 0, sub; j < subGroups.length; j++ ) { sub = subGroups[ j ]; // If any ui item is registered for this subgroup. items = lookup[ sub ]; items && fillGroup( toolbarGroup, items ); } } // Add the main group items as well. items = lookup[ toolbarGroup.name ]; items && fillGroup( toolbarGroup, items ); } return toolbar; } // Returns an object containing all toolbar groups used by ui items. function getItemDefinedGroups() { var groups = {}, itemName, item, itemToolbar, group, order; for ( itemName in editor.ui.items ) { item = editor.ui.items[ itemName ]; itemToolbar = item.toolbar || 'others'; if ( itemToolbar ) { // Break the toolbar property into its parts: "group_name[,order]". itemToolbar = itemToolbar.split( ',' ); group = itemToolbar[ 0 ]; order = parseInt( itemToolbar[ 1 ] || -1, 10 ); // Initialize the group, if necessary. groups[ group ] || ( groups[ group ] = [] ); // Push the data used to build the toolbar later. groups[ group ].push( { name: itemName, order: order } ); } } // Put the items in the right order. for ( group in groups ) { groups[ group ] = groups[ group ].sort( function( a, b ) { return a.order == b.order ? 0 : b.order < 0 ? -1 : a.order < 0 ? 1 : a.order < b.order ? -1 : 1; } ); } return groups; } function fillGroup( toolbarGroup, uiItems ) { if ( uiItems.length ) { if ( toolbarGroup.items ) toolbarGroup.items.push( editor.ui.create( '-' ) ); else toolbarGroup.items = []; var item, name; while ( ( item = uiItems.shift() ) ) { name = typeof item == 'string' ? item : item.name; // Ignore items that are configured to be removed. if ( !removeButtons || CKEDITOR.tools.indexOf( removeButtons, name ) == -1 ) { item = editor.ui.create( name ); if ( !item ) continue; if ( !editor.addFeature( item ) ) continue; toolbarGroup.items.push( item ); } } } } function populateToolbarConfig( config ) { var toolbar = [], i, group, newGroup; for ( i = 0; i < config.length; ++i ) { group = config[ i ]; newGroup = {}; if ( group == '/' ) toolbar.push( group ); else if ( CKEDITOR.tools.isArray( group ) ) { fillGroup( newGroup, CKEDITOR.tools.clone( group ) ); toolbar.push( newGroup ); } else if ( group.items ) { fillGroup( newGroup, CKEDITOR.tools.clone( group.items ) ); newGroup.name = group.name; toolbar.push( newGroup ); } } return toolbar; } var toolbar = editor.config.toolbar; // If it is a string, return the relative "toolbar_name" config. if ( typeof toolbar == 'string' ) toolbar = editor.config[ 'toolbar_' + toolbar ]; return ( editor.toolbar = toolbar ? populateToolbarConfig( toolbar ) : buildToolbarConfig() ); } /** * Adds a toolbar group. See {@link CKEDITOR.config#toolbarGroups} for more details. * * **Note:** This method will not modify toolbar groups set explicitly by * {@link CKEDITOR.config#toolbarGroups}. It will only extend the default setting. * * @param {String} name Toolbar group name. * @param {Number/String} previous The name of the toolbar group after which this one * should be added or `0` if this group should be the first one. * @param {String} [subgroupOf] The name of the parent group. * @member CKEDITOR.ui */ CKEDITOR.ui.prototype.addToolbarGroup = function( name, previous, subgroupOf ) { // The toolbarGroups from the privates is the one we gonna use for automatic toolbar creation. var toolbarGroups = getPrivateToolbarGroups( this.editor ), atStart = previous === 0, newGroup = { name: name }; if ( subgroupOf ) { // Transform the subgroupOf name in the real subgroup object. subgroupOf = CKEDITOR.tools.search( toolbarGroups, function( group ) { return group.name == subgroupOf; } ); if ( subgroupOf ) { !subgroupOf.groups && ( subgroupOf.groups = [] ) ; if ( previous ) { // Search the "previous" item and add the new one after it. previous = CKEDITOR.tools.indexOf( subgroupOf.groups, previous ); if ( previous >= 0 ) { subgroupOf.groups.splice( previous + 1, 0, name ); return; } } // If no previous found. if ( atStart ) subgroupOf.groups.splice( 0, 0, name ); else subgroupOf.groups.push( name ); return; } else { // Ignore "previous" if subgroupOf has not been found. previous = null; } } if ( previous ) { // Transform the "previous" name into its index. previous = CKEDITOR.tools.indexOf( toolbarGroups, function( group ) { return group.name == previous; } ); } if ( atStart ) toolbarGroups.splice( 0, 0, name ); else if ( typeof previous == 'number' ) toolbarGroups.splice( previous + 1, 0, newGroup ); else toolbarGroups.push( name ); }; function getPrivateToolbarGroups( editor ) { return editor._.toolbarGroups || ( editor._.toolbarGroups = [ { name: 'document', groups: [ 'mode', 'document', 'doctools' ] }, { name: 'clipboard', groups: [ 'clipboard', 'undo' ] }, { name: 'editing', groups: [ 'find', 'selection', 'spellchecker' ] }, { name: 'forms' }, '/', { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] }, { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ] }, { name: 'links' }, { name: 'insert' }, '/', { name: 'styles' }, { name: 'colors' }, { name: 'tools' }, { name: 'others' }, { name: 'about' } ] ); } } )(); /** * Separator UI element. * * @readonly * @property {String} [='separator'] * @member CKEDITOR */ CKEDITOR.UI_SEPARATOR = 'separator'; /** * The part of the user interface where the toolbar will be rendered. For the default * editor implementation, the recommended options are `'top'` and `'bottom'`. * * Please note that this option is only applicable to {@glink guide/dev_framed classic} * (`iframe`-based) editor. In case of {@glink guide/dev_inline inline} editor the toolbar * position is set dynamically depending on the position of the editable element on the screen. * * Read more in the {@glink features/toolbarlocation documentation} * and see the {@glink examples/toolbarlocation example}. * * config.toolbarLocation = 'bottom'; * * @cfg * @member CKEDITOR.config */ CKEDITOR.config.toolbarLocation = 'top'; /** * The toolbox (alias toolbar) definition. It is a toolbar name or an array of * toolbars (strips), each one being also an array, containing a list of UI items. * * If set to `null`, the toolbar will be generated automatically using all available buttons * and {@link #toolbarGroups} as a toolbar groups layout. * * In CKEditor 4.5.0+ you can generate your toolbar customization code by using the {@glink features/toolbar visual * toolbar configurator}. * * // Defines a toolbar with only one strip containing the "Source" button, a * // separator, and the "Bold" and "Italic" buttons. * config.toolbar = [ * [ 'Source', '-', 'Bold', 'Italic' ] * ]; * * // Similar to the example above, defines a "Basic" toolbar with only one strip containing three buttons. * // Note that this setting is composed by "toolbar_" added to the toolbar name, which in this case is called "Basic". * // This second part of the setting name can be anything. You must use this name in the CKEDITOR.config.toolbar setting * // in order to instruct the editor which `toolbar_(name)` setting should be used. * config.toolbar_Basic = [ * [ 'Source', '-', 'Bold', 'Italic' ] * ]; * // Load toolbar_Name where Name = Basic. * config.toolbar = 'Basic'; * * @cfg {Array/String} [toolbar=null] * @member CKEDITOR.config */ /** * The toolbar groups definition. * * If the toolbar layout is not explicitly defined by the {@link #toolbar} setting, then * this setting is used to group all defined buttons (see {@link CKEDITOR.ui#addButton}). * Buttons are associated with toolbar groups by the `toolbar` property in their definition objects. * * New groups may be dynamically added during the editor and plugin initialization by * {@link CKEDITOR.ui#addToolbarGroup}. This is only possible if the default setting was used. * * // Default setting. * config.toolbarGroups = [ * { name: 'document', groups: [ 'mode', 'document', 'doctools' ] }, * { name: 'clipboard', groups: [ 'clipboard', 'undo' ] }, * { name: 'editing', groups: [ 'find', 'selection', 'spellchecker' ] }, * { name: 'forms' }, * '/', * { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] }, * { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ] }, * { name: 'links' }, * { name: 'insert' }, * '/', * { name: 'styles' }, * { name: 'colors' }, * { name: 'tools' }, * { name: 'others' }, * { name: 'about' } * ]; * * @cfg {Array} [toolbarGroups=see example] * @member CKEDITOR.config */ /** * Whether the toolbar can be collapsed by the user. If disabled, the Collapse Toolbar * button will not be displayed. * * config.toolbarCanCollapse = true; * * @cfg {Boolean} [toolbarCanCollapse=false] * @member CKEDITOR.config */ /** * Whether the toolbar must start expanded when the editor is loaded. * * Setting this option to `false` will affect the toolbar only when * {@link #toolbarCanCollapse} is set to `true`: * * config.toolbarCanCollapse = true; * config.toolbarStartupExpanded = false; * * @cfg {Boolean} [toolbarStartupExpanded=true] * @member CKEDITOR.config */ /** * When enabled, causes the *Arrow* keys navigation to cycle within the current * toolbar group. Otherwise the *Arrow* keys will move through all items available in * the toolbar. The *Tab* key will still be used to quickly jump among the * toolbar groups. * * config.toolbarGroupCycling = false; * * @since 3.6.0 * @cfg {Boolean} [toolbarGroupCycling=true] * @member CKEDITOR.config */ /** * List of toolbar button names that must not be rendered. This will also work * for non-button toolbar items, like the Font drop-down list. * * config.removeButtons = 'Underline,JustifyCenter'; * * This configuration option should not be overused. The recommended way is to use the * {@link CKEDITOR.config#removePlugins} setting to remove features from the editor * or even better, [create a custom editor build](https://ckeditor.com/cke4/builder) with * just the features that you will use. * In some cases though, a single plugin may define a set of toolbar buttons and * `removeButtons` may be useful when just a few of them are to be removed. * * @cfg {String} [removeButtons] * @member CKEDITOR.config */ /** * The toolbar definition used by the editor. It is created from the * {@link CKEDITOR.config#toolbar} option if it is set or automatically * based on {@link CKEDITOR.config#toolbarGroups}. * * @readonly * @property {Object} toolbar * @member CKEDITOR.editor */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @ignore * File overview: Clipboard support. */ // // COPY & PASTE EXECUTION FLOWS: // -- CTRL+C // * if ( isCustomCopyCutSupported ) // * dataTransfer.setData( 'text/html', getSelectedHtml ) // * else // * browser's default behavior // -- CTRL+X // * listen onKey (onkeydown) // * fire 'saveSnapshot' on editor // * if ( isCustomCopyCutSupported ) // * dataTransfer.setData( 'text/html', getSelectedHtml ) // * extractSelectedHtml // remove selected contents // * else // * browser's default behavior // * deferred second 'saveSnapshot' event // -- CTRL+V // * listen onKey (onkeydown) // * simulate 'beforepaste' for non-IEs on editable // * listen 'onpaste' on editable ('onbeforepaste' for IE) // * fire 'beforePaste' on editor // * if ( !canceled && ( htmlInDataTransfer || !external paste) && dataTransfer is not empty ) getClipboardDataByPastebin // * fire 'paste' on editor // * !canceled && fire 'afterPaste' on editor // -- Copy command // * tryToCutCopy // * execCommand // * !success && notification // -- Cut command // * fixCut // * tryToCutCopy // * execCommand // * !success && notification // -- Paste command // * fire 'paste' on editable ('beforepaste' for IE) // * !canceled && execCommand 'paste' // -- Paste from native context menu & menubar // (Fx & Webkits are handled in 'paste' default listener. // Opera cannot be handled at all because it doesn't fire any events // Special treatment is needed for IE, for which is this part of doc) // * listen 'onpaste' // * cancel native event // * fire 'beforePaste' on editor // * if ( !canceled && ( htmlInDataTransfer || !external paste) && dataTransfer is not empty ) getClipboardDataByPastebin // * execIECommand( 'paste' ) -> this fires another 'paste' event, so cancel it // * fire 'paste' on editor // * !canceled && fire 'afterPaste' on editor // // // PASTE EVENT - PREPROCESSING: // -- Possible dataValue types: auto, text, html. // -- Possible dataValue contents: // * text (possible \n\r) // * htmlified text (text + br,div,p - no presentational markup & attrs - depends on browser) // * html // -- Possible flags: // * htmlified - if true then content is a HTML even if no markup inside. This flag is set // for content from editable pastebins, because they 'htmlify' pasted content. // // -- Type: auto: // * content: htmlified text -> filter, unify text markup (brs, ps, divs), set type: text // * content: html -> filter, set type: html // -- Type: text: // * content: htmlified text -> filter, unify text markup // * content: html -> filter, strip presentational markup, unify text markup // -- Type: html: // * content: htmlified text -> filter, unify text markup // * content: html -> filter // // -- Phases: // * if dataValue is empty copy data from dataTransfer to dataValue (priority 1) // * filtering (priorities 3-5) - e.g. pastefromword filters // * content type sniffing (priority 6) // * markup transformations for text (priority 6) // // DRAG & DROP EXECUTION FLOWS: // -- Drag // * save to the global object: // * drag timestamp (with 'cke-' prefix), // * selected html, // * drag range, // * editor instance. // * put drag timestamp into event.dataTransfer.text // -- Drop // * if events text == saved timestamp && editor == saved editor // internal drag & drop occurred // * getRangeAtDropPosition // * create bookmarks for drag and drop ranges starting from the end of the document // * dragRange.deleteContents() // * fire 'paste' with saved html and drop range // * if events text == saved timestamp && editor != saved editor // cross editor drag & drop occurred // * getRangeAtDropPosition // * fire 'paste' with saved html // * dragRange.deleteContents() // * FF: refreshCursor on afterPaste // * if events text != saved timestamp // drop form external source occurred // * getRangeAtDropPosition // * if event contains html data then fire 'paste' with html // * else if event contains text data then fire 'paste' with encoded text // * FF: refreshCursor on afterPaste 'use strict'; ( function() { var clipboardIdDataType; // Register the plugin. CKEDITOR.plugins.add( 'clipboard', { requires: 'dialog,notification,toolbar', // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { var filterType, filtersFactory = filtersFactoryFactory( editor ); if ( editor.config.forcePasteAsPlainText ) { filterType = 'plain-text'; } else if ( editor.config.pasteFilter ) { filterType = editor.config.pasteFilter; } // On Webkit the pasteFilter defaults 'semantic-content' because pasted data is so terrible // that it must be always filtered. else if ( CKEDITOR.env.webkit && !( 'pasteFilter' in editor.config ) ) { filterType = 'semantic-content'; } editor.pasteFilter = filtersFactory.get( filterType ); initPasteClipboard( editor ); initDragDrop( editor ); CKEDITOR.dialog.add( 'paste', CKEDITOR.getUrl( this.path + 'dialogs/paste.js' ) ); // Convert image file (if present) to base64 string for Firefox. Do it as the first // step as the conversion is asynchronous and should hold all further paste processing. if ( CKEDITOR.env.gecko ) { var supportedImageTypes = [ 'image/png', 'image/jpeg', 'image/gif' ], latestId; editor.on( 'paste', function( evt ) { var dataObj = evt.data, data = dataObj.dataValue, dataTransfer = dataObj.dataTransfer; // If data empty check for image content inside data transfer. https://dev.ckeditor.com/ticket/16705 if ( !data && dataObj.method == 'paste' && dataTransfer && dataTransfer.getFilesCount() == 1 && latestId != dataTransfer.id ) { var file = dataTransfer.getFile( 0 ); if ( CKEDITOR.tools.indexOf( supportedImageTypes, file.type ) != -1 ) { var fileReader = new FileReader(); // Convert image file to img tag with base64 image. fileReader.addEventListener( 'load', function() { evt.data.dataValue = ''; editor.fire( 'paste', evt.data ); }, false ); // Proceed with normal flow if reading file was aborted. fileReader.addEventListener( 'abort', function() { editor.fire( 'paste', evt.data ); }, false ); // Proceed with normal flow if reading file failed. fileReader.addEventListener( 'error', function() { editor.fire( 'paste', evt.data ); }, false ); fileReader.readAsDataURL( file ); latestId = dataObj.dataTransfer.id; evt.stop(); } } }, null, null, 1 ); } editor.on( 'paste', function( evt ) { // Init `dataTransfer` if `paste` event was fired without it, so it will be always available. if ( !evt.data.dataTransfer ) { evt.data.dataTransfer = new CKEDITOR.plugins.clipboard.dataTransfer(); } // If dataValue is already set (manually or by paste bin), so do not override it. if ( evt.data.dataValue ) { return; } var dataTransfer = evt.data.dataTransfer, // IE support only text data and throws exception if we try to get html data. // This html data object may also be empty if we drag content of the textarea. value = dataTransfer.getData( 'text/html' ); if ( value ) { evt.data.dataValue = value; evt.data.type = 'html'; } else { // Try to get text data otherwise. value = dataTransfer.getData( 'text/plain' ); if ( value ) { evt.data.dataValue = editor.editable().transformPlainTextToHtml( value ); evt.data.type = 'text'; } } }, null, null, 1 ); editor.on( 'paste', function( evt ) { var data = evt.data.dataValue, blockElements = CKEDITOR.dtd.$block; // Filter webkit garbage. if ( data.indexOf( 'Apple-' ) > -1 ) { // Replace special webkit's   with simple space, because webkit // produces them even for normal spaces. data = data.replace( / <\/span>/gi, ' ' ); // Strip around white-spaces when not in forced 'html' content type. // This spans are created only when pasting plain text into Webkit, // but for safety reasons remove them always. if ( evt.data.type != 'html' ) { data = data.replace( /]*>([^<]*)<\/span>/gi, function( all, spaces ) { // Replace tabs with 4 spaces like Fx does. return spaces.replace( /\t/g, '    ' ); } ); } // This br is produced only when copying & pasting HTML content. if ( data.indexOf( '
' ) > -1 ) { evt.data.startsWithEOL = 1; evt.data.preSniffing = 'html'; // Mark as not text. data = data.replace( /
/, '' ); } // Remove all other classes. data = data.replace( /(<[^>]+) class="Apple-[^"]*"/gi, '$1' ); } // Strip editable that was copied from inside. (https://dev.ckeditor.com/ticket/9534) if ( data.match( /^<[^<]+cke_(editable|contents)/i ) ) { var tmp, editable_wrapper, wrapper = new CKEDITOR.dom.element( 'div' ); wrapper.setHtml( data ); // Verify for sure and check for nested editor UI parts. (https://dev.ckeditor.com/ticket/9675) while ( wrapper.getChildCount() == 1 && ( tmp = wrapper.getFirst() ) && tmp.type == CKEDITOR.NODE_ELEMENT && // Make sure first-child is element. ( tmp.hasClass( 'cke_editable' ) || tmp.hasClass( 'cke_contents' ) ) ) { wrapper = editable_wrapper = tmp; } // If editable wrapper was found strip it and bogus
(added on FF). if ( editable_wrapper ) data = editable_wrapper.getHtml().replace( /
$/i, '' ); } if ( CKEDITOR.env.ie ) { //  

->

(br.cke-pasted-remove will be removed later) data = data.replace( /^ (?: |\r\n)?<(\w+)/g, function( match, elementName ) { if ( elementName.toLowerCase() in blockElements ) { evt.data.preSniffing = 'html'; // Mark as not a text. return '<' + elementName; } return match; } ); } else if ( CKEDITOR.env.webkit ) { //


->


// We don't mark br, because this situation can happen for htmlified text too. data = data.replace( /<\/(\w+)>

<\/div>$/, function( match, elementName ) { if ( elementName in blockElements ) { evt.data.endsWithEOL = 1; return ''; } return match; } ); } else if ( CKEDITOR.env.gecko ) { // Firefox adds bogus
when user pasted text followed by space(s). data = data.replace( /(\s)
$/, '$1' ); } evt.data.dataValue = data; }, null, null, 3 ); editor.on( 'paste', function( evt ) { var dataObj = evt.data, type = editor._.nextPasteType || dataObj.type, data = dataObj.dataValue, trueType, // Default is 'html'. defaultType = editor.config.clipboard_defaultContentType || 'html', transferType = dataObj.dataTransfer.getTransferType( editor ), isExternalPaste = transferType == CKEDITOR.DATA_TRANSFER_EXTERNAL, isActiveForcePAPT = editor.config.forcePasteAsPlainText === true; // If forced type is 'html' we don't need to know true data type. if ( type == 'html' || dataObj.preSniffing == 'html' ) { trueType = 'html'; } else { trueType = recogniseContentType( data ); } delete editor._.nextPasteType; // Unify text markup. if ( trueType == 'htmlifiedtext' ) { data = htmlifiedTextHtmlification( editor.config, data ); } // Strip presentational markup & unify text markup. // Forced plain text (dialog or forcePAPT). // Note: we do not check dontFilter option in this case, because forcePAPT was implemented // before pasteFilter and pasteFilter is automatically used on Webkit&Blink since 4.5, so // forcePAPT should have priority as it had before 4.5. if ( type == 'text' && trueType == 'html' ) { data = filterContent( editor, data, filtersFactory.get( 'plain-text' ) ); } // External paste and pasteFilter exists and filtering isn't disabled. // Or force filtering even for internal and cross-editor paste, when forcePAPT is active (#620). else if ( isExternalPaste && editor.pasteFilter && !dataObj.dontFilter || isActiveForcePAPT ) { data = filterContent( editor, data, editor.pasteFilter ); } if ( dataObj.startsWithEOL ) { data = '
' + data; } if ( dataObj.endsWithEOL ) { data += '
'; } if ( type == 'auto' ) { type = ( trueType == 'html' || defaultType == 'html' ) ? 'html' : 'text'; } dataObj.type = type; dataObj.dataValue = data; delete dataObj.preSniffing; delete dataObj.startsWithEOL; delete dataObj.endsWithEOL; }, null, null, 6 ); // Inserts processed data into the editor at the end of the // events chain. editor.on( 'paste', function( evt ) { var data = evt.data; if ( data.dataValue ) { editor.insertHtml( data.dataValue, data.type, data.range ); // Defer 'afterPaste' so all other listeners for 'paste' will be fired first. // Fire afterPaste only if paste inserted some HTML. setTimeout( function() { editor.fire( 'afterPaste' ); }, 0 ); } }, null, null, 1000 ); editor.on( 'pasteDialog', function( evt ) { // TODO it's possible that this setTimeout is not needed any more, // because of changes introduced in the same commit as this comment. // Editor.getClipboardData adds listener to the dialog's events which are // fired after a while (not like 'showDialog'). setTimeout( function() { // Open default paste dialog. editor.openDialog( 'paste', evt.data ); }, 0 ); } ); } } ); function firePasteEvents( editor, data, withBeforePaste ) { if ( !data.type ) { data.type = 'auto'; } if ( withBeforePaste ) { // Fire 'beforePaste' event so clipboard flavor get customized // by other plugins. if ( editor.fire( 'beforePaste', data ) === false ) return false; // Event canceled } // Do not fire paste if there is no data (dataValue and dataTranfser are empty). // This check should be done after firing 'beforePaste' because for native paste // 'beforePaste' is by default fired even for empty clipboard. if ( !data.dataValue && data.dataTransfer.isEmpty() ) { return false; } if ( !data.dataValue ) { data.dataValue = ''; } // Because of FF bug we need to use this hack, otherwise cursor is hidden // or it is not possible to move it (https://dev.ckeditor.com/ticket/12420). // Also, check that editor.toolbox exists, because the toolbar plugin might not be loaded (https://dev.ckeditor.com/ticket/13305). if ( CKEDITOR.env.gecko && data.method == 'drop' && editor.toolbox ) { editor.once( 'afterPaste', function() { editor.toolbox.focus(); } ); } return editor.fire( 'paste', data ); } function initPasteClipboard( editor ) { var clipboard = CKEDITOR.plugins.clipboard, preventBeforePasteEvent = 0, preventPasteEvent = 0; addListeners(); addButtonsCommands(); /** * Gets clipboard data by directly accessing the clipboard (IE only) or opening the paste dialog window. * * editor.getClipboardData( function( data ) { * if ( data ) * alert( data.type + ' ' + data.dataValue ); * } ); * * @member CKEDITOR.editor * @param {Function/Object} callbackOrOptions For function, see the `callback` parameter documentation. The object was used before 4.7.0 with the `title` property, to set the paste dialog's title. * @param {Function} callback A function that will be executed with the `data` property of the * {@link CKEDITOR.editor#event-paste paste event} or `null` if none of the capturing methods succeeded. * Since 4.7.0 the `callback` should be provided as a first argument, just like in the example above. This parameter will be removed in * an upcoming major release. */ editor.getClipboardData = function( callbackOrOptions, callback ) { var beforePasteNotCanceled = false, dataType = 'auto'; // Options are optional - args shift. if ( !callback ) { callback = callbackOrOptions; callbackOrOptions = null; } // Listen at the end of listeners chain to see if event wasn't canceled // and to retrieve modified data.type. editor.on( 'beforePaste', onBeforePaste, null, null, 1000 ); // Listen with maximum priority to handle content before everyone else. // This callback will handle paste event that will be fired if direct // access to the clipboard succeed in IE. editor.on( 'paste', onPaste, null, null, 0 ); // If command didn't succeed (only IE allows to access clipboard and only if // user agrees) invoke callback with null, meaning that paste is not blocked. if ( getClipboardDataDirectly() === false ) { // Direct access to the clipboard wasn't successful so remove listener. editor.removeListener( 'paste', onPaste ); // If beforePaste was canceled do not open dialog. // Add listeners only if dialog really opened. 'pasteDialog' can be canceled. if ( editor._.forcePasteDialog && beforePasteNotCanceled && editor.fire( 'pasteDialog' ) ) { editor.on( 'pasteDialogCommit', onDialogCommit ); // 'dialogHide' will be fired after 'pasteDialogCommit'. editor.on( 'dialogHide', function( evt ) { evt.removeListener(); evt.data.removeListener( 'pasteDialogCommit', onDialogCommit ); // Notify even if user canceled dialog (clicked 'cancel', ESC, etc). if ( !evt.data._.committed ) { callback( null ); } } ); } else { callback( null ); } } function onPaste( evt ) { evt.removeListener(); evt.cancel(); callback( evt.data ); } function onBeforePaste( evt ) { evt.removeListener(); beforePasteNotCanceled = true; dataType = evt.data.type; } function onDialogCommit( evt ) { evt.removeListener(); // Cancel pasteDialogCommit so paste dialog won't automatically fire // 'paste' evt by itself. evt.cancel(); callback( { type: dataType, dataValue: evt.data.dataValue, dataTransfer: evt.data.dataTransfer, method: 'paste' } ); } }; function addButtonsCommands() { addButtonCommand( 'Cut', 'cut', createCutCopyCmd( 'cut' ), 10, 1 ); addButtonCommand( 'Copy', 'copy', createCutCopyCmd( 'copy' ), 20, 4 ); addButtonCommand( 'Paste', 'paste', createPasteCmd(), 30, 8 ); // Force adding touchend handler to paste button (#595). if ( !editor._.pasteButtons ) { editor._.pasteButtons = []; } editor._.pasteButtons.push( 'Paste' ); function addButtonCommand( buttonName, commandName, command, toolbarOrder, ctxMenuOrder ) { var lang = editor.lang.clipboard[ commandName ]; editor.addCommand( commandName, command ); editor.ui.addButton && editor.ui.addButton( buttonName, { label: lang, command: commandName, toolbar: 'clipboard,' + toolbarOrder } ); // If the "menu" plugin is loaded, register the menu item. if ( editor.addMenuItems ) { editor.addMenuItem( commandName, { label: lang, command: commandName, group: 'clipboard', order: ctxMenuOrder } ); } } } function addListeners() { editor.on( 'key', onKey ); editor.on( 'contentDom', addPasteListenersToEditable ); // For improved performance, we're checking the readOnly state on selectionChange instead of hooking a key event for that. editor.on( 'selectionChange', setToolbarStates ); // If the "contextmenu" plugin is loaded, register the listeners. if ( editor.contextMenu ) { editor.contextMenu.addListener( function() { return { cut: stateFromNamedCommand( 'cut' ), copy: stateFromNamedCommand( 'copy' ), paste: stateFromNamedCommand( 'paste' ) }; } ); // Adds 'touchend' integration with context menu paste item (#1347). var pasteListener = null; editor.on( 'menuShow', function() { // Remove previous listener. if ( pasteListener ) { pasteListener.removeListener(); pasteListener = null; } // Attach new 'touchend' listeners to context menu paste items. var item = editor.contextMenu.findItemByCommandName( 'paste' ); if ( item && item.element ) { pasteListener = item.element.on( 'touchend', function() { editor._.forcePasteDialog = true; } ); } } ); } // Detect if any of paste buttons was touched. In such case we assume that user is using // touch device and force displaying paste dialog (#595). if ( editor.ui.addButton ) { // Waiting for editor instance to be ready seems to be the most reliable way to // be sure that paste buttons are already created. editor.once( 'instanceReady', function() { if ( !editor._.pasteButtons ) { return; } CKEDITOR.tools.array.forEach( editor._.pasteButtons, function( name ) { var pasteButton = editor.ui.get( name ); // Check if button was not removed by `removeButtons` config. if ( pasteButton ) { var buttonElement = CKEDITOR.document.getById( pasteButton._.id ); if ( buttonElement ) { buttonElement.on( 'touchend', function() { editor._.forcePasteDialog = true; } ); } } } ); } ); } } // Add events listeners to editable. function addPasteListenersToEditable() { var editable = editor.editable(); if ( CKEDITOR.plugins.clipboard.isCustomCopyCutSupported ) { var initOnCopyCut = function( evt ) { // There shouldn't be anything to copy/cut when selection is collapsed (#869). if ( editor.getSelection().isCollapsed() ) { return; } // If user tries to cut in read-only editor, we must prevent default action (https://dev.ckeditor.com/ticket/13872). if ( !editor.readOnly || evt.name != 'cut' ) { clipboard.initPasteDataTransfer( evt, editor ); } evt.data.preventDefault(); }; editable.on( 'copy', initOnCopyCut ); editable.on( 'cut', initOnCopyCut ); // Delete content with the low priority so one can overwrite cut data. editable.on( 'cut', function() { // If user tries to cut in read-only editor, we must prevent default action. (https://dev.ckeditor.com/ticket/13872) if ( !editor.readOnly ) { editor.extractSelectedHtml(); } }, null, null, 999 ); } // We'll be catching all pasted content in one line, regardless of whether // it's introduced by a document command execution (e.g. toolbar buttons) or // user paste behaviors (e.g. CTRL+V). editable.on( clipboard.mainPasteEvent, function( evt ) { if ( clipboard.mainPasteEvent == 'beforepaste' && preventBeforePasteEvent ) { return; } // If you've just asked yourself why preventPasteEventNow() is not here, but // in listener for CTRL+V and exec method of 'paste' command // you've asked the same question we did. // // THE ANSWER: // // First thing to notice - this answer makes sense only for IE, // because other browsers don't listen for 'paste' event. // // What would happen if we move preventPasteEventNow() here? // For: // * CTRL+V - IE fires 'beforepaste', so we prevent 'paste' and pasteDataFromClipboard(). OK. // * editor.execCommand( 'paste' ) - we fire 'beforepaste', so we prevent // 'paste' and pasteDataFromClipboard() and doc.execCommand( 'Paste' ). OK. // * native context menu - IE fires 'beforepaste', so we prevent 'paste', but unfortunately // on IE we fail with pasteDataFromClipboard() here, because of... we don't know why, but // we just fail, so... we paste nothing. FAIL. // * native menu bar - the same as for native context menu. // // But don't you know any way to distinguish first two cases from last two? // Only one - special flag set in CTRL+V handler and exec method of 'paste' // command. And that's what we did using preventPasteEventNow(). pasteDataFromClipboard( evt ); } ); // It's not possible to clearly handle all four paste methods (ctrl+v, native menu bar // native context menu, editor's command) in one 'paste/beforepaste' event in IE. // // For ctrl+v & editor's command it's easy to handle pasting in 'beforepaste' listener, // so we do this. For another two methods it's better to use 'paste' event. // // 'paste' is always being fired after 'beforepaste' (except of weird one on opening native // context menu), so for two methods handled in 'beforepaste' we're canceling 'paste' // using preventPasteEvent state. // // 'paste' event in IE is being fired before getClipboardDataByPastebin executes its callback. // // QUESTION: Why didn't you handle all 4 paste methods in handler for 'paste'? // Wouldn't this just be simpler? // ANSWER: Then we would have to evt.data.preventDefault() only for native // context menu and menu bar pastes. The same with execIECommand(). // That would force us to mark CTRL+V and editor's paste command with // special flag, other than preventPasteEvent. But we still would have to // have preventPasteEvent for the second event fired by execIECommand. // Code would be longer and not cleaner. if ( clipboard.mainPasteEvent == 'beforepaste' ) { editable.on( 'paste', function( evt ) { if ( preventPasteEvent ) { return; } // Cancel next 'paste' event fired by execIECommand( 'paste' ) // at the end of this callback. preventPasteEventNow(); // Prevent native paste. evt.data.preventDefault(); pasteDataFromClipboard( evt ); // Force IE to paste content into pastebin so pasteDataFromClipboard will work. execIECommand( 'paste' ); } ); // If mainPasteEvent is 'beforePaste' (IE before Edge), // dismiss the (wrong) 'beforepaste' event fired on context/toolbar menu open. (https://dev.ckeditor.com/ticket/7953) editable.on( 'contextmenu', preventBeforePasteEventNow, null, null, 0 ); editable.on( 'beforepaste', function( evt ) { // Do not prevent event on CTRL+V and SHIFT+INS because it blocks paste (https://dev.ckeditor.com/ticket/11970). if ( evt.data && !evt.data.$.ctrlKey && !evt.data.$.shiftKey ) preventBeforePasteEventNow(); }, null, null, 0 ); } editable.on( 'beforecut', function() { !preventBeforePasteEvent && fixCut( editor ); } ); var mouseupTimeout; // Use editor.document instead of editable in non-IEs for observing mouseup // since editable won't fire the event if selection process started within // iframe and ended out of the editor (https://dev.ckeditor.com/ticket/9851). editable.attachListener( CKEDITOR.env.ie ? editable : editor.document.getDocumentElement(), 'mouseup', function() { mouseupTimeout = setTimeout( setToolbarStates, 0 ); } ); // Make sure that deferred mouseup callback isn't executed after editor instance // had been destroyed. This may happen when editor.destroy() is called in parallel // with mouseup event (i.e. a button with onclick callback) (https://dev.ckeditor.com/ticket/10219). editor.on( 'destroy', function() { clearTimeout( mouseupTimeout ); } ); editable.on( 'keyup', setToolbarStates ); } // Create object representing Cut or Copy commands. function createCutCopyCmd( type ) { return { type: type, canUndo: type == 'cut', // We can't undo copy to clipboard. startDisabled: true, fakeKeystroke: type == 'cut' ? CKEDITOR.CTRL + 88 /*X*/ : CKEDITOR.CTRL + 67 /*C*/, exec: function() { // Attempts to execute the Cut and Copy operations. function tryToCutCopy( type ) { if ( CKEDITOR.env.ie ) return execIECommand( type ); // non-IEs part try { // Other browsers throw an error if the command is disabled. return editor.document.$.execCommand( type, false, null ); } catch ( e ) { return false; } } this.type == 'cut' && fixCut(); var success = tryToCutCopy( this.type ); if ( !success ) { // Show cutError or copyError. editor.showNotification( editor.lang.clipboard[ this.type + 'Error' ] ); // jshint ignore:line } return success; } }; } function createPasteCmd() { return { // Snapshots are done manually by editable.insertXXX methods. canUndo: false, async: true, fakeKeystroke: CKEDITOR.CTRL + 86 /*V*/, /** * The default implementation of the paste command. * * @private * @param {CKEDITOR.editor} editor An instance of the editor where the command is being executed. * @param {Object/String} data If `data` is a string, then it is considered content that is being pasted. * Otherwise it is treated as an object with options. * @param {Boolean/String} [data.notification=true] Content for a notification shown after an unsuccessful * paste attempt. If `false`, the notification will not be displayed. This parameter was added in 4.7.0. * @param {String} [data.type='html'] The type of pasted content. There are two allowed values: * * 'html' * * 'text' * @param {String/Object} data.dataValue Content being pasted. If this parameter is an object, it * is supposed to be a `data` property of the {@link CKEDITOR.editor#paste} event. * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer Data transfer instance connected * with the current paste action. * @member CKEDITOR.editor.commands.paste */ exec: function( editor, data ) { data = typeof data !== 'undefined' && data !== null ? data : {}; var cmd = this, notification = typeof data.notification !== 'undefined' ? data.notification : true, forcedType = data.type, keystroke = CKEDITOR.tools.keystrokeToString( editor.lang.common.keyboard, editor.getCommandKeystroke( this ) ), msg = typeof notification === 'string' ? notification : editor.lang.clipboard.pasteNotification .replace( /%1/, '' + keystroke.display + '' ), pastedContent = typeof data === 'string' ? data : data.dataValue; function callback( data, withBeforePaste ) { withBeforePaste = typeof withBeforePaste !== 'undefined' ? withBeforePaste : true; if ( data ) { data.method = 'paste'; if ( !data.dataTransfer ) { data.dataTransfer = clipboard.initPasteDataTransfer(); } firePasteEvents( editor, data, withBeforePaste ); } else if ( notification && !editor._.forcePasteDialog ) { editor.showNotification( msg, 'info', editor.config.clipboard_notificationDuration ); } // Reset dialog mode (#595). editor._.forcePasteDialog = false; editor.fire( 'afterCommandExec', { name: 'paste', command: cmd, returnValue: !!data } ); } // Force type for the next paste. Do not force if `config.forcePasteAsPlainText` set to true or 'allow-word' (#1013). if ( forcedType && editor.config.forcePasteAsPlainText !== true && editor.config.forcePasteAsPlainText !== 'allow-word' ) { editor._.nextPasteType = forcedType; } else { delete editor._.nextPasteType; } if ( typeof pastedContent === 'string' ) { callback( { dataValue: pastedContent } ); } else { editor.getClipboardData( callback ); } } }; } function preventPasteEventNow() { preventPasteEvent = 1; // For safety reason we should wait longer than 0/1ms. // We don't know how long execution of quite complex getClipboardData will take // and in for example 'paste' listener execCommand() (which fires 'paste') is called // after getClipboardData finishes. // Luckily, it's impossible to immediately fire another 'paste' event we want to handle, // because we only handle there native context menu and menu bar. setTimeout( function() { preventPasteEvent = 0; }, 100 ); } function preventBeforePasteEventNow() { preventBeforePasteEvent = 1; setTimeout( function() { preventBeforePasteEvent = 0; }, 10 ); } // Tries to execute any of the paste, cut or copy commands in IE. Returns a // boolean indicating that the operation succeeded. // @param {String} command *LOWER CASED* name of command ('paste', 'cut', 'copy'). function execIECommand( command ) { var doc = editor.document, body = doc.getBody(), enabled = false, onExec = function() { enabled = true; }; // The following seems to be the only reliable way to detect that // clipboard commands are enabled in IE. It will fire the // onpaste/oncut/oncopy events only if the security settings allowed // the command to execute. body.on( command, onExec ); // IE7: document.execCommand has problem to paste into positioned element. if ( CKEDITOR.env.version > 7 ) { doc.$.execCommand( command ); } else { doc.$.selection.createRange().execCommand( command ); } body.removeListener( command, onExec ); return enabled; } // Cutting off control type element in IE standards breaks the selection entirely. (https://dev.ckeditor.com/ticket/4881) function fixCut() { if ( !CKEDITOR.env.ie || CKEDITOR.env.quirks ) return; var sel = editor.getSelection(), control, range, dummy; if ( ( sel.getType() == CKEDITOR.SELECTION_ELEMENT ) && ( control = sel.getSelectedElement() ) ) { range = sel.getRanges()[ 0 ]; dummy = editor.document.createText( '' ); dummy.insertBefore( control ); range.setStartBefore( dummy ); range.setEndAfter( control ); sel.selectRanges( [ range ] ); // Clear up the fix if the paste wasn't succeeded. setTimeout( function() { // Element still online? if ( control.getParent() ) { dummy.remove(); sel.selectElement( control ); } }, 0 ); } } // Allow to peek clipboard content by redirecting the // pasting content into a temporary bin and grab the content of it. function getClipboardDataByPastebin( evt, callback ) { var doc = editor.document, editable = editor.editable(), cancel = function( evt ) { evt.cancel(); }, blurListener; // Avoid recursions on 'paste' event or consequent paste too fast. (https://dev.ckeditor.com/ticket/5730) if ( doc.getById( 'cke_pastebin' ) ) return; var sel = editor.getSelection(); var bms = sel.createBookmarks(); // https://dev.ckeditor.com/ticket/11384. On IE9+ we use native selectionchange (i.e. editor#selectionCheck) to cache the most // recent selection which we then lock on editable blur. See selection.js for more info. // selectionchange fired before getClipboardDataByPastebin() cached selection // before creating bookmark (cached selection will be invalid, because bookmarks modified the DOM), // so we need to fire selectionchange one more time, to store current seleciton. // Selection will be locked when we focus pastebin. if ( CKEDITOR.env.ie ) sel.root.fire( 'selectionchange' ); // Create container to paste into. // For rich content we prefer to use "body" since it holds // the least possibility to be splitted by pasted content, while this may // breaks the text selection on a frame-less editable, "div" would be // the best one in that case. // In another case on old IEs moving the selection into a "body" paste bin causes error panic. // Body can't be also used for Opera which fills it with
// what is indistinguishable from pasted
(copying
in Opera isn't possible, // but it can be copied from other browser). var pastebin = new CKEDITOR.dom.element( ( CKEDITOR.env.webkit || editable.is( 'body' ) ) && !CKEDITOR.env.ie ? 'body' : 'div', doc ); pastebin.setAttributes( { id: 'cke_pastebin', 'data-cke-temp': '1' } ); var containerOffset = 0, offsetParent, win = doc.getWindow(); if ( CKEDITOR.env.webkit ) { // It's better to paste close to the real paste destination, so inherited styles // (which Webkits will try to compensate by styling span) differs less from the destination's one. editable.append( pastebin ); // Style pastebin like .cke_editable, to minimize differences between origin and destination. (https://dev.ckeditor.com/ticket/9754) pastebin.addClass( 'cke_editable' ); // Compensate position of offsetParent. if ( !editable.is( 'body' ) ) { // We're not able to get offsetParent from pastebin (body element), so check whether // its parent (editable) is positioned. if ( editable.getComputedStyle( 'position' ) != 'static' ) offsetParent = editable; // And if not - safely get offsetParent from editable. else offsetParent = CKEDITOR.dom.element.get( editable.$.offsetParent ); containerOffset = offsetParent.getDocumentPosition().y; } } else { // Opera and IE doesn't allow to append to html element. editable.getAscendant( CKEDITOR.env.ie ? 'body' : 'html', 1 ).append( pastebin ); } pastebin.setStyles( { position: 'absolute', // Position the bin at the top (+10 for safety) of viewport to avoid any subsequent document scroll. top: ( win.getScrollPosition().y - containerOffset + 10 ) + 'px', width: '1px', // Caret has to fit in that height, otherwise browsers like Chrome & Opera will scroll window to show it. // Set height equal to viewport's height - 20px (safety gaps), minimum 1px. height: Math.max( 1, win.getViewPaneSize().height - 20 ) + 'px', overflow: 'hidden', // Reset styles that can mess up pastebin position. margin: 0, padding: 0 } ); // Paste fails in Safari when the body tag has 'user-select: none'. (https://dev.ckeditor.com/ticket/12506) if ( CKEDITOR.env.safari ) pastebin.setStyles( CKEDITOR.tools.cssVendorPrefix( 'user-select', 'text' ) ); // Check if the paste bin now establishes new editing host. var isEditingHost = pastebin.getParent().isReadOnly(); if ( isEditingHost ) { // Hide the paste bin. pastebin.setOpacity( 0 ); // And make it editable. pastebin.setAttribute( 'contenteditable', true ); } // Transparency is not enough since positioned non-editing host always shows // resize handler, pull it off the screen instead. else { pastebin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-10000px' ); } editor.on( 'selectionChange', cancel, null, null, 0 ); // Webkit fill fire blur on editable when moving selection to // pastebin (if body is used). Cancel it because it causes incorrect // selection lock in case of inline editor (https://dev.ckeditor.com/ticket/10644). // The same seems to apply to Firefox (https://dev.ckeditor.com/ticket/10787). if ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) blurListener = editable.once( 'blur', cancel, null, null, -100 ); // Temporarily move selection to the pastebin. isEditingHost && pastebin.focus(); var range = new CKEDITOR.dom.range( pastebin ); range.selectNodeContents( pastebin ); var selPastebin = range.select(); // If non-native paste is executed, IE will open security alert and blur editable. // Editable will then lock selection inside itself and after accepting security alert // this selection will be restored. We overwrite stored selection, so it's restored // in pastebin. (https://dev.ckeditor.com/ticket/9552) if ( CKEDITOR.env.ie ) { blurListener = editable.once( 'blur', function() { editor.lockSelection( selPastebin ); } ); } var scrollTop = CKEDITOR.document.getWindow().getScrollPosition().y; // Wait a while and grab the pasted contents. setTimeout( function() { // Restore main window's scroll position which could have been changed // by browser in cases described in https://dev.ckeditor.com/ticket/9771. if ( CKEDITOR.env.webkit ) CKEDITOR.document.getBody().$.scrollTop = scrollTop; // Blur will be fired only on non-native paste. In other case manually remove listener. blurListener && blurListener.removeListener(); // Restore properly the document focus. (https://dev.ckeditor.com/ticket/8849) if ( CKEDITOR.env.ie ) editable.focus(); // IE7: selection must go before removing pastebin. (https://dev.ckeditor.com/ticket/8691) sel.selectBookmarks( bms ); pastebin.remove(); // Grab the HTML contents. // We need to look for a apple style wrapper on webkit it also adds // a div wrapper if you copy/paste the body of the editor. // Remove hidden div and restore selection. var bogusSpan; if ( CKEDITOR.env.webkit && ( bogusSpan = pastebin.getFirst() ) && ( bogusSpan.is && bogusSpan.hasClass( 'Apple-style-span' ) ) ) pastebin = bogusSpan; editor.removeListener( 'selectionChange', cancel ); callback( pastebin.getHtml() ); }, 0 ); } // Try to get content directly on IE from clipboard, without native event // being fired before. In other words - synthetically get clipboard data, if it's possible. // mainPasteEvent will be fired, so if forced native paste: // * worked, getClipboardDataByPastebin will grab it, // * didn't work, dataValue and dataTransfer will be empty and editor#paste won't be fired. // Clipboard data can be accessed directly only on IEs older than Edge. // On other browsers we should fire beforePaste event and return false. function getClipboardDataDirectly() { if ( clipboard.mainPasteEvent == 'paste' ) { editor.fire( 'beforePaste', { type: 'auto', method: 'paste' } ); return false; } // Prevent IE from pasting at the begining of the document. editor.focus(); // Command will be handled by 'beforepaste', but as // execIECommand( 'paste' ) will fire also 'paste' event // we're canceling it. preventPasteEventNow(); // https://dev.ckeditor.com/ticket/9247: Lock focus to prevent IE from hiding toolbar for inline editor. var focusManager = editor.focusManager; focusManager.lock(); if ( editor.editable().fire( clipboard.mainPasteEvent ) && !execIECommand( 'paste' ) ) { focusManager.unlock(); return false; } focusManager.unlock(); return true; } // Listens for some clipboard related keystrokes, so they get customized. // Needs to be bind to keydown event. function onKey( event ) { if ( editor.mode != 'wysiwyg' ) return; switch ( event.data.keyCode ) { // Paste case CKEDITOR.CTRL + 86: // CTRL+V case CKEDITOR.SHIFT + 45: // SHIFT+INS var editable = editor.editable(); // Cancel 'paste' event because ctrl+v is for IE handled // by 'beforepaste'. preventPasteEventNow(); // Simulate 'beforepaste' event for all browsers using 'paste' as main event. if ( clipboard.mainPasteEvent == 'paste' ) { editable.fire( 'beforepaste' ); } return; // Cut case CKEDITOR.CTRL + 88: // CTRL+X case CKEDITOR.SHIFT + 46: // SHIFT+DEL // Save Undo snapshot. editor.fire( 'saveSnapshot' ); // Save before cut setTimeout( function() { editor.fire( 'saveSnapshot' ); // Save after cut }, 50 ); // OSX is slow (https://dev.ckeditor.com/ticket/11416). } } function pasteDataFromClipboard( evt ) { // Default type is 'auto', but can be changed by beforePaste listeners. var eventData = { type: 'auto', method: 'paste', dataTransfer: clipboard.initPasteDataTransfer( evt ) }; eventData.dataTransfer.cacheData(); // Fire 'beforePaste' event so clipboard flavor get customized by other plugins. // If 'beforePaste' is canceled continue executing getClipboardDataByPastebin and then do nothing // (do not fire 'paste', 'afterPaste' events). This way we can grab all - synthetically // and natively pasted content and prevent its insertion into editor // after canceling 'beforePaste' event. var beforePasteNotCanceled = editor.fire( 'beforePaste', eventData ) !== false; // Do not use paste bin if the browser let us get HTML or files from dataTranfer. if ( beforePasteNotCanceled && clipboard.canClipboardApiBeTrusted( eventData.dataTransfer, editor ) ) { evt.data.preventDefault(); setTimeout( function() { firePasteEvents( editor, eventData ); }, 0 ); } else { getClipboardDataByPastebin( evt, function( data ) { // Clean up. eventData.dataValue = data.replace( /]+data-cke-bookmark[^<]*?<\/span>/ig, '' ); // Fire remaining events (without beforePaste) beforePasteNotCanceled && firePasteEvents( editor, eventData ); } ); } } function setToolbarStates() { if ( editor.mode != 'wysiwyg' ) return; var pasteState = stateFromNamedCommand( 'paste' ); editor.getCommand( 'cut' ).setState( stateFromNamedCommand( 'cut' ) ); editor.getCommand( 'copy' ).setState( stateFromNamedCommand( 'copy' ) ); editor.getCommand( 'paste' ).setState( pasteState ); editor.fire( 'pasteState', pasteState ); } function stateFromNamedCommand( command ) { var selection = editor.getSelection(), range = selection && selection.getRanges()[ 0 ], // We need to correctly update toolbar states on readOnly (#2775). inReadOnly = editor.readOnly || ( range && range.checkReadOnly() ); if ( inReadOnly && command in { paste: 1, cut: 1 } ) { return CKEDITOR.TRISTATE_DISABLED; } if ( command == 'paste' ) { return CKEDITOR.TRISTATE_OFF; } // Cut, copy - check if the selection is not empty. var sel = editor.getSelection(), ranges = sel.getRanges(), selectionIsEmpty = sel.getType() == CKEDITOR.SELECTION_NONE || ( ranges.length == 1 && ranges[ 0 ].collapsed ); return selectionIsEmpty ? CKEDITOR.TRISTATE_DISABLED : CKEDITOR.TRISTATE_OFF; } } // Returns: // * 'htmlifiedtext' if content looks like transformed by browser from plain text. // See clipboard/paste.html TCs for more info. // * 'html' if it is not 'htmlifiedtext'. function recogniseContentType( data ) { if ( CKEDITOR.env.webkit ) { // Plain text or (

and text inside
). if ( !data.match( /^[^<]*$/g ) && !data.match( /^(
<\/div>|
[^<]*<\/div>)*$/gi ) ) return 'html'; } else if ( CKEDITOR.env.ie ) { // Text and
or ( text and
in

- paragraphs can be separated by new \r\n ). if ( !data.match( /^([^<]|)*$/gi ) && !data.match( /^(

([^<]|)*<\/p>|(\r\n))*$/gi ) ) return 'html'; } else if ( CKEDITOR.env.gecko ) { // Text or
. if ( !data.match( /^([^<]|)*$/gi ) ) return 'html'; } else { return 'html'; } return 'htmlifiedtext'; } // This function transforms what browsers produce when // pasting plain text into editable element (see clipboard/paste.html TCs // for more info) into correct HTML (similar to that produced by text2Html). function htmlifiedTextHtmlification( config, data ) { function repeatParagraphs( repeats ) { // Repeat blocks floor((n+1)/2) times. // Even number of repeats - add
at the beginning of last

. return CKEDITOR.tools.repeat( '

', ~~( repeats / 2 ) ) + ( repeats % 2 == 1 ? '
' : '' ); } // Replace adjacent white-spaces (EOLs too - Fx sometimes keeps them) with one space. // We have to skip \u3000 (IDEOGRAPHIC SPACE) character - it's special space character correctly rendered by the browsers (#1321). data = data.replace( /(?!\u3000)\s+/g, ' ' ) // Remove spaces from between tags. .replace( /> +<' ) // Normalize XHTML syntax and upper cased
tags. .replace( /
/gi, '
' ); // IE - lower cased tags. data = data.replace( /<\/?[A-Z]+>/g, function( match ) { return match.toLowerCase(); } ); // Don't touch single lines (no ) - nothing to do here. if ( data.match( /^[^<]$/ ) ) return data; // Webkit. if ( CKEDITOR.env.webkit && data.indexOf( '

' ) > -1 ) { // One line break at the beginning - insert
data = data.replace( /^(
(
|)<\/div>)(?!$|(
(
|)<\/div>))/g, '
' ) // Two or more - reduce number of new lines by one. .replace( /^(
(
|)<\/div>){2}(?!$)/g, '
' ); // Two line breaks create one paragraph in Webkit. if ( data.match( /
(
|)<\/div>/ ) ) { data = '

' + data.replace( /(

(
|)<\/div>)+/g, function( match ) { return repeatParagraphs( match.split( '
' ).length + 1 ); } ) + '

'; } // One line break create br. data = data.replace( /<\/div>
/g, '
' ); // Remove remaining divs. data = data.replace( /<\/?div>/g, '' ); } // Opera and Firefox and enterMode != BR. if ( CKEDITOR.env.gecko && config.enterMode != CKEDITOR.ENTER_BR ) { // Remove bogus
- Fx generates two for one line break. // For two line breaks it still produces two , but it's better to ignore this case than the first one. if ( CKEDITOR.env.gecko ) data = data.replace( /^

$/, '
' ); // This line satisfy edge case when for Opera we have two line breaks //data = data.replace( /) if ( data.indexOf( '

' ) > -1 ) { // Two line breaks create one paragraph, three - 2, four - 3, etc. data = '

' + data.replace( /(
){2,}/g, function( match ) { return repeatParagraphs( match.length / 4 ); } ) + '

'; } } return switchEnterMode( config, data ); } function filtersFactoryFactory( editor ) { var filters = {}; function setUpTags() { var tags = {}; for ( var tag in CKEDITOR.dtd ) { if ( tag.charAt( 0 ) != '$' && tag != 'div' && tag != 'span' ) { tags[ tag ] = 1; } } return tags; } function createSemanticContentFilter() { var filter = new CKEDITOR.filter( editor, {} ); filter.allow( { $1: { elements: setUpTags(), attributes: true, styles: false, classes: false } } ); return filter; } return { get: function( type ) { if ( type == 'plain-text' ) { // Does this look confusing to you? Did we forget about enter mode? // It is a trick that let's us creating one filter for edidtor, regardless of its // activeEnterMode (which as the name indicates can change during runtime). // // How does it work? // The active enter mode is passed to the filter.applyTo method. // The filter first marks all elements except
as disallowed and then tries to remove // them. However, it cannot remove e.g. a

element completely, because it's a basic structural element, // so it tries to replace it with an element created based on the active enter mode, eventually doing nothing. // // Now you can sleep well. return filters.plainText || ( filters.plainText = new CKEDITOR.filter( editor, 'br' ) ); } else if ( type == 'semantic-content' ) { return filters.semanticContent || ( filters.semanticContent = createSemanticContentFilter() ); } else if ( type ) { // Create filter based on rules (string or object). return new CKEDITOR.filter( editor, type ); } return null; } }; } function filterContent( editor, data, filter ) { var fragment = CKEDITOR.htmlParser.fragment.fromHtml( data ), writer = new CKEDITOR.htmlParser.basicWriter(); filter.applyTo( fragment, true, false, editor.activeEnterMode ); fragment.writeHtml( writer ); return writer.getHtml(); } function switchEnterMode( config, data ) { if ( config.enterMode == CKEDITOR.ENTER_BR ) { data = data.replace( /(<\/p>

)+/g, function( match ) { return CKEDITOR.tools.repeat( '
', match.length / 7 * 2 ); } ).replace( /<\/?p>/g, '' ); } else if ( config.enterMode == CKEDITOR.ENTER_DIV ) { data = data.replace( /<(\/)?p>/g, '<$1div>' ); } return data; } function preventDefaultSetDropEffectToNone( evt ) { evt.data.preventDefault(); evt.data.$.dataTransfer.dropEffect = 'none'; } function initDragDrop( editor ) { var clipboard = CKEDITOR.plugins.clipboard; editor.on( 'contentDom', function() { var editable = editor.editable(), dropTarget = CKEDITOR.plugins.clipboard.getDropTarget( editor ), top = editor.ui.space( 'top' ), bottom = editor.ui.space( 'bottom' ); // -------------- DRAGOVER TOP & BOTTOM -------------- // Not allowing dragging on toolbar and bottom (https://dev.ckeditor.com/ticket/12613). clipboard.preventDefaultDropOnElement( top ); clipboard.preventDefaultDropOnElement( bottom ); // -------------- DRAGSTART -------------- // Listed on dragstart to mark internal and cross-editor drag & drop // and save range and selected HTML. editable.attachListener( dropTarget, 'dragstart', fireDragEvent ); // Make sure to reset data transfer (in case dragend was not called or was canceled). editable.attachListener( editor, 'dragstart', clipboard.resetDragDataTransfer, clipboard, null, 1 ); // Create a dataTransfer object and save it globally. editable.attachListener( editor, 'dragstart', function( evt ) { clipboard.initDragDataTransfer( evt, editor ); }, null, null, 2 ); editable.attachListener( editor, 'dragstart', function() { // Save drag range globally for cross editor D&D. var dragRange = clipboard.dragRange = editor.getSelection().getRanges()[ 0 ]; // Store number of children, so we can later tell if any text node was split on drop. (https://dev.ckeditor.com/ticket/13011, https://dev.ckeditor.com/ticket/13447) if ( CKEDITOR.env.ie && CKEDITOR.env.version < 10 ) { clipboard.dragStartContainerChildCount = dragRange ? getContainerChildCount( dragRange.startContainer ) : null; clipboard.dragEndContainerChildCount = dragRange ? getContainerChildCount( dragRange.endContainer ) : null; } }, null, null, 100 ); // -------------- DRAGEND -------------- // Clean up on dragend. editable.attachListener( dropTarget, 'dragend', fireDragEvent ); // Init data transfer if someone wants to use it in dragend. editable.attachListener( editor, 'dragend', clipboard.initDragDataTransfer, clipboard, null, 1 ); // When drag & drop is done we need to reset dataTransfer so the future // external drop will be not recognize as internal. editable.attachListener( editor, 'dragend', clipboard.resetDragDataTransfer, clipboard, null, 100 ); // -------------- DRAGOVER -------------- // We need to call preventDefault on dragover because otherwise if // we drop image it will overwrite document. editable.attachListener( dropTarget, 'dragover', function( evt ) { // Edge requires this handler to have `preventDefault()` regardless of the situation. if ( CKEDITOR.env.edge ) { evt.data.preventDefault(); return; } var target = evt.data.getTarget(); // Prevent reloading page when dragging image on empty document (https://dev.ckeditor.com/ticket/12619). if ( target && target.is && target.is( 'html' ) ) { evt.data.preventDefault(); return; } // If we do not prevent default dragover on IE the file path // will be loaded and we will lose content. On the other hand // if we prevent it the cursor will not we shown, so we prevent // dragover only on IE, on versions which support file API and only // if the event contains files. if ( CKEDITOR.env.ie && CKEDITOR.plugins.clipboard.isFileApiSupported && evt.data.$.dataTransfer.types.contains( 'Files' ) ) { evt.data.preventDefault(); } } ); // -------------- DROP -------------- editable.attachListener( dropTarget, 'drop', function( evt ) { // Do nothing if event was already prevented. (https://dev.ckeditor.com/ticket/13879) if ( evt.data.$.defaultPrevented ) { return; } // Cancel native drop. evt.data.preventDefault(); var target = evt.data.getTarget(), readOnly = target.isReadOnly(); // Do nothing if drop on non editable element (https://dev.ckeditor.com/ticket/13015). // The tag isn't editable (body is), but we want to allow drop on it // (so it is possible to drop below editor contents). if ( readOnly && !( target.type == CKEDITOR.NODE_ELEMENT && target.is( 'html' ) ) ) { return; } // Getting drop position is one of the most complex parts. var dropRange = clipboard.getRangeAtDropPosition( evt, editor ), dragRange = clipboard.dragRange; // Do nothing if it was not possible to get drop range. if ( !dropRange ) { return; } // Fire drop. fireDragEvent( evt, dragRange, dropRange ); }, null, null, 9999 ); // Create dataTransfer or get it, if it was created before. editable.attachListener( editor, 'drop', clipboard.initDragDataTransfer, clipboard, null, 1 ); // Execute drop action, fire paste. editable.attachListener( editor, 'drop', function( evt ) { var data = evt.data; if ( !data ) { return; } // Let user modify drag and drop range. var dropRange = data.dropRange, dragRange = data.dragRange, dataTransfer = data.dataTransfer; if ( dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_INTERNAL ) { // Execute drop with a timeout because otherwise selection, after drop, // on IE is in the drag position, instead of drop position. setTimeout( function() { clipboard.internalDrop( dragRange, dropRange, dataTransfer, editor ); }, 0 ); } else if ( dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_CROSS_EDITORS ) { crossEditorDrop( dragRange, dropRange, dataTransfer ); } else { externalDrop( dropRange, dataTransfer ); } }, null, null, 9999 ); // Cross editor drag and drop (drag in one Editor and drop in the other). function crossEditorDrop( dragRange, dropRange, dataTransfer ) { // Paste event should be fired before delete contents because otherwise // Chrome have a problem with drop range (Chrome split the drop // range container so the offset is bigger then container length). dropRange.select(); firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop' }, 1 ); // Remove dragged content and make a snapshot. dataTransfer.sourceEditor.fire( 'saveSnapshot' ); dataTransfer.sourceEditor.editable().extractHtmlFromRange( dragRange ); // Make some selection before saving snapshot, otherwise error will be thrown, because // there will be no valid selection after content is removed. dataTransfer.sourceEditor.getSelection().selectRanges( [ dragRange ] ); dataTransfer.sourceEditor.fire( 'saveSnapshot' ); } // Drop from external source. function externalDrop( dropRange, dataTransfer ) { // Paste content into the drop position. dropRange.select(); firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop' }, 1 ); // Usually we reset DataTranfer on dragend, // but dragend is called on the same element as dragstart // so it will not be called on on external drop. clipboard.resetDragDataTransfer(); } // Fire drag/drop events (dragstart, dragend, drop). function fireDragEvent( evt, dragRange, dropRange ) { var eventData = { $: evt.data.$, target: evt.data.getTarget() }; if ( dragRange ) { eventData.dragRange = dragRange; } if ( dropRange ) { eventData.dropRange = dropRange; } if ( editor.fire( evt.name, eventData ) === false ) { evt.data.preventDefault(); } } function getContainerChildCount( container ) { if ( container.type != CKEDITOR.NODE_ELEMENT ) { container = container.getParent(); } return container.getChildCount(); } } ); } /** * @singleton * @class CKEDITOR.plugins.clipboard */ CKEDITOR.plugins.clipboard = { /** * True if the environment allows to set data on copy or cut manually. This value is false in IE, because this browser * shows the security dialog window when the script tries to set clipboard data and on iOS, because custom data is * not saved to clipboard there. * * @since 4.5.0 * @readonly * @property {Boolean} */ isCustomCopyCutSupported: ( !CKEDITOR.env.ie || CKEDITOR.env.version >= 16 ) && !CKEDITOR.env.iOS, /** * True if the environment supports MIME types and custom data types in dataTransfer/cliboardData getData/setData methods. * * @since 4.5.0 * @readonly * @property {Boolean} */ isCustomDataTypesSupported: !CKEDITOR.env.ie || CKEDITOR.env.version >= 16, /** * True if the environment supports File API. * * @since 4.5.0 * @readonly * @property {Boolean} */ isFileApiSupported: !CKEDITOR.env.ie || CKEDITOR.env.version > 9, /** * Main native paste event editable should listen to. * * **Note:** Safari does not like the {@link CKEDITOR.editor#beforePaste} event — it sometimes does not * handle Ctrl+C properly. This is probably caused by some race condition between events. * Chrome, Firefox and Edge work well with both events, so it is better to use {@link CKEDITOR.editor#paste} * which will handle pasting from e.g. browsers' menu bars. * IE7/8 does not like the {@link CKEDITOR.editor#paste} event for which it is throwing random errors. * * @since 4.5.0 * @readonly * @property {String} */ mainPasteEvent: ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) ? 'beforepaste' : 'paste', /** * Adds a new paste button to the editor. * * This method should be called for buttons that should display the Paste Dialog fallback in mobile environments. * See [the rationale](https://github.com/ckeditor/ckeditor-dev/issues/595#issuecomment-345971174) for more * details. * * @since 4.9.0 * @param {CKEDITOR.editor} editor The editor instance. * @param {String} name Name of the button. * @param {Object} definition Definition of the button. */ addPasteButton: function( editor, name, definition ) { if ( !editor.ui.addButton ) { return; } editor.ui.addButton( name, definition ); if ( !editor._.pasteButtons ) { editor._.pasteButtons = []; } editor._.pasteButtons.push( name ); }, /** * Returns `true` if it is expected that a browser provides HTML data through the Clipboard API. * If not, this method returns `false` and as a result CKEditor will use the paste bin. Read more in * the [Clipboard Integration](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_clipboard.html#clipboard-api) guide. * * @since 4.5.2 * @returns {Boolean} */ canClipboardApiBeTrusted: function( dataTransfer, editor ) { // If it's an internal or cross-editor data transfer, then it means that custom cut/copy/paste support works // and that the data were put manually on the data transfer so we can be sure that it's available. if ( dataTransfer.getTransferType( editor ) != CKEDITOR.DATA_TRANSFER_EXTERNAL ) { return true; } // In Chrome we can trust Clipboard API, with the exception of Chrome on Android (in both - mobile and desktop modes), where // clipboard API is not available so we need to check it (https://dev.ckeditor.com/ticket/13187). if ( CKEDITOR.env.chrome && !dataTransfer.isEmpty() ) { return true; } // Because of a Firefox bug HTML data are not available in some cases (e.g. paste from Word), in such cases we // need to use the pastebin (https://dev.ckeditor.com/ticket/13528, https://bugzilla.mozilla.org/show_bug.cgi?id=1183686). if ( CKEDITOR.env.gecko && ( dataTransfer.getData( 'text/html' ) || dataTransfer.getFilesCount() ) ) { return true; } // Safari fixed clipboard in 10.1 (https://bugs.webkit.org/show_bug.cgi?id=19893) (https://dev.ckeditor.com/ticket/16982). // However iOS version still doesn't work well enough (https://bugs.webkit.org/show_bug.cgi?id=19893#c34). if ( CKEDITOR.env.safari && CKEDITOR.env.version >= 603 && !CKEDITOR.env.iOS ) { return true; } // Edge 15 added support for Clipboard API // (https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/6515107-clipboard-api), however it is // usable for our case starting from Edge 16 (#468). if ( CKEDITOR.env.edge && CKEDITOR.env.version >= 16 ) { return true; } // In older Safari and IE HTML data is not available through the Clipboard API. // In older Edge version things are also a bit messy - // https://connect.microsoft.com/IE/feedback/details/1572456/edge-clipboard-api-text-html-content-messed-up-in-event-clipboarddata // It is safer to use the paste bin in unknown cases. return false; }, /** * Returns the element that should be used as the target for the drop event. * * @since 4.5.0 * @param {CKEDITOR.editor} editor The editor instance. * @returns {CKEDITOR.dom.domObject} the element that should be used as the target for the drop event. */ getDropTarget: function( editor ) { var editable = editor.editable(); // https://dev.ckeditor.com/ticket/11123 Firefox needs to listen on document, because otherwise event won't be fired. // https://dev.ckeditor.com/ticket/11086 IE8 cannot listen on document. if ( ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) || editable.isInline() ) { return editable; } else { return editor.document; } }, /** * IE 8 & 9 split text node on drop so the first node contains the * text before the drop position and the second contains the rest. If you * drag the content from the same node you will be not be able to get * it (the range becomes invalid), so you need to join them back. * * Note that the first node in IE 8 & 9 is the original node object * but with shortened content. * * Before: * --- Text Node A ---------------------------------- * /\ * Drag position * * After (IE 8 & 9): * --- Text Node A ----- --- Text Node B ----------- * /\ /\ * Drop position Drag position * (invalid) * * After (other browsers): * --- Text Node A ---------------------------------- * /\ /\ * Drop position Drag position * * **Note:** This function is in the public scope for tests usage only. * * @since 4.5.0 * @private * @param {CKEDITOR.dom.range} dragRange The drag range. * @param {CKEDITOR.dom.range} dropRange The drop range. * @param {Number} preDragStartContainerChildCount The number of children of the drag range start container before the drop. * @param {Number} preDragEndContainerChildCount The number of children of the drag range end container before the drop. */ fixSplitNodesAfterDrop: function( dragRange, dropRange, preDragStartContainerChildCount, preDragEndContainerChildCount ) { var dropContainer = dropRange.startContainer; if ( typeof preDragEndContainerChildCount != 'number' || typeof preDragStartContainerChildCount != 'number' ) { return; } // We are only concerned about ranges anchored in elements. if ( dropContainer.type != CKEDITOR.NODE_ELEMENT ) { return; } if ( handleContainer( dragRange.startContainer, dropContainer, preDragStartContainerChildCount ) ) { return; } if ( handleContainer( dragRange.endContainer, dropContainer, preDragEndContainerChildCount ) ) { return; } function handleContainer( dragContainer, dropContainer, preChildCount ) { var dragElement = dragContainer; if ( dragElement.type == CKEDITOR.NODE_TEXT ) { dragElement = dragContainer.getParent(); } if ( dragElement.equals( dropContainer ) && preChildCount != dropContainer.getChildCount() ) { applyFix( dropRange ); return true; } } function applyFix( dropRange ) { var nodeBefore = dropRange.startContainer.getChild( dropRange.startOffset - 1 ), nodeAfter = dropRange.startContainer.getChild( dropRange.startOffset ); if ( nodeBefore && nodeBefore.type == CKEDITOR.NODE_TEXT && nodeAfter && nodeAfter.type == CKEDITOR.NODE_TEXT ) { var offset = nodeBefore.getLength(); nodeBefore.setText( nodeBefore.getText() + nodeAfter.getText() ); nodeAfter.remove(); dropRange.setStart( nodeBefore, offset ); dropRange.collapse( true ); } } }, /** * Checks whether turning the drag range into bookmarks will invalidate the drop range. * This usually happens when the drop range shares the container with the drag range and is * located after the drag range, but there are countless edge cases. * * This function is stricly related to {@link #internalDrop} which toggles * order in which it creates bookmarks for both ranges based on a value returned * by this method. In some cases this method returns a value which is not necessarily * true in terms of what it was meant to check, but it is convenient, because * we know how it is interpreted in {@link #internalDrop}, so the correct * behavior of the entire algorithm is assured. * * **Note:** This function is in the public scope for tests usage only. * * @since 4.5.0 * @private * @param {CKEDITOR.dom.range} dragRange The first range to compare. * @param {CKEDITOR.dom.range} dropRange The second range to compare. * @returns {Boolean} `true` if the first range is before the second range. */ isDropRangeAffectedByDragRange: function( dragRange, dropRange ) { var dropContainer = dropRange.startContainer, dropOffset = dropRange.endOffset; // Both containers are the same and drop offset is at the same position or later. // " A L] A " " M A " // ^ ^ if ( dragRange.endContainer.equals( dropContainer ) && dragRange.endOffset <= dropOffset ) { return true; } // Bookmark for drag start container will mess up with offsets. // " O [L A " " M A " // ^ ^ if ( dragRange.startContainer.getParent().equals( dropContainer ) && dragRange.startContainer.getIndex() < dropOffset ) { return true; } // Bookmark for drag end container will mess up with offsets. // " O] L A " " M A " // ^ ^ if ( dragRange.endContainer.getParent().equals( dropContainer ) && dragRange.endContainer.getIndex() < dropOffset ) { return true; } return false; }, /** * Internal drag and drop (drag and drop in the same editor instance). * * **Note:** This function is in the public scope for tests usage only. * * @since 4.5.0 * @private * @param {CKEDITOR.dom.range} dragRange The first range to compare. * @param {CKEDITOR.dom.range} dropRange The second range to compare. * @param {CKEDITOR.plugins.clipboard.dataTransfer} dataTransfer * @param {CKEDITOR.editor} editor */ internalDrop: function( dragRange, dropRange, dataTransfer, editor ) { var clipboard = CKEDITOR.plugins.clipboard, editable = editor.editable(), dragBookmark, dropBookmark, isDropRangeAffected; // Save and lock snapshot so there will be only // one snapshot for both remove and insert content. editor.fire( 'saveSnapshot' ); editor.fire( 'lockSnapshot', { dontUpdate: 1 } ); if ( CKEDITOR.env.ie && CKEDITOR.env.version < 10 ) { this.fixSplitNodesAfterDrop( dragRange, dropRange, clipboard.dragStartContainerChildCount, clipboard.dragEndContainerChildCount ); } // Because we manipulate multiple ranges we need to do it carefully, // changing one range (event creating a bookmark) may make other invalid. // We need to change ranges into bookmarks so we can manipulate them easily in the future. // We can change the range which is later in the text before we change the preceding range. // We call isDropRangeAffectedByDragRange to test the order of ranges. isDropRangeAffected = this.isDropRangeAffectedByDragRange( dragRange, dropRange ); if ( !isDropRangeAffected ) { dragBookmark = dragRange.createBookmark( false ); } dropBookmark = dropRange.clone().createBookmark( false ); if ( isDropRangeAffected ) { dragBookmark = dragRange.createBookmark( false ); } // Check if drop range is inside range. // This is an edge case when we drop something on editable's margin/padding. // That space is not treated as a part of the range we drag, so it is possible to drop there. // When we drop, browser tries to find closest drop position and it finds it inside drag range. (https://dev.ckeditor.com/ticket/13453) var startNode = dragBookmark.startNode, endNode = dragBookmark.endNode, dropNode = dropBookmark.startNode, dropInsideDragRange = // Must check endNode because dragRange could be collapsed in some edge cases (simulated DnD). endNode && ( startNode.getPosition( dropNode ) & CKEDITOR.POSITION_PRECEDING ) && ( endNode.getPosition( dropNode ) & CKEDITOR.POSITION_FOLLOWING ); // If the drop range happens to be inside drag range change it's position to the beginning of the drag range. if ( dropInsideDragRange ) { // We only change position of bookmark span that is connected with dropBookmark. // dropRange will be overwritten and set to the dropBookmark later. dropNode.insertBefore( startNode ); } // No we can safely delete content for the drag range... dragRange = editor.createRange(); dragRange.moveToBookmark( dragBookmark ); editable.extractHtmlFromRange( dragRange, 1 ); // ...and paste content into the drop position. dropRange = editor.createRange(); // Get actual selection with bookmarks if drop's bookmark are not in editable any longer. // This might happen after extracting content from range (#2292). if ( !dropBookmark.startNode.getCommonAncestor( editable ) ) { dropBookmark = editor.getSelection().createBookmarks()[ 0 ]; } dropRange.moveToBookmark( dropBookmark ); // We do not select drop range, because of may be in the place we can not set the selection // (e.g. between blocks, in case of block widget D&D). We put range to the paste event instead. firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop', range: dropRange }, 1 ); editor.fire( 'unlockSnapshot' ); }, /** * Gets the range from the `drop` event. * * @since 4.5.0 * @param {Object} domEvent A native DOM drop event object. * @param {CKEDITOR.editor} editor The source editor instance. * @returns {CKEDITOR.dom.range} range at drop position. */ getRangeAtDropPosition: function( dropEvt, editor ) { var $evt = dropEvt.data.$, x = $evt.clientX, y = $evt.clientY, $range, defaultRange = editor.getSelection( true ).getRanges()[ 0 ], range = editor.createRange(); // Make testing possible. if ( dropEvt.data.testRange ) return dropEvt.data.testRange; // Webkits. if ( document.caretRangeFromPoint && editor.document.$.caretRangeFromPoint( x, y ) ) { $range = editor.document.$.caretRangeFromPoint( x, y ); range.setStart( CKEDITOR.dom.node( $range.startContainer ), $range.startOffset ); range.collapse( true ); } // FF. else if ( $evt.rangeParent ) { range.setStart( CKEDITOR.dom.node( $evt.rangeParent ), $evt.rangeOffset ); range.collapse( true ); } // IEs 9+. // We check if editable is focused to make sure that it's an internal DnD. External DnD must use the second // mechanism because of https://dev.ckeditor.com/ticket/13472#comment:6. else if ( CKEDITOR.env.ie && CKEDITOR.env.version > 8 && defaultRange && editor.editable().hasFocus ) { // On IE 9+ range by default is where we expected it. // defaultRange may be undefined if dragover was canceled (file drop). return defaultRange; } // IE 8 and all IEs if !defaultRange or external DnD. else if ( document.body.createTextRange ) { // To use this method we need a focus (which may be somewhere else in case of external drop). editor.focus(); $range = editor.document.getBody().$.createTextRange(); try { var sucess = false; // If user drop between text line IEs moveToPoint throws exception: // // Lorem ipsum pulvinar purus et euismod // // dolor sit amet,| consectetur adipiscing // * // vestibulum tincidunt augue eget tempus. // // * - drop position // | - expected cursor position // // So we try to call moveToPoint with +-1px up to +-20px above or // below original drop position to find nearest good drop position. for ( var i = 0; i < 20 && !sucess; i++ ) { if ( !sucess ) { try { $range.moveToPoint( x, y - i ); sucess = true; } catch ( err ) { } } if ( !sucess ) { try { $range.moveToPoint( x, y + i ); sucess = true; } catch ( err ) { } } } if ( sucess ) { var id = 'cke-temp-' + ( new Date() ).getTime(); $range.pasteHTML( '\u200b' ); var span = editor.document.getById( id ); range.moveToPosition( span, CKEDITOR.POSITION_BEFORE_START ); span.remove(); } else { // If the fist method does not succeed we might be next to // the short element (like header): // // Lorem ipsum pulvinar purus et euismod. // // // SOME HEADER| * // // // vestibulum tincidunt augue eget tempus. // // * - drop position // | - expected cursor position // // In such situation elementFromPoint returns proper element. Using getClientRect // it is possible to check if the cursor should be at the beginning or at the end // of paragraph. var $element = editor.document.$.elementFromPoint( x, y ), element = new CKEDITOR.dom.element( $element ), rect; if ( !element.equals( editor.editable() ) && element.getName() != 'html' ) { rect = element.getClientRect(); if ( x < rect.left ) { range.setStartAt( element, CKEDITOR.POSITION_AFTER_START ); range.collapse( true ); } else { range.setStartAt( element, CKEDITOR.POSITION_BEFORE_END ); range.collapse( true ); } } // If drop happens on no element elementFromPoint returns html or body. // // * |Lorem ipsum pulvinar purus et euismod. // // vestibulum tincidunt augue eget tempus. // // * - drop position // | - expected cursor position // // In such case we can try to use default selection. If startContainer is not // 'editable' element it is probably proper selection. else if ( defaultRange && defaultRange.startContainer && !defaultRange.startContainer.equals( editor.editable() ) ) { return defaultRange; // Otherwise we can not find any drop position and we have to return null // and cancel drop event. } else { return null; } } } catch ( err ) { return null; } } else { return null; } return range; }, /** * This function tries to link the `evt.data.dataTransfer` property of the {@link CKEDITOR.editor#dragstart}, * {@link CKEDITOR.editor#dragend} and {@link CKEDITOR.editor#drop} events to a single * {@link CKEDITOR.plugins.clipboard.dataTransfer} object. * * This method is automatically used by the core of the drag and drop functionality and * usually does not have to be called manually when using the drag and drop events. * * This method behaves differently depending on whether the drag and drop events were fired * artificially (to represent a non-native drag and drop) or whether they were caused by the native drag and drop. * * If the native event is not available, then it will create a new {@link CKEDITOR.plugins.clipboard.dataTransfer} * instance (if it does not exist already) and will link it to this and all following event objects until * the {@link #resetDragDataTransfer} method is called. It means that all three drag and drop events must be fired * in order to ensure that the data transfer is bound correctly. * * If the native event is available, then the {@link CKEDITOR.plugins.clipboard.dataTransfer} is identified * by its ID and a new instance is assigned to the `evt.data.dataTransfer` only if the ID changed or * the {@link #resetDragDataTransfer} method was called. * * @since 4.5.0 * @param {CKEDITOR.dom.event} [evt] A drop event object. * @param {CKEDITOR.editor} [sourceEditor] The source editor instance. */ initDragDataTransfer: function( evt, sourceEditor ) { // Create a new dataTransfer object based on the drop event. // If this event was used on dragstart to create dataTransfer // both dataTransfer objects will have the same id. var nativeDataTransfer = evt.data.$ ? evt.data.$.dataTransfer : null, dataTransfer = new this.dataTransfer( nativeDataTransfer, sourceEditor ); // Set dataTransfer.id only for 'dragstart' event (so for events initializing dataTransfer inside editor) (#962). if ( evt.name === 'dragstart' ) { dataTransfer.storeId(); } if ( !nativeDataTransfer ) { // No native event. if ( this.dragData ) { dataTransfer = this.dragData; } else { this.dragData = dataTransfer; } } else { // Native event. If there is the same id we will replace dataTransfer with the one // created on drag, because it contains drag editor, drag content and so on. // Otherwise (in case of drag from external source) we save new object to // the global clipboard.dragData. if ( this.dragData && dataTransfer.id == this.dragData.id ) { dataTransfer = this.dragData; } else { this.dragData = dataTransfer; } } evt.data.dataTransfer = dataTransfer; }, /** * Removes the global {@link #dragData} so the next call to {@link #initDragDataTransfer} * always creates a new instance of {@link CKEDITOR.plugins.clipboard.dataTransfer}. * * @since 4.5.0 */ resetDragDataTransfer: function() { this.dragData = null; }, /** * Global object storing the data transfer of the current drag and drop operation. * Do not use it directly, use {@link #initDragDataTransfer} and {@link #resetDragDataTransfer}. * * Note: This object is global (meaning that it is not related to a single editor instance) * in order to handle drag and drop from one editor into another. * * @since 4.5.0 * @private * @property {CKEDITOR.plugins.clipboard.dataTransfer} dragData */ /** * Range object to save the drag range and remove its content after the drop. * * @since 4.5.0 * @private * @property {CKEDITOR.dom.range} dragRange */ /** * Initializes and links data transfer objects based on the paste event. If the data * transfer object was already initialized on this event, the function will * return that object. In IE it is not possible to link copy/cut and paste events * so the method always returns a new object. The same happens if there is no paste event * passed to the method. * * @since 4.5.0 * @param {CKEDITOR.dom.event} [evt] A paste event object. * @param {CKEDITOR.editor} [sourceEditor] The source editor instance. * @returns {CKEDITOR.plugins.clipboard.dataTransfer} The data transfer object. */ initPasteDataTransfer: function( evt, sourceEditor ) { if ( !this.isCustomCopyCutSupported ) { // Edge < 16 does not support custom copy/cut, but it has some useful data in the clipboardData (https://dev.ckeditor.com/ticket/13755). return new this.dataTransfer( ( CKEDITOR.env.edge && evt && evt.data.$ && evt.data.$.clipboardData ) || null, sourceEditor ); } else if ( evt && evt.data && evt.data.$ ) { var clipboardData = evt.data.$.clipboardData, dataTransfer = new this.dataTransfer( clipboardData, sourceEditor ); // Set dataTransfer.id only for 'copy'/'cut' events (so for events initializing dataTransfer inside editor) (#962). if ( evt.name === 'copy' || evt.name === 'cut' ) { dataTransfer.storeId(); } if ( this.copyCutData && dataTransfer.id == this.copyCutData.id ) { dataTransfer = this.copyCutData; dataTransfer.$ = clipboardData; } else { this.copyCutData = dataTransfer; } return dataTransfer; } else { return new this.dataTransfer( null, sourceEditor ); } }, /** * Prevents dropping on the specified element. * * @since 4.5.0 * @param {CKEDITOR.dom.element} element The element on which dropping should be disabled. */ preventDefaultDropOnElement: function( element ) { element && element.on( 'dragover', preventDefaultSetDropEffectToNone ); } }; // Data type used to link drag and drop events. // // In IE URL data type is buggie and there is no way to mark drag & drop without // modifying text data (which would be displayed if user drop content to the textarea) // so we just read dragged text. // // In Chrome and Firefox we can use custom data types. clipboardIdDataType = CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ? 'cke/id' : 'Text'; /** * Facade for the native `dataTransfer`/`clipboadData` object to hide all differences * between browsers. * * @since 4.5.0 * @class CKEDITOR.plugins.clipboard.dataTransfer * @constructor Creates a class instance. * @param {Object} [nativeDataTransfer] A native data transfer object. * @param {CKEDITOR.editor} [editor] The source editor instance. If the editor is defined, dataValue will * be created based on the editor content and the type will be 'html'. */ CKEDITOR.plugins.clipboard.dataTransfer = function( nativeDataTransfer, editor ) { if ( nativeDataTransfer ) { this.$ = nativeDataTransfer; } this._ = { metaRegExp: /^/i, bodyRegExp: /([\s\S]*)<\/body>/i, fragmentRegExp: //g, data: {}, files: [], // Stores full HTML so it can be accessed asynchronously with `getData( 'text/html', true )`. nativeHtmlCache: '', normalizeType: function( type ) { type = type.toLowerCase(); if ( type == 'text' || type == 'text/plain' ) { return 'Text'; // IE support only Text and URL; } else if ( type == 'url' ) { return 'URL'; // IE support only Text and URL; } else { return type; } } }; this._.fallbackDataTransfer = new CKEDITOR.plugins.clipboard.fallbackDataTransfer( this ); // Check if ID is already created. this.id = this.getData( clipboardIdDataType ); // If there is no ID we need to create it. Different browsers needs different ID. if ( !this.id ) { if ( clipboardIdDataType == 'Text' ) { // For IE10+ only Text data type is supported and we have to compare dragged // and dropped text. If the ID is not set it means that empty string was dragged // (ex. image with no alt). We change null to empty string. this.id = ''; } else { // String for custom data type. this.id = 'cke-' + CKEDITOR.tools.getUniqueId(); } } if ( editor ) { this.sourceEditor = editor; this.setData( 'text/html', editor.getSelectedHtml( 1 ) ); // Without setData( 'text', ... ) on dragstart there is no drop event in Safari. // Also 'text' data is empty as drop to the textarea does not work if we do not put there text. if ( clipboardIdDataType != 'Text' && !this.getData( 'text/plain' ) ) { this.setData( 'text/plain', editor.getSelection().getSelectedText() ); } } /** * Data transfer ID used to bind all dataTransfer * objects based on the same event (e.g. in drag and drop events). * * @readonly * @property {String} id */ /** * A native DOM event object. * * @readonly * @property {Object} $ */ /** * Source editor — the editor where the drag starts. * Might be undefined if the drag starts outside the editor (e.g. when dropping files to the editor). * * @readonly * @property {CKEDITOR.editor} sourceEditor */ /** * Private properties and methods. * * @private * @property {Object} _ */ }; /** * Data transfer operation (drag and drop or copy and paste) started and ended in the same * editor instance. * * @since 4.5.0 * @readonly * @property {Number} [=1] * @member CKEDITOR */ CKEDITOR.DATA_TRANSFER_INTERNAL = 1; /** * Data transfer operation (drag and drop or copy and paste) started in one editor * instance and ended in another. * * @since 4.5.0 * @readonly * @property {Number} [=2] * @member CKEDITOR */ CKEDITOR.DATA_TRANSFER_CROSS_EDITORS = 2; /** * Data transfer operation (drag and drop or copy and paste) started outside of the editor. * The source of the data may be a textarea, HTML, another application, etc. * * @since 4.5.0 * @readonly * @property {Number} [=3] * @member CKEDITOR */ CKEDITOR.DATA_TRANSFER_EXTERNAL = 3; CKEDITOR.plugins.clipboard.dataTransfer.prototype = { /** * Facade for the native `getData` method. * * @param {String} type The type of data to retrieve. * @param {Boolean} [getNative=false] Indicates if the whole, original content of the dataTransfer should be returned. * Introduced in CKEditor 4.7.0. * @returns {String} type Stored data for the given type or an empty string if the data for that type does not exist. */ getData: function( type, getNative ) { function isEmpty( data ) { return data === undefined || data === null || data === ''; } function filterUnwantedCharacters( data ) { if ( typeof data !== 'string' ) { return data; } var htmlEnd = data.indexOf( '' ); if ( htmlEnd !== -1 ) { // Just cut everything after ``, so everything after htmlEnd index + length of ``. // Required to workaround bug: https://bugs.chromium.org/p/chromium/issues/detail?id=696978 return data.substring( 0, htmlEnd + 7 ); } return data; } type = this._.normalizeType( type ); var data = type == 'text/html' && getNative ? this._.nativeHtmlCache : this._.data[ type ]; if ( isEmpty( data ) ) { if ( this._.fallbackDataTransfer.isRequired() ) { data = this._.fallbackDataTransfer.getData( type, getNative ); } else { try { data = this.$.getData( type ) || ''; } catch ( e ) { data = ''; } } if ( type == 'text/html' && !getNative ) { data = this._stripHtml( data ); } } // Firefox on Linux put files paths as a text/plain data if there are files // in the dataTransfer object. We need to hide it, because files should be // handled on paste only if dataValue is empty. if ( type == 'Text' && CKEDITOR.env.gecko && this.getFilesCount() && data.substring( 0, 7 ) == 'file://' ) { data = ''; } return filterUnwantedCharacters( data ); }, /** * Facade for the native `setData` method. * * @param {String} type The type of data to retrieve. * @param {String} value The data to add. */ setData: function( type, value ) { type = this._.normalizeType( type ); if ( type == 'text/html' ) { this._.data[ type ] = this._stripHtml( value ); // If 'text/html' is set manually we also store it in `nativeHtmlCache` without modifications. this._.nativeHtmlCache = value; } else { this._.data[ type ] = value; } // There is "Unexpected call to method or property access." error if you try // to set data of unsupported type on IE. if ( !CKEDITOR.plugins.clipboard.isCustomDataTypesSupported && type != 'URL' && type != 'Text' ) { return; } // If we use the text type to bind the ID, then if someone tries to set the text, we must also // update ID accordingly. https://dev.ckeditor.com/ticket/13468. if ( clipboardIdDataType == 'Text' && type == 'Text' ) { this.id = value; } if ( this._.fallbackDataTransfer.isRequired() ) { this._.fallbackDataTransfer.setData( type, value ); } else { try { this.$.setData( type, value ); } catch ( e ) {} } }, /** * Stores dataTransfer id in native data transfer object * so it can be retrieved by other events. * * @since 4.8.0 */ storeId: function() { if ( clipboardIdDataType !== 'Text' ) { this.setData( clipboardIdDataType, this.id ); } }, /** * Gets the data transfer type. * * @param {CKEDITOR.editor} targetEditor The drop/paste target editor instance. * @returns {Number} Possible values: {@link CKEDITOR#DATA_TRANSFER_INTERNAL}, * {@link CKEDITOR#DATA_TRANSFER_CROSS_EDITORS}, {@link CKEDITOR#DATA_TRANSFER_EXTERNAL}. */ getTransferType: function( targetEditor ) { if ( !this.sourceEditor ) { return CKEDITOR.DATA_TRANSFER_EXTERNAL; } else if ( this.sourceEditor == targetEditor ) { return CKEDITOR.DATA_TRANSFER_INTERNAL; } else { return CKEDITOR.DATA_TRANSFER_CROSS_EDITORS; } }, /** * Copies the data from the native data transfer to a private cache. * This function is needed because the data from the native data transfer * is available only synchronously to the event listener. It is not possible * to get the data asynchronously, after a timeout, and the {@link CKEDITOR.editor#paste} * event is fired asynchronously — hence the need for caching the data. */ cacheData: function() { if ( !this.$ ) { return; } var that = this, i, file; function getAndSetData( type ) { type = that._.normalizeType( type ); var data = that.getData( type ); // Cache full html. if ( type == 'text/html' ) { that._.nativeHtmlCache = that.getData( type, true ); data = that._stripHtml( data ); } if ( data ) { that._.data[ type ] = data; } } // Copy data. if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ) { if ( this.$.types ) { for ( i = 0; i < this.$.types.length; i++ ) { getAndSetData( this.$.types[ i ] ); } } } else { getAndSetData( 'Text' ); getAndSetData( 'URL' ); } // Copy files references. file = this._getImageFromClipboard(); if ( ( this.$ && this.$.files ) || file ) { this._.files = []; // Edge have empty files property with no length (https://dev.ckeditor.com/ticket/13755). if ( this.$.files && this.$.files.length ) { for ( i = 0; i < this.$.files.length; i++ ) { this._.files.push( this.$.files[ i ] ); } } // Don't include $.items if both $.files and $.items contains files, because, // according to spec and browsers behavior, they contain the same files. if ( this._.files.length === 0 && file ) { this._.files.push( file ); } } }, /** * Gets the number of files in the dataTransfer object. * * @returns {Number} The number of files. */ getFilesCount: function() { if ( this._.files.length ) { return this._.files.length; } if ( this.$ && this.$.files && this.$.files.length ) { return this.$.files.length; } return this._getImageFromClipboard() ? 1 : 0; }, /** * Gets the file at the index given. * * @param {Number} i Index. * @returns {File} File instance. */ getFile: function( i ) { if ( this._.files.length ) { return this._.files[ i ]; } if ( this.$ && this.$.files && this.$.files.length ) { return this.$.files[ i ]; } // File or null if the file was not found. return i === 0 ? this._getImageFromClipboard() : undefined; }, /** * Checks if the data transfer contains any data. * * @returns {Boolean} `true` if the object contains no data. */ isEmpty: function() { var typesToCheck = {}, type; // If dataTransfer contains files it is not empty. if ( this.getFilesCount() ) { return false; } CKEDITOR.tools.array.forEach( CKEDITOR.tools.object.keys( this._.data ), function( type ) { typesToCheck[ type ] = 1; } ); // Add native types. if ( this.$ ) { if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ) { if ( this.$.types ) { for ( var i = 0; i < this.$.types.length; i++ ) { typesToCheck[ this.$.types[ i ] ] = 1; } } } else { typesToCheck.Text = 1; typesToCheck.URL = 1; } } // Remove ID. if ( clipboardIdDataType != 'Text' ) { typesToCheck[ clipboardIdDataType ] = 0; } for ( type in typesToCheck ) { if ( typesToCheck[ type ] && this.getData( type ) !== '' ) { return false; } } return true; }, /** * When the content of the clipboard is pasted in Chrome, the clipboard data object has an empty `files` property, * but it is possible to get the file as `items[0].getAsFile();` (https://dev.ckeditor.com/ticket/12961). * * @private * @returns {File} File instance or `null` if not found. */ _getImageFromClipboard: function() { var file; try { if ( this.$ && this.$.items && this.$.items[ 0 ] ) { file = this.$.items[ 0 ].getAsFile(); // Duck typing if ( file && file.type ) { return file; } } } catch ( err ) { // noop } return undefined; }, /** * This function removes this meta information and returns only the contents of the `` element if found. * * Various environments use miscellaneous meta tags in HTML clipboard, e.g. * * * `` at the begging of the HTML data. * * Surrounding HTML with `` and `` nested within `` elements. * * @private * @param {String} html * @returns {String} */ _stripHtml: function( html ) { var result = html; // Passed HTML may be empty or null. There is no need to strip such values (#1299). if ( result && result.length ) { // See https://dev.ckeditor.com/ticket/13583 for more details. // Additionally https://dev.ckeditor.com/ticket/16847 adds a flag allowing to get the whole, original content. result = result.replace( this._.metaRegExp, '' ); // Keep only contents of the element var match = this._.bodyRegExp.exec( result ); if ( match && match.length ) { result = match[ 1 ]; // Remove also comments. result = result.replace( this._.fragmentRegExp, '' ); } } return result; } }; /** * Fallback dataTransfer object which is used together with {@link CKEDITOR.plugins.clipboard.dataTransfer} * for browsers supporting Clipboard API, but not supporting custom * MIME types (Edge 16+, see [ckeditor-dev/issues/#962](https://github.com/ckeditor/ckeditor-dev/issues/962)). * * @since 4.8.0 * @class CKEDITOR.plugins.clipboard.fallbackDataTransfer * @constructor * @param {CKEDITOR.plugins.clipboard.dataTransfer} dataTransfer DataTransfer * object which internal cache and * {@link CKEDITOR.plugins.clipboard.dataTransfer#$ data transfer} objects will be reused. */ CKEDITOR.plugins.clipboard.fallbackDataTransfer = function( dataTransfer ) { /** * DataTransfer object which internal cache and * {@link CKEDITOR.plugins.clipboard.dataTransfer#$ data transfer} objects will be modified if needed. * * @private * @property {CKEDITOR.plugins.clipboard.dataTransfer} _dataTransfer */ this._dataTransfer = dataTransfer; /** * A MIME type used for storing custom MIME types. * * @private * @property {String} [_customDataFallbackType='text/html'] */ this._customDataFallbackType = 'text/html'; }; /** * True if the environment supports custom MIME types in {@link CKEDITOR.plugins.clipboard.dataTransfer#getData} * and {@link CKEDITOR.plugins.clipboard.dataTransfer#setData} methods. * * Introduced to distinguish between browsers which support only some whitelisted types (like `text/html`, `application/xml`), * but do not support custom MIME types (like `cke/id`). When the value of this property equals `null` * it means it was not yet initialized. * * This property should not be accessed directly, use {@link #isRequired} method instead. * * @private * @static * @property {Boolean} */ CKEDITOR.plugins.clipboard.fallbackDataTransfer._isCustomMimeTypeSupported = null; /** * Array containing MIME types which are not supported by native `setData`. Those types are * recognized by error which is thrown when using native `setData` with a given type * (see {@link CKEDITOR.plugins.clipboard.fallbackDataTransfer#_isUnsupportedMimeTypeError}). * * @private * @static * @property {String[]} */ CKEDITOR.plugins.clipboard.fallbackDataTransfer._customTypes = []; CKEDITOR.plugins.clipboard.fallbackDataTransfer.prototype = { /** * Whether {@link CKEDITOR.plugins.clipboard.fallbackDataTransfer fallbackDataTransfer object} should * be used when operating on native `dataTransfer`. If `true` is returned, it means custom MIME types * are not supported in the current browser (see {@link #_isCustomMimeTypeSupported}). * * @returns {Boolean} */ isRequired: function() { var fallbackDataTransfer = CKEDITOR.plugins.clipboard.fallbackDataTransfer, nativeDataTransfer = this._dataTransfer.$; if ( fallbackDataTransfer._isCustomMimeTypeSupported === null ) { // If there is no `dataTransfer` we cannot detect if fallback is needed. // Method returns `false` so regular flow will be applied. if ( !nativeDataTransfer ) { return false; } else { var testValue = 'cke test value', testType = 'cke/mimetypetest'; fallbackDataTransfer._isCustomMimeTypeSupported = false; // It looks like after our custom MIME type test Edge 17 is denying access on nativeDataTransfer (#2169). // Upstream issue: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/18089287/ if ( CKEDITOR.env.edge && CKEDITOR.env.version >= 17 ) { return true; } try { nativeDataTransfer.setData( testType, testValue ); fallbackDataTransfer._isCustomMimeTypeSupported = nativeDataTransfer.getData( testType ) === testValue; nativeDataTransfer.clearData( testType ); } catch ( e ) {} } } return !fallbackDataTransfer._isCustomMimeTypeSupported; }, /** * Returns the data of the given MIME type if stored in a regular way or in a special comment. If given type * is the same as {@link #_customDataFallbackType} the whole data without special comment is returned. * * @param {String} type * @param {Boolean} [getNative=false] Indicates if the whole, original content of the dataTransfer should be returned. * @returns {String} */ getData: function( type, getNative ) { // As cache is already checked in CKEDITOR.plugins.clipboard.dataTransfer#getData it is skipped // here. So the assumption is the given type is not in cache. var nativeData = this._getData( this._customDataFallbackType, true ); if ( getNative ) { return nativeData; } var dataComment = this._extractDataComment( nativeData ), value = null; // If we are getting the same type which may store custom data we need to extract content only. if ( type === this._customDataFallbackType ) { value = dataComment.content; } else { // If we are getting different type we need to check inside data comment if it is stored there. if ( dataComment.data && dataComment.data[ type ] ) { value = dataComment.data[ type ]; } else { // And then fallback to regular `getData`. value = this._getData( type, true ); } } return value !== null ? value : ''; }, /** * Sets given data in native `dataTransfer` object. If given MIME type is not supported it uses * {@link #_customDataFallbackType} MIME type to save data using special comment format: * * * * It is important to keep in mind that `{ type: value }` object is stringified (using `JSON.stringify`) * and encoded (using `encodeURIComponent`). * * @param {String} type * @param {String} value * @returns {String} The value which was set. */ setData: function( type, value ) { // In case of fallbackDataTransfer, cache does not reflect native data one-to-one. For example, having // types like text/plain, text/html, cke/id will result in cache storing: // // { // text/plain: value1, // text/html: value2, // cke/id: value3 // } // // and native dataTransfer storing: // // { // text/plain: value1, // text/html: value2 // } // // This way, accessing cache will always return proper value for a given type without a need for further processing. // Cache is already set in CKEDITOR.plugins.clipboard.dataTransfer#setData so it is skipped here. var isFallbackDataType = type === this._customDataFallbackType; if ( isFallbackDataType ) { value = this._applyDataComment( value, this._getFallbackTypeData() ); } var data = value, nativeDataTransfer = this._dataTransfer.$; try { nativeDataTransfer.setData( type, data ); if ( isFallbackDataType ) { // If fallback type used, the native data is different so we overwrite `nativeHtmlCache` here. this._dataTransfer._.nativeHtmlCache = data; } } catch ( e ) { if ( this._isUnsupportedMimeTypeError( e ) ) { var fallbackDataTransfer = CKEDITOR.plugins.clipboard.fallbackDataTransfer; if ( CKEDITOR.tools.indexOf( fallbackDataTransfer._customTypes, type ) === -1 ) { fallbackDataTransfer._customTypes.push( type ); } var fallbackTypeContent = this._getFallbackTypeContent(), fallbackTypeData = this._getFallbackTypeData(); fallbackTypeData[ type ] = data; try { data = this._applyDataComment( fallbackTypeContent, fallbackTypeData ); nativeDataTransfer.setData( this._customDataFallbackType, data ); // Again, fallback type was changed, so we need to refresh the cache. this._dataTransfer._.nativeHtmlCache = data; } catch ( e ) { data = ''; // Some dev logger should be added here. } } } return data; }, /** * Native getData wrapper. * * @private * @param {String} type * @param {Boolean} [skipCache=false] * @returns {String|null} */ _getData: function( type, skipCache ) { var cache = this._dataTransfer._.data; if ( !skipCache && cache[ type ] ) { return cache[ type ]; } else { try { return this._dataTransfer.$.getData( type ); } catch ( e ) { return null; } } }, /** * Returns content stored in {@link #\_customDataFallbackType}. Content is always first retrieved * from {@link #_dataTransfer} cache and then from native `dataTransfer` object. * * @private * @returns {String} */ _getFallbackTypeContent: function() { var fallbackTypeContent = this._dataTransfer._.data[ this._customDataFallbackType ]; if ( !fallbackTypeContent ) { fallbackTypeContent = this._extractDataComment( this._getData( this._customDataFallbackType, true ) ).content; } return fallbackTypeContent; }, /** * Returns custom data stored in {@link #\_customDataFallbackType}. Custom data is always first retrieved * from {@link #_dataTransfer} cache and then from native `dataTransfer` object. * * @private * @returns {Object} */ _getFallbackTypeData: function() { var fallbackTypes = CKEDITOR.plugins.clipboard.fallbackDataTransfer._customTypes, fallbackTypeData = this._extractDataComment( this._getData( this._customDataFallbackType, true ) ).data || {}, cache = this._dataTransfer._.data; CKEDITOR.tools.array.forEach( fallbackTypes, function( type ) { if ( cache[ type ] !== undefined ) { fallbackTypeData[ type ] = cache[ type ]; } else if ( fallbackTypeData[ type ] !== undefined ) { fallbackTypeData[ type ] = fallbackTypeData[ type ]; } }, this ); return fallbackTypeData; }, /** * Whether provided error means that unsupported MIME type was used when calling native `dataTransfer.setData` method. * * @private * @param {Error} error * @returns {Boolean} */ _isUnsupportedMimeTypeError: function( error ) { return error.message && error.message.search( /element not found/gi ) !== -1; }, /** * Extracts `cke-data` comment from the given content. * * @private * @param {String} content * @returns {Object} Returns an object containing extracted data as `data` * and content (without `cke-data` comment) as `content`. * @returns {Object|null} return.data Object containing `MIME type : value` pairs * or null if `cke-data` comment is not present. * @returns {String} return.content Regular content without `cke-data` comment. */ _extractDataComment: function( content ) { var result = { data: null, content: content || '' }; // At least 17 characters length: . if ( content && content.length > 16 ) { var matcher = //g, matches; matches = matcher.exec( content ); if ( matches && matches[ 1 ] ) { result.data = JSON.parse( decodeURIComponent( matches[ 1 ] ) ); result.content = content.replace( matches[ 0 ], '' ); } } return result; }, /** * Creates `cke-data` comment containing stringified and encoded data object which is prepended to a given content. * * @private * @param {String} content * @param {Object} data * @returns {String} */ _applyDataComment: function( content, data ) { var customData = ''; if ( data && CKEDITOR.tools.object.keys( data ).length ) { customData = ''; } return customData + ( content && content.length ? content : '' ); } }; } )(); /** * The default content type that is used when pasted data cannot be clearly recognized as HTML or text. * * For example: `'foo'` may come from a plain text editor or a website. It is not possible to recognize the content * type in this case, so the default type will be used. At the same time it is clear that `'example text'` is * HTML and its origin is a web page, email or another rich text editor. * * **Note:** If content type is text, then styles of the paste context are preserved. * * CKEDITOR.config.clipboard_defaultContentType = 'text'; * * See also the {@link CKEDITOR.editor#paste} event and read more about the integration with clipboard * in the {@glink guide/dev_clipboard Clipboard Deep Dive guide}. * * @since 4.0.0 * @cfg {'html'/'text'} [clipboard_defaultContentType='html'] * @member CKEDITOR.config */ /** * Fired after the user initiated a paste action, but before the data is inserted into the editor. * The listeners to this event are able to process the content before its insertion into the document. * * Read more about the integration with clipboard in the {@glink guide/dev_clipboard Clipboard Deep Dive guide}. * * See also: * * * the {@link CKEDITOR.config#pasteFilter} option, * * the {@link CKEDITOR.editor#drop} event, * * the {@link CKEDITOR.plugins.clipboard.dataTransfer} class. * * @since 3.1.0 * @event paste * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param data * @param {String} data.type The type of data in `data.dataValue`. Usually `'html'` or `'text'`, but for listeners * with a priority smaller than `6` it may also be `'auto'` which means that the content type has not been recognised yet * (this will be done by the content type sniffer that listens with priority `6`). * @param {String} data.dataValue HTML to be pasted. * @param {String} data.method Indicates the data transfer method. It could be drag and drop or copy and paste. * Possible values: `'drop'`, `'paste'`. Introduced in CKEditor 4.5. * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer Facade for the native dataTransfer object * which provides access to various data types and files, and passes some data between linked events * (like drag and drop). Introduced in CKEditor 4.5. * @param {Boolean} [data.dontFilter=false] Whether the {@link CKEDITOR.editor#pasteFilter paste filter} should not * be applied to data. This option has no effect when `data.type` equals `'text'` which means that for instance * {@link CKEDITOR.config#forcePasteAsPlainText} has a higher priority. Introduced in CKEditor 4.5. */ /** * Fired before the {@link #paste} event. Allows to preset data type. * * **Note:** This event is deprecated. Add a `0` priority listener for the * {@link #paste} event instead. * * @deprecated * @event beforePaste * @member CKEDITOR.editor */ /** * Fired after the {@link #paste} event if content was modified. Note that if the paste * event does not insert any data, the `afterPaste` event will not be fired. * * @event afterPaste * @member CKEDITOR.editor */ /** * Internal event to open the Paste dialog window. * * * This event was not available in 4.7.0-4.8.0 versions. * * @private * @event pasteDialog * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param {Function} [data] Callback that will be passed to {@link CKEDITOR.editor#openDialog}. */ /** * Facade for the native `drop` event. Fired when the native `drop` event occurs. * * **Note:** To manipulate dropped data, use the {@link CKEDITOR.editor#paste} event. * Use the `drop` event only to control drag and drop operations (e.g. to prevent the ability to drop some content). * * Read more about integration with drag and drop in the {@glink guide/dev_clipboard Clipboard Deep Dive guide}. * * See also: * * * The {@link CKEDITOR.editor#paste} event, * * The {@link CKEDITOR.editor#dragstart} and {@link CKEDITOR.editor#dragend} events, * * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class. * * @since 4.5.0 * @event drop * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param data * @param {Object} data.$ Native drop event. * @param {CKEDITOR.dom.node} data.target Drop target. * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade. * @param {CKEDITOR.dom.range} data.dragRange Drag range, lets you manipulate the drag range. * Note that dragged HTML is saved as `text/html` data on `dragstart` so if you change the drag range * on drop, dropped HTML will not change. You need to change it manually using * {@link CKEDITOR.plugins.clipboard.dataTransfer#setData dataTransfer.setData}. * @param {CKEDITOR.dom.range} data.dropRange Drop range, lets you manipulate the drop range. */ /** * Facade for the native `dragstart` event. Fired when the native `dragstart` event occurs. * * This event can be canceled in order to block the drag start operation. It can also be fired to mimic the start of the drag and drop * operation. For instance, the `widget` plugin uses this option to integrate its custom block widget drag and drop with * the entire system. * * Read more about integration with drag and drop in the {@glink guide/dev_clipboard Clipboard Deep Dive guide}. * * See also: * * * The {@link CKEDITOR.editor#paste} event, * * The {@link CKEDITOR.editor#drop} and {@link CKEDITOR.editor#dragend} events, * * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class. * * @since 4.5.0 * @event dragstart * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param data * @param {Object} data.$ Native dragstart event. * @param {CKEDITOR.dom.node} data.target Drag target. * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade. */ /** * Facade for the native `dragend` event. Fired when the native `dragend` event occurs. * * Read more about integration with drag and drop in the {@glink guide/dev_clipboard Clipboard Deep Dive guide}. * * See also: * * * The {@link CKEDITOR.editor#paste} event, * * The {@link CKEDITOR.editor#drop} and {@link CKEDITOR.editor#dragend} events, * * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class. * * @since 4.5.0 * @event dragend * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param data * @param {Object} data.$ Native dragend event. * @param {CKEDITOR.dom.node} data.target Drag target. * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade. */ /** * Defines a filter which is applied to external data pasted or dropped into the editor. Possible values are: * * * `'plain-text'` – Content will be pasted as a plain text. * * `'semantic-content'` – Known tags (except `div`, `span`) with all attributes (except * `style` and `class`) will be kept. * * `'h1 h2 p div'` – Custom rules compatible with {@link CKEDITOR.filter}. * * `null` – Content will not be filtered by the paste filter (but it still may be filtered * by {@glink guide/dev_advanced_content_filter Advanced Content Filter}). This value can be used to * disable the paste filter in Chrome and Safari, where this option defaults to `'semantic-content'`. * * Example: * * config.pasteFilter = 'plain-text'; * * Custom setting: * * config.pasteFilter = 'h1 h2 p ul ol li; img[!src, alt]; a[!href]'; * * Based on this configuration option, a proper {@link CKEDITOR.filter} instance will be defined and assigned to the editor * as a {@link CKEDITOR.editor#pasteFilter}. You can tweak the paste filter settings on the fly on this object * as well as delete or replace it. * * var editor = CKEDITOR.replace( 'editor', { * pasteFilter: 'semantic-content' * } ); * * editor.on( 'instanceReady', function() { * // The result of this will be that all semantic content will be preserved * // except tables. * editor.pasteFilter.disallow( 'table' ); * } ); * * Note that the paste filter is applied only to **external** data. There are three data sources: * * * copied and pasted in the same editor (internal), * * copied from one editor and pasted into another (cross-editor), * * coming from all other sources like websites, MS Word, etc. (external). * * If {@link CKEDITOR.config#allowedContent Advanced Content Filter} is not disabled, then * it will also be applied to pasted and dropped data. The paste filter job is to "normalize" * external data which often needs to be handled differently than content produced by the editor. * * This setting defaults to `'semantic-content'` in Chrome, Opera and Safari (all Blink and Webkit based browsers) * due to messy HTML which these browsers keep in the clipboard. In other browsers it defaults to `null`. * * @since 4.5.0 * @cfg {String} [pasteFilter='semantic-content' in Chrome and Safari and `null` in other browsers] * @member CKEDITOR.config */ /** * {@link CKEDITOR.filter Content filter} which is used when external data is pasted or dropped into the editor * or a forced paste as plain text occurs. * * This object might be used on the fly to define rules for pasted external content. * This object is available and used if the {@link CKEDITOR.plugins.clipboard clipboard} plugin is enabled and * {@link CKEDITOR.config#pasteFilter} or {@link CKEDITOR.config#forcePasteAsPlainText} was defined. * * To enable the filter: * * var editor = CKEDITOR.replace( 'editor', { * pasteFilter: 'plain-text' * } ); * * You can also modify the filter on the fly later on: * * editor.pasteFilter = new CKEDITOR.filter( 'p h1 h2; a[!href]' ); * * Note that the paste filter is only applied to **external** data. There are three data sources: * * * copied and pasted in the same editor (internal), * * copied from one editor and pasted into another (cross-editor), * * coming from all other sources like websites, MS Word, etc. (external). * * If {@link CKEDITOR.config#allowedContent Advanced Content Filter} is not disabled, then * it will also be applied to pasted and dropped data. The paste filter job is to "normalize" * external data which often needs to be handled differently than content produced by the editor. * * @since 4.5.0 * @readonly * @property {CKEDITOR.filter} [pasteFilter] * @member CKEDITOR.editor */ /** * Duration of the notification displayed after pasting was blocked by the browser. * * @since 4.7.0 * @cfg {Number} [clipboard_notificationDuration=10000] * @member CKEDITOR.config */ CKEDITOR.config.clipboard_notificationDuration = 10000; /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'panelbutton', { requires: 'button', onLoad: function() { function clickFn( editor ) { var _ = this._; if ( _.state == CKEDITOR.TRISTATE_DISABLED ) return; this.createPanel( editor ); if ( _.on ) { _.panel.hide(); return; } _.panel.showBlock( this._.id, this.document.getById( this._.id ), 4 ); } /** * @class * @extends CKEDITOR.ui.button * @todo class and methods */ CKEDITOR.ui.panelButton = CKEDITOR.tools.createClass( { base: CKEDITOR.ui.button, /** * Creates a panelButton class instance. * * @constructor */ $: function( definition ) { // We don't want the panel definition in this object. var panelDefinition = definition.panel || {}; delete definition.panel; this.base( definition ); this.document = ( panelDefinition.parent && panelDefinition.parent.getDocument() ) || CKEDITOR.document; panelDefinition.block = { attributes: panelDefinition.attributes }; panelDefinition.toolbarRelated = true; this.hasArrow = 'listbox'; this.click = clickFn; this._ = { panelDefinition: panelDefinition }; }, statics: { handler: { create: function( definition ) { return new CKEDITOR.ui.panelButton( definition ); } } }, proto: { createPanel: function( editor ) { var _ = this._; if ( _.panel ) return; var panelDefinition = this._.panelDefinition, panelBlockDefinition = this._.panelDefinition.block, panelParentElement = panelDefinition.parent || CKEDITOR.document.getBody(), panel = this._.panel = new CKEDITOR.ui.floatPanel( editor, panelParentElement, panelDefinition ), block = panel.addBlock( _.id, panelBlockDefinition ), me = this; panel.onShow = function() { if ( me.className ) this.element.addClass( me.className + '_panel' ); me.setState( CKEDITOR.TRISTATE_ON ); _.on = 1; me.editorFocus && editor.focus(); if ( me.onOpen ) me.onOpen(); }; panel.onHide = function( preventOnClose ) { if ( me.className ) this.element.getFirst().removeClass( me.className + '_panel' ); me.setState( me.modes && me.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED ); _.on = 0; if ( !preventOnClose && me.onClose ) me.onClose(); }; panel.onEscape = function() { panel.hide( 1 ); me.document.getById( _.id ).focus(); }; if ( this.onBlock ) this.onBlock( panel, block ); block.onHide = function() { _.on = 0; me.setState( CKEDITOR.TRISTATE_OFF ); }; } } } ); }, beforeInit: function( editor ) { editor.ui.addHandler( CKEDITOR.UI_PANELBUTTON, CKEDITOR.ui.panelButton.handler ); } } ); /** * Button UI element. * * @readonly * @property {String} [='panelbutton'] * @member CKEDITOR */ CKEDITOR.UI_PANELBUTTON = 'panelbutton'; /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-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 ) { 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 }; 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. (https://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 (https://dev.ckeditor.com/ticket/4534). doc.on( 'keydown', function( evt ) { var keystroke = evt.data.getKeystroke(), dir = this.document.getById( this.id ).getAttribute( 'dir' ); // Arrow left and right should use native behaviour inside input element if ( evt.data.getTarget().getName() === 'input' && ( keystroke === 37 || keystroke === 39 ) ) { return; } // Delegate key processing to block. if ( this._.onKeyDown && this._.onKeyDown( keystroke ) === false ) { if ( !( evt.data.getTarget().getName() === 'input' && keystroke === 32 ) ) { // Don't prevent space when is pressed on a input filed. 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; }; 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 && !CKEDITOR.env.edge ) ? '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. (https://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 focusables = this._.getItems(); var item = focusables.getItem( this._.focusIndex = index ); // Safari need focus on the iframe window first(https://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'; }, focusables = this._.getItems(), item, focused; for ( var i = focusables.count() - 1; i >= 0; i-- ) { item = focusables.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.find( 'a,input' ); } }, 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, focusables = this._.getItems(), focusable; while ( ( focusable = focusables.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 ( focusable.getAttribute( '_cke_focus' ) && focusable.$.offsetWidth ) { this._.focusIndex = index; focusable.focus( true ); break; } } // If no focusable was found, cycle and restart from the top. (https://dev.ckeditor.com/ticket/11125) if ( !focusable && !noCycle ) { this._.focusIndex = -1; return this.onKeyDown( keystroke, 1 ); } return false; // Move backward. case 'prev': index = this._.focusIndex; focusables = this._.getItems(); while ( index > 0 && ( focusable = focusables.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 ( focusable.getAttribute( '_cke_focus' ) && focusable.$.offsetWidth ) { this._.focusIndex = index; focusable.focus( true ); break; } // Make sure focusable is null when the loop ends and nothing was // found (https://dev.ckeditor.com/ticket/11125). focusable = null; } // If no focusable was found, cycle and restart from the bottom. (https://dev.ckeditor.com/ticket/11125) if ( !focusable && !noCycle ) { this._.focusIndex = focusables.count(); return this.onKeyDown( keystroke, 1 ); } return false; case 'click': case 'mouseup': index = this._.focusIndex; focusable = index >= 0 && this._.getItems().getItem( index ); if ( focusable ) focusable.$[ keyAction ] ? focusable.$[ keyAction ]() : focusable.$[ '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. */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'floatpanel', { requires: 'panel' } ); ( function() { var panels = {}; function getPanel( editor, doc, parentElement, definition, level ) { // Generates the panel key: docId-eleId-skinName-langDir[-uiColor][-CSSs][-level] var key = CKEDITOR.tools.genKey( doc.getUniqueId(), parentElement.getUniqueId(), editor.lang.dir, editor.uiColor || '', definition.css || '', level || '' ), panel = panels[ key ]; if ( !panel ) { panel = panels[ key ] = new CKEDITOR.ui.panel( doc, definition ); panel.element = parentElement.append( CKEDITOR.dom.element.createFromHtml( panel.render( editor ), doc ) ); panel.element.setStyles( { display: 'none', position: 'absolute' } ); } return panel; } /** * Represents a floating panel UI element. * * It is reused by rich combos, color combos, menus, etc. * and it renders its content using {@link CKEDITOR.ui.panel}. * * @class * @todo */ CKEDITOR.ui.floatPanel = CKEDITOR.tools.createClass( { /** * Creates a floatPanel class instance. * * @constructor * @param {CKEDITOR.editor} editor * @param {CKEDITOR.dom.element} parentElement * @param {Object} definition Definition of the panel that will be floating. * @param {Number} level */ $: function( editor, parentElement, definition, level ) { definition.forceIFrame = 1; // In case of editor with floating toolbar append panels that should float // to the main UI element. if ( definition.toolbarRelated && editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) parentElement = CKEDITOR.document.getById( 'cke_' + editor.name ); var doc = parentElement.getDocument(), panel = getPanel( editor, doc, parentElement, definition, level || 0 ), element = panel.element, iframe = element.getFirst(), that = this; // Disable native browser menu. (https://dev.ckeditor.com/ticket/4825) element.disableContextMenu(); this.element = element; this._ = { editor: editor, // The panel that will be floating. panel: panel, parentElement: parentElement, definition: definition, document: doc, iframe: iframe, children: [], dir: editor.lang.dir, showBlockParams: null, markFirst: definition.markFirst !== undefined ? definition.markFirst : true }; editor.on( 'mode', hide ); editor.on( 'resize', hide ); // When resize of the window is triggered floatpanel should be repositioned according to new dimensions. // https://dev.ckeditor.com/ticket/11724. Fixes issue with undesired panel hiding on Android and iOS. doc.getWindow().on( 'resize', function() { this.reposition(); }, this ); // We need a wrapper because events implementation doesn't allow to attach // one listener more than once for the same event on the same object. // Remember that floatPanel#hide is shared between all instances. function hide() { that.hide(); } }, proto: { /** * @todo */ addBlock: function( name, block ) { return this._.panel.addBlock( name, block ); }, /** * @todo */ addListBlock: function( name, multiSelect ) { return this._.panel.addListBlock( name, multiSelect ); }, /** * @todo */ getBlock: function( name ) { return this._.panel.getBlock( name ); }, /** * Shows the panel block. * * @param {String} name * @param {CKEDITOR.dom.element} offsetParent Positioned parent. * @param {Number} corner * * * For LTR (left to right) oriented editor: * * `1` = top-left * * `2` = top-right * * `3` = bottom-right * * `4` = bottom-left * * For RTL (right to left): * * `1` = top-right * * `2` = top-left * * `3` = bottom-left * * `4` = bottom-right * * @param {Number} [offsetX=0] * @param {Number} [offsetY=0] * @param {Function} [callback] A callback function executed when block positioning is done. * @todo what do exactly these params mean (especially corner)? */ showBlock: function( name, offsetParent, corner, offsetX, offsetY, callback ) { var panel = this._.panel, block = panel.showBlock( name ); this._.showBlockParams = [].slice.call( arguments ); this.allowBlur( false ); // Record from where the focus is when open panel. var editable = this._.editor.editable(); this._.returnFocus = editable.hasFocus ? editable : new CKEDITOR.dom.element( CKEDITOR.document.$.activeElement ); this._.hideTimeout = 0; var element = this.element, iframe = this._.iframe, // Edge prefers iframe's window to the iframe, just like the rest of the browsers (https://dev.ckeditor.com/ticket/13143). focused = CKEDITOR.env.ie && !CKEDITOR.env.edge ? iframe : new CKEDITOR.dom.window( iframe.$.contentWindow ), doc = element.getDocument(), positionedAncestor = this._.parentElement.getPositionedAncestor(), position = offsetParent.getDocumentPosition( doc ), positionedAncestorPosition = positionedAncestor ? positionedAncestor.getDocumentPosition( doc ) : { x: 0, y: 0 }, rtl = this._.dir == 'rtl', left = position.x + ( offsetX || 0 ) - positionedAncestorPosition.x, top = position.y + ( offsetY || 0 ) - positionedAncestorPosition.y; // Floating panels are off by (-1px, 0px) in RTL mode. (https://dev.ckeditor.com/ticket/3438) if ( rtl && ( corner == 1 || corner == 4 ) ) left += offsetParent.$.offsetWidth; else if ( !rtl && ( corner == 2 || corner == 3 ) ) left += offsetParent.$.offsetWidth - 1; if ( corner == 3 || corner == 4 ) top += offsetParent.$.offsetHeight - 1; // Memorize offsetParent by it's ID. this._.panel._.offsetParentId = offsetParent.getId(); element.setStyles( { top: top + 'px', left: 0, display: '' } ); // Don't use display or visibility style because we need to // calculate the rendering layout later and focus the element. element.setOpacity( 0 ); // To allow the context menu to decrease back their width element.getFirst().removeStyle( 'width' ); // Report to focus manager. this._.editor.focusManager.add( focused ); // Configure the IFrame blur event. Do that only once. if ( !this._.blurSet ) { // With addEventListener compatible browsers, we must // useCapture when registering the focus/blur events to // guarantee they will be firing in all situations. (https://dev.ckeditor.com/ticket/3068, https://dev.ckeditor.com/ticket/3222 ) CKEDITOR.event.useCapture = true; focused.on( 'blur', function( ev ) { // As we are using capture to register the listener, // the blur event may get fired even when focusing // inside the window itself, so we must ensure the // target is out of it. if ( !this.allowBlur() || ev.data.getPhase() != CKEDITOR.EVENT_PHASE_AT_TARGET ) return; if ( this.visible && !this._.activeChild ) { // [iOS] Allow hide to be prevented if touch is bound // to any parent of the iframe blur happens before touch (https://dev.ckeditor.com/ticket/10714). if ( CKEDITOR.env.iOS ) { if ( !this._.hideTimeout ) this._.hideTimeout = CKEDITOR.tools.setTimeout( doHide, 0, this ); } else { doHide.call( this ); } } function doHide() { // Panel close is caused by user's navigating away the focus, e.g. click outside the panel. // DO NOT restore focus in this case. delete this._.returnFocus; this.hide(); } }, this ); focused.on( 'focus', function() { this._.focused = true; this.hideChild(); this.allowBlur( true ); }, this ); // [iOS] if touch is bound to any parent of the iframe blur // happens twice before touchstart and before touchend (https://dev.ckeditor.com/ticket/10714). if ( CKEDITOR.env.iOS ) { // Prevent false hiding on blur. // We don't need to return focus here because touchend will fire anyway. // If user scrolls and pointer gets out of the panel area touchend will also fire. focused.on( 'touchstart', function() { clearTimeout( this._.hideTimeout ); }, this ); // Set focus back to handle blur and hide panel when needed. focused.on( 'touchend', function() { this._.hideTimeout = 0; this.focus(); }, this ); } CKEDITOR.event.useCapture = false; this._.blurSet = 1; } panel.onEscape = CKEDITOR.tools.bind( function( keystroke ) { if ( this.onEscape && this.onEscape( keystroke ) === false ) return false; }, this ); CKEDITOR.tools.setTimeout( function() { var panelLoad = CKEDITOR.tools.bind( function() { var target = element; // Reset panel width as the new content can be narrower // than the old one. (https://dev.ckeditor.com/ticket/9355) target.removeStyle( 'width' ); if ( block.autoSize ) { var panelDoc = block.element.getDocument(), width = ( ( CKEDITOR.env.webkit || CKEDITOR.env.edge ) ? block.element : panelDoc.getBody() ).$.scrollWidth; // Account for extra height needed due to IE quirks box model bug: // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug // (https://dev.ckeditor.com/ticket/3426) if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && width > 0 ) width += ( target.$.offsetWidth || 0 ) - ( target.$.clientWidth || 0 ) + 3; // Add some extra pixels to improve the appearance. width += 10; target.setStyle( 'width', width + 'px' ); var height = block.element.$.scrollHeight; // Account for extra height needed due to IE quirks box model bug: // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug // (https://dev.ckeditor.com/ticket/3426) if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && height > 0 ) height += ( target.$.offsetHeight || 0 ) - ( target.$.clientHeight || 0 ) + 3; target.setStyle( 'height', height + 'px' ); // Fix IE < 8 visibility. panel._.currentBlock.element.setStyle( 'display', 'none' ).removeStyle( 'display' ); } else { target.removeStyle( 'height' ); } // Flip panel layout horizontally in RTL with known width. if ( rtl ) left -= element.$.offsetWidth; // Pop the style now for measurement. element.setStyle( 'left', left + 'px' ); /* panel layout smartly fit the viewport size. */ var panelElement = panel.element, panelWindow = panelElement.getWindow(), rect = element.$.getBoundingClientRect(), viewportSize = panelWindow.getViewPaneSize(); // Compensation for browsers that dont support "width" and "height". var rectWidth = rect.width || rect.right - rect.left, rectHeight = rect.height || rect.bottom - rect.top; // Check if default horizontal layout is impossible. var spaceAfter = rtl ? rect.right : viewportSize.width - rect.left, spaceBefore = rtl ? viewportSize.width - rect.right : rect.left; if ( rtl ) { if ( spaceAfter < rectWidth ) { // Flip to show on right. if ( spaceBefore > rectWidth ) left += rectWidth; // Align to window left. else if ( viewportSize.width > rectWidth ) left = left - rect.left; // Align to window right, never cutting the panel at right. else left = left - rect.right + viewportSize.width; } } else if ( spaceAfter < rectWidth ) { // Flip to show on left. if ( spaceBefore > rectWidth ) left -= rectWidth; // Align to window right. else if ( viewportSize.width > rectWidth ) left = left - rect.right + viewportSize.width; // Align to window left, never cutting the panel at left. else left = left - rect.left; } // Check if the default vertical layout is possible. var spaceBelow = viewportSize.height - rect.top, spaceAbove = rect.top; if ( spaceBelow < rectHeight ) { // Flip to show above. if ( spaceAbove > rectHeight ) top -= rectHeight; // Align to window bottom. else if ( viewportSize.height > rectHeight ) top = top - rect.bottom + viewportSize.height; // Align to top, never cutting the panel at top. else top = top - rect.top; } // If IE is in RTL, we have troubles with absolute // position and horizontal scrolls. Here we have a // series of hacks to workaround it. (https://dev.ckeditor.com/ticket/6146) if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) { var offsetParent = new CKEDITOR.dom.element( element.$.offsetParent ), scrollParent = offsetParent; // Quirks returns , but standards returns . if ( scrollParent.getName() == 'html' ) { scrollParent = scrollParent.getDocument().getBody(); } if ( scrollParent.getComputedStyle( 'direction' ) == 'rtl' ) { // For IE8, there is not much logic on this, but it works. if ( CKEDITOR.env.ie8Compat ) { left -= element.getDocument().getDocumentElement().$.scrollLeft * 2; } else { left -= ( offsetParent.$.scrollWidth - offsetParent.$.clientWidth ); } } } // Trigger the onHide event of the previously active panel to prevent // incorrect styles from being applied (https://dev.ckeditor.com/ticket/6170) var innerElement = element.getFirst(), activePanel; if ( ( activePanel = innerElement.getCustomData( 'activePanel' ) ) ) activePanel.onHide && activePanel.onHide.call( this, 1 ); innerElement.setCustomData( 'activePanel', this ); element.setStyles( { top: top + 'px', left: left + 'px' } ); element.setOpacity( 1 ); callback && callback(); }, this ); panel.isLoaded ? panelLoad() : panel.onLoad = panelLoad; CKEDITOR.tools.setTimeout( function() { var scrollTop = CKEDITOR.env.webkit && CKEDITOR.document.getWindow().getScrollPosition().y; // Focus the panel frame first, so blur gets fired. this.focus(); // Focus the block now. block.element.focus(); // https://dev.ckeditor.com/ticket/10623, https://dev.ckeditor.com/ticket/10951 - restore the viewport's scroll position after focusing list element. if ( CKEDITOR.env.webkit ) CKEDITOR.document.getBody().$.scrollTop = scrollTop; // We need this get fired manually because of unfired focus() function. this.allowBlur( true ); // Ensure that the first item is focused (https://dev.ckeditor.com/ticket/16804). if ( this._.markFirst ) { if ( CKEDITOR.env.ie ) { CKEDITOR.tools.setTimeout( function() { block.markFirstDisplayed ? block.markFirstDisplayed() : block._.markFirstDisplayed(); }, 0 ); } else { block.markFirstDisplayed ? block.markFirstDisplayed() : block._.markFirstDisplayed(); } } this._.editor.fire( 'panelShow', this ); }, 0, this ); }, CKEDITOR.env.air ? 200 : 0, this ); this.visible = 1; if ( this.onShow ) this.onShow.call( this ); }, /** * Repositions the panel with the same parameters that were used in the last {@link #showBlock} call. * * @since 4.5.4 */ reposition: function() { var blockParams = this._.showBlockParams; if ( this.visible && this._.showBlockParams ) { this.hide(); this.showBlock.apply( this, blockParams ); } }, /** * Restores the last focused element or simply focuses the panel window. */ focus: function() { // Webkit requires to blur any previous focused page element, in // order to properly fire the "focus" event. if ( CKEDITOR.env.webkit ) { var active = CKEDITOR.document.getActive(); active && !active.equals( this._.iframe ) && active.$.blur(); } // Restore last focused element or simply focus panel window. var focus = this._.lastFocused || this._.iframe.getFrameDocument().getWindow(); focus.focus(); }, /** * @todo */ blur: function() { var doc = this._.iframe.getFrameDocument(), active = doc.getActive(); active && active.is( 'a' ) && ( this._.lastFocused = active ); }, /** * Hides the panel. * * @todo */ hide: function( returnFocus ) { if ( this.visible && ( !this.onHide || this.onHide.call( this ) !== true ) ) { this.hideChild(); // Blur previously focused element. (https://dev.ckeditor.com/ticket/6671) CKEDITOR.env.gecko && this._.iframe.getFrameDocument().$.activeElement.blur(); this.element.setStyle( 'display', 'none' ); this.visible = 0; this.element.getFirst().removeCustomData( 'activePanel' ); // Return focus properly. (https://dev.ckeditor.com/ticket/6247) var focusReturn = returnFocus && this._.returnFocus; if ( focusReturn ) { // Webkit requires focus moved out panel iframe first. if ( CKEDITOR.env.webkit && focusReturn.type ) focusReturn.getWindow().$.focus(); focusReturn.focus(); } delete this._.lastFocused; this._.showBlockParams = null; this._.editor.fire( 'panelHide', this ); } }, /** * @todo */ allowBlur: function( allow ) { // Prevent editor from hiding the panel. (https://dev.ckeditor.com/ticket/3222) var panel = this._.panel; if ( allow !== undefined ) panel.allowBlur = allow; return panel.allowBlur; }, /** * Shows the specified panel as a child of one block of this one. * * @param {CKEDITOR.ui.floatPanel} panel * @param {String} blockName * @param {CKEDITOR.dom.element} offsetParent Positioned parent. * @param {Number} corner * * * For LTR (left to right) oriented editor: * * `1` = top-left * * `2` = top-right * * `3` = bottom-right * * `4` = bottom-left * * For RTL (right to left): * * `1` = top-right * * `2` = top-left * * `3` = bottom-left * * `4` = bottom-right * * @param {Number} [offsetX=0] * @param {Number} [offsetY=0] * @todo */ showAsChild: function( panel, blockName, offsetParent, corner, offsetX, offsetY ) { // Skip reshowing of child which is already visible. if ( this._.activeChild == panel && panel._.panel._.offsetParentId == offsetParent.getId() ) return; this.hideChild(); panel.onHide = CKEDITOR.tools.bind( function() { // Use a timeout, so we give time for this menu to get // potentially focused. CKEDITOR.tools.setTimeout( function() { if ( !this._.focused ) this.hide(); }, 0, this ); }, this ); this._.activeChild = panel; this._.focused = false; panel.showBlock( blockName, offsetParent, corner, offsetX, offsetY ); this.blur(); /* https://dev.ckeditor.com/ticket/3767 IE: Second level menu may not have borders */ if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) { setTimeout( function() { panel.element.getChild( 0 ).$.style.cssText += ''; }, 100 ); } }, /** * @todo */ hideChild: function( restoreFocus ) { var activeChild = this._.activeChild; if ( activeChild ) { delete activeChild.onHide; delete this._.activeChild; activeChild.hide(); // At this point focus should be moved back to parent panel. restoreFocus && this.focus(); } } } } ); CKEDITOR.on( 'instanceDestroyed', function() { var isLastInstance = CKEDITOR.tools.isEmpty( CKEDITOR.instances ); for ( var i in panels ) { var panel = panels[ i ]; // Safe to destroy it since there're no more instances.(https://dev.ckeditor.com/ticket/4241) if ( isLastInstance ) panel.destroy(); // Panel might be used by other instances, just hide them.(https://dev.ckeditor.com/ticket/4552) else panel.element.hide(); } // Remove the registration. isLastInstance && ( panels = {} ); } ); } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview The "colorbutton" plugin that makes it possible to assign * text and background colors to editor contents. * */ CKEDITOR.plugins.add( 'colorbutton', { requires: 'panelbutton,floatpanel', // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { var config = editor.config, lang = editor.lang.colorbutton; if ( !CKEDITOR.env.hc ) { addButton( 'TextColor', 'fore', lang.textColorTitle, 10, { contentTransformations: [ [ { element: 'font', check: 'span{color}', left: function( element ) { return !!element.attributes.color; }, right: function( element ) { element.name = 'span'; element.attributes.color && ( element.styles.color = element.attributes.color ); delete element.attributes.color; } } ] ] } ); var bgOptions = {}, normalizeBackground = editor.config.colorButton_normalizeBackground; if ( normalizeBackground === undefined || normalizeBackground ) { // If background contains only color, then we want to convert it into background-color so that it's // correctly picked by colorbutton plugin. bgOptions.contentTransformations = [ [ { // Transform span that specify background with color only to background-color. element: 'span', left: function( element ) { var tools = CKEDITOR.tools; if ( element.name != 'span' || !element.styles || !element.styles.background ) { return false; } var background = tools.style.parse.background( element.styles.background ); // We return true only if background specifies **only** color property, and there's only one background directive. return background.color && tools.object.keys( background ).length === 1; }, right: function( element ) { var style = new CKEDITOR.style( editor.config.colorButton_backStyle, { color: element.styles.background } ), definition = style.getDefinition(); // Align the output object with the template used in config. element.name = definition.element; element.styles = definition.styles; element.attributes = definition.attributes || {}; return element; } } ] ]; } addButton( 'BGColor', 'back', lang.bgColorTitle, 20, bgOptions ); } function addButton( name, type, title, order, options ) { var style = new CKEDITOR.style( config[ 'colorButton_' + type + 'Style' ] ), colorBoxId = CKEDITOR.tools.getNextId() + '_colorBox', colorData = { type: type }, panelBlock; options = options || {}; editor.ui.add( name, CKEDITOR.UI_PANELBUTTON, { label: title, title: title, modes: { wysiwyg: 1 }, editorFocus: 0, toolbar: 'colors,' + order, allowedContent: style, requiredContent: style, contentTransformations: options.contentTransformations, panel: { css: CKEDITOR.skin.getPath( 'editor' ), attributes: { role: 'listbox', 'aria-label': lang.panelTitle } }, onBlock: function( panel, block ) { panelBlock = block; block.autoSize = true; block.element.addClass( 'cke_colorblock' ); block.element.setHtml( renderColors( panel, type, colorBoxId, colorData ) ); // The block should not have scrollbars (https://dev.ckeditor.com/ticket/5933, https://dev.ckeditor.com/ticket/6056) block.element.getDocument().getBody().setStyle( 'overflow', 'hidden' ); CKEDITOR.ui.fire( 'ready', this ); var keys = block.keys; var rtl = editor.lang.dir == 'rtl'; keys[ rtl ? 37 : 39 ] = 'next'; // ARROW-RIGHT keys[ 40 ] = 'next'; // ARROW-DOWN keys[ 9 ] = 'next'; // TAB keys[ rtl ? 39 : 37 ] = 'prev'; // ARROW-LEFT keys[ 38 ] = 'prev'; // ARROW-UP keys[ CKEDITOR.SHIFT + 9 ] = 'prev'; // SHIFT + TAB keys[ 32 ] = 'click'; // SPACE }, refresh: function() { if ( !editor.activeFilter.check( style ) ) { this.setState( CKEDITOR.TRISTATE_DISABLED ); } }, // The automatic colorbox should represent the real color (https://dev.ckeditor.com/ticket/6010) onOpen: function() { var selection = editor.getSelection(), block = selection && selection.getStartElement(), path = editor.elementPath( block ), automaticColor; if ( !path ) { return; } // Find the closest block element. block = path.block || path.blockLimit || editor.document.getBody(); // The background color might be transparent. In that case, look up the color in the DOM tree. do { automaticColor = block && block.getComputedStyle( type == 'back' ? 'background-color' : 'color' ) || 'transparent'; } while ( type == 'back' && automaticColor == 'transparent' && block && ( block = block.getParent() ) ); // The box should never be transparent. if ( !automaticColor || automaticColor == 'transparent' ) { automaticColor = '#ffffff'; } if ( config.colorButton_enableAutomatic !== false ) { this._.panel._.iframe.getFrameDocument().getById( colorBoxId ).setStyle( 'background-color', automaticColor ); } var range = selection && selection.getRanges()[ 0 ]; if ( range ) { var walker = new CKEDITOR.dom.walker( range ), element = range.collapsed ? range.startContainer : walker.next(), finalColor = '', currentColor; while ( element ) { // (#2296) if ( element.type !== CKEDITOR.NODE_ELEMENT ) { element = element.getParent(); } currentColor = normalizeColor( element.getComputedStyle( type == 'back' ? 'background-color' : 'color' ) ); finalColor = finalColor || currentColor; if ( finalColor !== currentColor ) { finalColor = ''; break; } element = walker.next(); } if ( finalColor == 'transparent' ) { finalColor = ''; } if ( type == 'fore' ) { colorData.automaticTextColor = '#' + normalizeColor( automaticColor ); } colorData.selectionColor = finalColor ? '#' + finalColor : ''; selectColor( panelBlock, finalColor ); } return automaticColor; } } ); } function renderColors( panel, type, colorBoxId, colorData ) { var output = [], colors = config.colorButton_colors.split( ',' ), colorsPerRow = config.colorButton_colorsPerRow || 6, // Tells if we should include "More Colors..." button. moreColorsEnabled = editor.plugins.colordialog && config.colorButton_enableMore !== false, // aria-setsize and aria-posinset attributes are used to indicate size of options, because // screen readers doesn't play nice with table, based layouts (https://dev.ckeditor.com/ticket/12097). total = colors.length + ( moreColorsEnabled ? 2 : 1 ); var clickFn = CKEDITOR.tools.addFunction( function applyColorStyle( color, type ) { editor.focus(); editor.fire( 'saveSnapshot' ); if ( color == '?' ) { editor.getColorFromDialog( function( color ) { if ( color ) { return setColor( color ); } }, null, colorData ); } else { return setColor( color && '#' + color ); } function setColor( color ) { var colorStyle = config[ 'colorButton_' + type + 'Style' ]; // Clean up any conflicting style within the range. editor.removeStyle( new CKEDITOR.style( colorStyle, { color: 'inherit' } ) ); colorStyle.childRule = type == 'back' ? function( element ) { // It's better to apply background color as the innermost style. (https://dev.ckeditor.com/ticket/3599) // Except for "unstylable elements". (https://dev.ckeditor.com/ticket/6103) return isUnstylable( element ); } : function( element ) { // Fore color style must be applied inside links instead of around it. (https://dev.ckeditor.com/ticket/4772,https://dev.ckeditor.com/ticket/6908) return !( element.is( 'a' ) || element.getElementsByTag( 'a' ).count() ) || isUnstylable( element ); }; editor.focus(); if ( color ) { editor.applyStyle( new CKEDITOR.style( colorStyle, { color: color } ) ); } editor.fire( 'saveSnapshot' ); } } ); if ( config.colorButton_enableAutomatic !== false ) { // Render the "Automatic" button. output.push( '' + '' + '' + '' + '' + '
', lang.auto, '
' + '
' ); } output.push( '' ); // Render the color boxes. for ( var i = 0; i < colors.length; i++ ) { if ( ( i % colorsPerRow ) === 0 ) output.push( '' ); var parts = colors[ i ].split( '/' ), colorName = parts[ 0 ], colorCode = parts[ 1 ] || colorName, colorLabel; // The data can be only a color code (without #) or colorName + color code // If only a color code is provided, then the colorName is the color with the hash // Convert the color from RGB to RRGGBB for better compatibility with IE and . See https://dev.ckeditor.com/ticket/5676 // Additionally, if the data is a single color code then let's try to translate it or fallback on the // color code. If the data is a color name/code, then use directly the color name provided. if ( !parts[ 1 ] ) { colorLabel = editor.lang.colorbutton.colors[ colorCode ] || colorCode; } else { colorLabel = colorName; } output.push( '' ); } // Render the "More Colors" button. if ( moreColorsEnabled ) { output.push( '' + '' + '' ); // tr is later in the code. } output.push( '
' + '' + '' + '' + '
' + '', lang.more, '' + '
' ); return output.join( '' ); } function isUnstylable( ele ) { return ( ele.getAttribute( 'contentEditable' ) == 'false' ) || ele.getAttribute( 'data-nostyle' ); } /* * Selects the specified color in the specified panel block. * * @private * @member CKEDITOR.plugins.colorbutton * @param {CKEDITOR.ui.panel.block} block * @param {String} color */ function selectColor( block, color ) { var items = block._.getItems(); for ( var i = 0; i < items.count(); i++ ) { var item = items.getItem( i ); item.removeAttribute( 'aria-selected' ); if ( color && color == normalizeColor( item.getAttribute( 'data-value' ) ) ) { item.setAttribute( 'aria-selected', true ); } } } /* * Converts a CSS color value to an easily comparable form. * * @private * @member CKEDITOR.plugins.colorbutton * @param {String} color * @returns {String} */ function normalizeColor( color ) { // Replace 3-character hexadecimal notation with a 6-character hexadecimal notation (#1008). return CKEDITOR.tools.normalizeHex( '#' + CKEDITOR.tools.convertRgbToHex( color || '' ) ).replace( /#/g, '' ); } } } ); /** * Whether to enable the **More Colors** button in the color selectors. * * Read more in the {@glink features/colorbutton documentation} * and see the {@glink examples/colorbutton example}. * * config.colorButton_enableMore = false; * * @cfg {Boolean} [colorButton_enableMore=true] * @member CKEDITOR.config */ /** * Defines the colors to be displayed in the color selectors. This is a string * containing hexadecimal notation for HTML colors, without the `'#'` prefix. * * **Since 3.3:** A color name may optionally be defined by prefixing the entries with * a name and the slash character. For example, `'FontColor1/FF9900'` will be * displayed as the color `#FF9900` in the selector, but will be output as `'FontColor1'`. * **This behaviour was altered in version 4.12.0.** * * **Since 4.6.2:** The default color palette has changed. It contains fewer colors in more * pastel shades than the previous one. * * **Since 4.12.0:** Defining colors with names works in a different way. Colors names can be defined * by `colorName/colorCode`. The color name is only used in the tooltip. The output will now use the color code. * For example, `FontColor/FF9900` will be displayed as the color `#FF9900` in the selector, and will * be output as `#FF9900`. * * Read more in the {@glink features/colorbutton documentation} * and see the {@glink examples/colorbutton example}. * * // Brazil colors only. * config.colorButton_colors = '00923E,F8C100,28166F'; * * config.colorButton_colors = 'FontColor1/FF9900,FontColor2/0066CC,FontColor3/F00'; * * // CKEditor color palette available before version 4.6.2. * config.colorButton_colors = * '000,800000,8B4513,2F4F4F,008080,000080,4B0082,696969,' + * 'B22222,A52A2A,DAA520,006400,40E0D0,0000CD,800080,808080,' + * 'F00,FF8C00,FFD700,008000,0FF,00F,EE82EE,A9A9A9,' + * 'FFA07A,FFA500,FFFF00,00FF00,AFEEEE,ADD8E6,DDA0DD,D3D3D3,' + * 'FFF0F5,FAEBD7,FFFFE0,F0FFF0,F0FFFF,F0F8FF,E6E6FA,FFF'; * * @cfg {String} [colorButton_colors=see source] * @member CKEDITOR.config */ CKEDITOR.config.colorButton_colors = '1ABC9C,2ECC71,3498DB,9B59B6,4E5F70,F1C40F,' + '16A085,27AE60,2980B9,8E44AD,2C3E50,F39C12,' + 'E67E22,E74C3C,ECF0F1,95A5A6,DDD,FFF,' + 'D35400,C0392B,BDC3C7,7F8C8D,999,000'; /** * Stores the style definition that applies the text foreground color. * * Read more in the {@glink features/colorbutton documentation} * and see the {@glink examples/colorbutton example}. * * // This is actually the default value. * config.colorButton_foreStyle = { * element: 'span', * styles: { color: '#(color)' } * }; * * @cfg [colorButton_foreStyle=see source] * @member CKEDITOR.config */ CKEDITOR.config.colorButton_foreStyle = { element: 'span', styles: { 'color': '#(color)' }, overrides: [ { element: 'font', attributes: { 'color': null } } ] }; /** * Stores the style definition that applies the text background color. * * Read more in the {@glink features/colorbutton documentation} * and see the {@glink examples/colorbutton example}. * * // This is actually the default value. * config.colorButton_backStyle = { * element: 'span', * styles: { 'background-color': '#(color)' } * }; * * @cfg [colorButton_backStyle=see source] * @member CKEDITOR.config */ CKEDITOR.config.colorButton_backStyle = { element: 'span', styles: { 'background-color': '#(color)' } }; /** * Whether to enable the **Automatic** button in the color selectors. * * Read more in the {@glink features/colorbutton documentation} * and see the {@glink examples/colorbutton example}. * * config.colorButton_enableAutomatic = false; * * @cfg {Boolean} [colorButton_enableAutomatic=true] * @member CKEDITOR.config */ /** * Defines how many colors will be shown per row in the color selectors. * * Read more in the {@glink features/colorbutton documentation} * and see the {@glink examples/colorbutton example}. * * config.colorButton_colorsPerRow = 8; * * @since 4.6.2 * @cfg {Number} [colorButton_colorsPerRow=6] * @member CKEDITOR.config */ /** * Whether the plugin should convert `background` CSS properties with color only, to a `background-color` property, * allowing the [Color Button](https://ckeditor.com/cke4/addon/colorbutton) plugin to edit these styles. * * config.colorButton_normalizeBackground = false; * * @since 4.6.1 * @cfg {Boolean} [colorButton_normalizeBackground=true] * @member CKEDITOR.config */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ ( function() { function setupAdvParams( element ) { var attrName = this.att; var value = element && element.hasAttribute( attrName ) && element.getAttribute( attrName ) || ''; if ( value !== undefined ) this.setValue( value ); } function commitAdvParams() { // Dialogs may use different parameters in the commit list, so, by // definition, we take the first CKEDITOR.dom.element available. var element; for ( var i = 0; i < arguments.length; i++ ) { if ( arguments[ i ] instanceof CKEDITOR.dom.element ) { element = arguments[ i ]; break; } } if ( element ) { var attrName = this.att, value = this.getValue(); if ( value ) element.setAttribute( attrName, value ); else element.removeAttribute( attrName, value ); } } var defaultTabConfig = { id: 1, dir: 1, classes: 1, styles: 1 }; CKEDITOR.plugins.add( 'dialogadvtab', { requires: 'dialog', // Returns allowed content rule for the content created by this plugin. allowedContent: function( tabConfig ) { if ( !tabConfig ) tabConfig = defaultTabConfig; var allowedAttrs = []; if ( tabConfig.id ) allowedAttrs.push( 'id' ); if ( tabConfig.dir ) allowedAttrs.push( 'dir' ); var allowed = ''; if ( allowedAttrs.length ) allowed += '[' + allowedAttrs.join( ',' ) + ']'; if ( tabConfig.classes ) allowed += '(*)'; if ( tabConfig.styles ) allowed += '{*}'; return allowed; }, // @param tabConfig // id, dir, classes, styles createAdvancedTab: function( editor, tabConfig, element ) { if ( !tabConfig ) tabConfig = defaultTabConfig; var lang = editor.lang.common; var result = { id: 'advanced', label: lang.advancedTab, title: lang.advancedTab, elements: [ { type: 'vbox', padding: 1, children: [] } ] }; var contents = []; if ( tabConfig.id || tabConfig.dir ) { if ( tabConfig.id ) { contents.push( { id: 'advId', att: 'id', type: 'text', requiredContent: element ? element + '[id]' : null, label: lang.id, setup: setupAdvParams, commit: commitAdvParams } ); } if ( tabConfig.dir ) { contents.push( { id: 'advLangDir', att: 'dir', type: 'select', requiredContent: element ? element + '[dir]' : null, label: lang.langDir, 'default': '', style: 'width:100%', items: [ [ lang.notSet, '' ], [ lang.langDirLTR, 'ltr' ], [ lang.langDirRTL, 'rtl' ] ], setup: setupAdvParams, commit: commitAdvParams } ); } result.elements[ 0 ].children.push( { type: 'hbox', widths: [ '50%', '50%' ], children: [].concat( contents ) } ); } if ( tabConfig.styles || tabConfig.classes ) { contents = []; if ( tabConfig.styles ) { contents.push( { id: 'advStyles', att: 'style', type: 'text', requiredContent: element ? element + '{cke-xyz}' : null, label: lang.styles, 'default': '', validate: CKEDITOR.dialog.validate.inlineStyle( lang.invalidInlineStyle ), onChange: function() {}, getStyle: function( name, defaultValue ) { var match = this.getValue().match( new RegExp( '(?:^|;)\\s*' + name + '\\s*:\\s*([^;]*)', 'i' ) ); return match ? match[ 1 ] : defaultValue; }, updateStyle: function( name, value ) { var styles = this.getValue(); var tmp = editor.document.createElement( 'span' ); tmp.setAttribute( 'style', styles ); tmp.setStyle( name, value ); styles = CKEDITOR.tools.normalizeCssText( tmp.getAttribute( 'style' ) ); this.setValue( styles, 1 ); }, setup: setupAdvParams, commit: commitAdvParams } ); } if ( tabConfig.classes ) { contents.push( { type: 'hbox', widths: [ '45%', '55%' ], children: [ { id: 'advCSSClasses', att: 'class', type: 'text', requiredContent: element ? element + '(cke-xyz)' : null, label: lang.cssClasses, 'default': '', setup: setupAdvParams, commit: commitAdvParams } ] } ); } result.elements[ 0 ].children.push( { type: 'hbox', widths: [ '50%', '50%' ], children: [].concat( contents ) } ); } return result; } } ); } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview The "div" plugin. It wraps the selected block level elements with a 'div' element with specified styles and attributes. * */ ( function() { CKEDITOR.plugins.add( 'div', { requires: 'dialog', // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { if ( editor.blockless ) return; var lang = editor.lang.div, allowed = 'div(*)'; if ( CKEDITOR.dialog.isTabEnabled( editor, 'editdiv', 'advanced' ) ) allowed += ';div[dir,id,lang,title]{*}'; editor.addCommand( 'creatediv', new CKEDITOR.dialogCommand( 'creatediv', { allowedContent: allowed, requiredContent: 'div', contextSensitive: true, contentTransformations: [ [ 'div: alignmentToStyle' ] ], refresh: function( editor, path ) { var context = editor.config.div_wrapTable ? path.root : path.blockLimit; this.setState( 'div' in context.getDtd() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED ); } } ) ); editor.addCommand( 'editdiv', new CKEDITOR.dialogCommand( 'editdiv', { requiredContent: 'div' } ) ); editor.addCommand( 'removediv', { requiredContent: 'div', exec: function( editor ) { var selection = editor.getSelection(), ranges = selection && selection.getRanges(), range, bookmarks = selection.createBookmarks(), walker, toRemove = []; function findDiv( node ) { var div = CKEDITOR.plugins.div.getSurroundDiv( editor, node ); if ( div && !div.data( 'cke-div-added' ) ) { toRemove.push( div ); div.data( 'cke-div-added' ); } } for ( var i = 0; i < ranges.length; i++ ) { range = ranges[ i ]; if ( range.collapsed ) findDiv( selection.getStartElement() ); else { walker = new CKEDITOR.dom.walker( range ); walker.evaluator = findDiv; walker.lastForward(); } } for ( i = 0; i < toRemove.length; i++ ) toRemove[ i ].remove( true ); selection.selectBookmarks( bookmarks ); } } ); editor.ui.addButton && editor.ui.addButton( 'CreateDiv', { label: lang.toolbar, command: 'creatediv', toolbar: 'blocks,50' } ); if ( editor.addMenuItems ) { editor.addMenuItems( { editdiv: { label: lang.edit, command: 'editdiv', group: 'div', order: 1 }, removediv: { label: lang.remove, command: 'removediv', group: 'div', order: 5 } } ); if ( editor.contextMenu ) { editor.contextMenu.addListener( function( element ) { if ( !element || element.isReadOnly() ) return null; if ( CKEDITOR.plugins.div.getSurroundDiv( editor ) ) { return { editdiv: CKEDITOR.TRISTATE_OFF, removediv: CKEDITOR.TRISTATE_OFF }; } return null; } ); } } CKEDITOR.dialog.add( 'creatediv', this.path + 'dialogs/div.js' ); CKEDITOR.dialog.add( 'editdiv', this.path + 'dialogs/div.js' ); } } ); CKEDITOR.plugins.div = { getSurroundDiv: function( editor, start ) { var path = editor.elementPath( start ); return editor.elementPath( path.blockLimit ).contains( function( node ) { // Avoid read-only (i.e. contenteditable="false") divs (https://dev.ckeditor.com/ticket/11083). return node.is( 'div' ) && !node.isReadOnly(); }, 1 ); } }; } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview The "elementspath" plugin. It shows all elements in the DOM * parent tree relative to the current selection in the editing area. */ ( function() { var commands = { toolbarFocus: { editorFocus: false, readOnly: 1, exec: function( editor ) { var idBase = editor._.elementsPath.idBase; var element = CKEDITOR.document.getById( idBase + '0' ); // Make the first button focus accessible for IE. (https://dev.ckeditor.com/ticket/3417) // Adobe AIR instead need while of delay. element && element.focus( CKEDITOR.env.ie || CKEDITOR.env.air ); } } }; var emptyHtml = ' '; var extra = ''; // Some browsers don't cancel key events in the keydown but in the // keypress. // TODO: Check if really needed. if ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) extra += ' onkeypress="return false;"'; // With Firefox, we need to force the button to redraw, otherwise it // will remain in the focus state. if ( CKEDITOR.env.gecko ) extra += ' onblur="this.style.cssText = this.style.cssText;"'; var pathItemTpl = CKEDITOR.addTemplate( 'pathItem', '' + '{text}' + '' ); CKEDITOR.plugins.add( 'elementspath', { // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { editor._.elementsPath = { idBase: 'cke_elementspath_' + CKEDITOR.tools.getNextNumber() + '_', filters: [] }; editor.on( 'uiSpace', function( event ) { if ( event.data.space == 'bottom' ) initElementsPath( editor, event.data ); } ); } } ); function initElementsPath( editor, bottomSpaceData ) { var spaceId = editor.ui.spaceId( 'path' ), spaceElement, getSpaceElement = function() { if ( !spaceElement ) spaceElement = CKEDITOR.document.getById( spaceId ); return spaceElement; }, elementsPath = editor._.elementsPath, idBase = elementsPath.idBase; bottomSpaceData.html += '' + editor.lang.elementspath.eleLabel + '' + '' + emptyHtml + ''; // Register the ui element to the focus manager. editor.on( 'uiReady', function() { var element = editor.ui.space( 'path' ); element && editor.focusManager.add( element, 1 ); } ); function onClick( elementIndex ) { var element = elementsPath.list[ elementIndex ], selection; if ( element.equals( editor.editable() ) || element.getAttribute( 'contenteditable' ) == 'true' ) { var range = editor.createRange(); range.selectNodeContents( element ); selection = range.select(); } else { selection = editor.getSelection(); selection.selectElement( element ); } // Explicitly fire selectionChange when clicking on an element path button. (https://dev.ckeditor.com/ticket/13548) if ( CKEDITOR.env.ie ) { editor.fire( 'selectionChange', { selection: selection, path: new CKEDITOR.dom.elementPath( element ) } ); } // It is important to focus() *after* the above selection // manipulation, otherwise Firefox will have troubles. https://dev.ckeditor.com/ticket/10119 editor.focus(); } elementsPath.onClick = onClick; var onClickHanlder = CKEDITOR.tools.addFunction( onClick ), onKeyDownHandler = CKEDITOR.tools.addFunction( function( elementIndex, ev ) { var idBase = elementsPath.idBase, element; ev = new CKEDITOR.dom.event( ev ); var rtl = editor.lang.dir == 'rtl'; switch ( ev.getKeystroke() ) { case rtl ? 39 : 37: // LEFT-ARROW case 9: // TAB element = CKEDITOR.document.getById( idBase + ( elementIndex + 1 ) ); if ( !element ) element = CKEDITOR.document.getById( idBase + '0' ); element.focus(); return false; case rtl ? 37 : 39: // RIGHT-ARROW case CKEDITOR.SHIFT + 9: // SHIFT + TAB element = CKEDITOR.document.getById( idBase + ( elementIndex - 1 ) ); if ( !element ) element = CKEDITOR.document.getById( idBase + ( elementsPath.list.length - 1 ) ); element.focus(); return false; case 27: // ESC editor.focus(); return false; case 13: // ENTER // Opera case 32: // SPACE onClick( elementIndex ); return false; } return true; } ); editor.on( 'selectionChange', function( evt ) { var html = [], elementsList = elementsPath.list = [], namesList = [], filters = elementsPath.filters, isContentEditable = true, // Use elementPath to consider children of editable only (https://dev.ckeditor.com/ticket/11124). // Use elementPath from event (instead of editor.elementPath()), which is accurate in all cases (#801). elementsChain = evt.data.path.elements, name; // Starts iteration from body element, skipping html. for ( var j = elementsChain.length; j--; ) { var element = elementsChain[ j ], ignore = 0; if ( element.data( 'cke-display-name' ) ) name = element.data( 'cke-display-name' ); else if ( element.data( 'cke-real-element-type' ) ) name = element.data( 'cke-real-element-type' ); else name = element.getName(); isContentEditable = element.hasAttribute( 'contenteditable' ) ? element.getAttribute( 'contenteditable' ) == 'true' : isContentEditable; // If elem is non-contenteditable, and it's not specifying contenteditable // attribute - then elem should be ignored. if ( !isContentEditable && !element.hasAttribute( 'contenteditable' ) ) ignore = 1; for ( var i = 0; i < filters.length; i++ ) { var ret = filters[ i ]( element, name ); if ( ret === false ) { ignore = 1; break; } name = ret || name; } if ( !ignore ) { elementsList.unshift( element ); namesList.unshift( name ); } } for ( var iterationLimit = elementsList.length, index = 0; index < iterationLimit; index++ ) { name = namesList[ index ]; var label = editor.lang.elementspath.eleTitle.replace( /%1/, name ), item = pathItemTpl.output( { id: idBase + index, label: label, text: name, jsTitle: 'javascript:void(\'' + name + '\')', // jshint ignore:line index: index, keyDownFn: onKeyDownHandler, clickFn: onClickHanlder } ); html.unshift( item ); } var space = getSpaceElement(); space.setHtml( html.join( '' ) + emptyHtml ); editor.fire( 'elementsPathUpdate', { space: space } ); } ); function empty() { spaceElement && spaceElement.setHtml( emptyHtml ); delete elementsPath.list; } editor.on( 'readOnly', empty ); editor.on( 'contentDomUnload', empty ); editor.addCommand( 'elementsPathFocus', commands.toolbarFocus ); editor.setKeystroke( CKEDITOR.ALT + 122 /*F11*/, 'elementsPathFocus' ); } } )(); /** * Fired when the contents of the elementsPath are changed. * * @event elementsPathUpdate * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param data * @param {CKEDITOR.dom.element} data.space The elementsPath container. */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ ( function() { CKEDITOR.plugins.add( 'enterkey', { init: function( editor ) { editor.addCommand( 'enter', { modes: { wysiwyg: 1 }, editorFocus: false, exec: function( editor ) { enter( editor ); } } ); editor.addCommand( 'shiftEnter', { modes: { wysiwyg: 1 }, editorFocus: false, exec: function( editor ) { shiftEnter( editor ); } } ); editor.setKeystroke( [ [ 13, 'enter' ], [ CKEDITOR.SHIFT + 13, 'shiftEnter' ] ] ); } } ); var whitespaces = CKEDITOR.dom.walker.whitespaces(), bookmark = CKEDITOR.dom.walker.bookmark(), plugin, enterBr, enterBlock, headerTagRegex; CKEDITOR.plugins.enterkey = { enterBlock: function( editor, mode, range, forceMode ) { // Get the range for the current selection. range = range || getRange( editor ); // We may not have valid ranges to work on, like when inside a // contenteditable=false element. if ( !range ) return; // When range is in nested editable, we have to replace range with this one, // which have root property set to closest editable, to make auto paragraphing work. (https://dev.ckeditor.com/ticket/12162) range = replaceRangeWithClosestEditableRoot( range ); var doc = range.document; var atBlockStart = range.checkStartOfBlock(), atBlockEnd = range.checkEndOfBlock(), path = editor.elementPath( range.startContainer ), block = path.block, // Determine the block element to be used. blockTag = ( mode == CKEDITOR.ENTER_DIV ? 'div' : 'p' ), newBlock; if ( block && atBlockStart && atBlockEnd ) { var blockParent = block.getParent(); // Block placeholder breaks list structure (#2205). if ( blockParent.is( 'li' ) && blockParent.getChildCount() > 1 ) { var placeholder = new CKEDITOR.dom.element( 'li' ), newRange = editor.createRange(); placeholder.insertAfter( blockParent ); block.remove(); newRange.setStart( placeholder, 0 ); editor.getSelection().selectRanges( [ newRange ] ); return; } // Exit the list when we're inside an empty list item block. (https://dev.ckeditor.com/ticket/5376). else if ( block.is( 'li' ) || block.getParent().is( 'li' ) ) { // Make sure to point to the li when dealing with empty list item. if ( !block.is( 'li' ) ) { block = block.getParent(); blockParent = block.getParent(); } var blockGrandParent = blockParent.getParent(), firstChild = !block.hasPrevious(), lastChild = !block.hasNext(), selection = editor.getSelection(), bookmarks = selection.createBookmarks(), orgDir = block.getDirection( 1 ), className = block.getAttribute( 'class' ), style = block.getAttribute( 'style' ), dirLoose = blockGrandParent.getDirection( 1 ) != orgDir, enterMode = editor.enterMode, needsBlock = enterMode != CKEDITOR.ENTER_BR || dirLoose || style || className, child; if ( blockGrandParent.is( 'li' ) ) { // If block is the first or the last child of the parent // list, degrade it and move to the outer list: // before the parent list if block is first child and after // the parent list if block is the last child, respectively. // //
    =>
      //
    • =>
    • //
        =>
          //
        • x
        • =>
        • x
        • //
        • ^
        • =>
        //
      =>
    • // =>
    • ^
    • //
    =>
// // AND // //
    =>
      //
    • =>
    • ^
    • //
        =>
      • //
      • ^
      • =>
          //
        • x
        • =>
        • x
        • //
        =>
      // => //
    =>
if ( firstChild || lastChild ) { // If it's only child, we don't want to keep perent ul anymore. if ( firstChild && lastChild ) { blockParent.remove(); } block[lastChild ? 'insertAfter' : 'insertBefore']( blockGrandParent ); // If the empty block is neither first nor last child // then split the list and the block as an element // of outer list. // // =>
    // =>
  • //
      =>
        //
      • =>
      • x
      • //
          =>
        //
      • x
      • => //
      • ^
      • =>
      • ^
      • //
      • y
      • =>
      • //
      =>
        // =>
      • y
      • //
      =>
    // =>
  • // =>
} else { block.breakParent( blockGrandParent ); } } else if ( !needsBlock ) { block.appendBogus( true ); // If block is the first or last child of the parent // list, move all block's children out of the list: // before the list if block is first child and after the list // if block is the last child, respectively. // //
    =>
      //
    • x
    • =>
    • x
    • //
    • ^
    • =>
    //
=> ^ // // AND // //
    => ^ //
  • ^
  • =>
      //
    • x
    • =>
    • x
    • //
    =>
if ( firstChild || lastChild ) { while ( ( child = block[ firstChild ? 'getFirst' : 'getLast' ]() ) ) child[ firstChild ? 'insertBefore' : 'insertAfter' ]( blockParent ); } // If the empty block is neither first nor last child // then split the list and put all the block contents // between two lists. // //
    =>
      //
    • x
    • =>
    • x
    • //
    • ^
    • =>
    //
  • y
  • => ^ //
=>
    // =>
  • y
  • // =>
else { block.breakParent( blockParent ); while ( ( child = block.getLast() ) ) child.insertAfter( blockParent ); } block.remove(); } else { // Original path block is the list item, create new block for the list item content. if ( path.block.is( 'li' ) ) { // Use
block for ENTER_BR and ENTER_DIV. newBlock = doc.createElement( mode == CKEDITOR.ENTER_P ? 'p' : 'div' ); if ( dirLoose ) newBlock.setAttribute( 'dir', orgDir ); style && newBlock.setAttribute( 'style', style ); className && newBlock.setAttribute( 'class', className ); // Move all the child nodes to the new block. block.moveChildren( newBlock ); } // The original path block is not a list item, just copy the block to out side of the list. else { newBlock = path.block; } // If block is the first or last child of the parent // list, move it out of the list: // before the list if block is first child and after the list // if block is the last child, respectively. // //
    =>
      //
    • x
    • =>
    • x
    • //
    • ^
    • =>
    //
=>

^

// // AND // //
    =>

    ^

    //
  • ^
  • =>
      //
    • x
    • =>
    • x
    • //
    =>
if ( firstChild || lastChild ) newBlock[ firstChild ? 'insertBefore' : 'insertAfter' ]( blockParent ); // If the empty block is neither first nor last child // then split the list and put the new block between // two lists. // // =>
    //
      =>
    • x
    • //
    • x
    • =>
    //
  • ^
  • =>

    ^

    //
  • y
  • =>
      //
    =>
  • y
  • // =>
else { block.breakParent( blockParent ); newBlock.insertAfter( blockParent ); } block.remove(); } selection.selectBookmarks( bookmarks ); return; } if ( block && block.getParent().is( 'blockquote' ) ) { block.breakParent( block.getParent() ); // If we were at the start of
, there will be an empty element before it now. if ( !block.getPrevious().getFirst( CKEDITOR.dom.walker.invisible( 1 ) ) ) block.getPrevious().remove(); // If we were at the end of
, there will be an empty element after it now. if ( !block.getNext().getFirst( CKEDITOR.dom.walker.invisible( 1 ) ) ) block.getNext().remove(); range.moveToElementEditStart( block ); range.select(); return; } } // Don't split
 if we're in the middle of it, act as shift enter key.
			else if ( block && block.is( 'pre' ) ) {
				if ( !atBlockEnd ) {
					enterBr( editor, mode, range, forceMode );
					return;
				}
			}

			// Split the range.
			var splitInfo = range.splitBlock( blockTag );

			if ( !splitInfo )
				return;

			// Get the current blocks.
			var previousBlock = splitInfo.previousBlock,
				nextBlock = splitInfo.nextBlock;

			var isStartOfBlock = splitInfo.wasStartOfBlock,
				isEndOfBlock = splitInfo.wasEndOfBlock;

			var node;

			// If this is a block under a list item, split it as well. (https://dev.ckeditor.com/ticket/1647)
			if ( nextBlock ) {
				node = nextBlock.getParent();
				if ( node.is( 'li' ) ) {
					nextBlock.breakParent( node );
					nextBlock.move( nextBlock.getNext(), 1 );
				}
			} else if ( previousBlock && ( node = previousBlock.getParent() ) && node.is( 'li' ) ) {
				previousBlock.breakParent( node );
				node = previousBlock.getNext();
				range.moveToElementEditStart( node );
				previousBlock.move( previousBlock.getPrevious() );
			}
			// If we have both the previous and next blocks, it means that the
			// boundaries were on separated blocks, or none of them where on the
			// block limits (start/end).
			if ( !isStartOfBlock && !isEndOfBlock ) {
				// If the next block is an 
  • with another list tree as the first // child, we'll need to append a filler (
    /NBSP) or the list item // wouldn't be editable. (https://dev.ckeditor.com/ticket/1420) if ( nextBlock.is( 'li' ) ) { var walkerRange = range.clone(); walkerRange.selectNodeContents( nextBlock ); var walker = new CKEDITOR.dom.walker( walkerRange ); walker.evaluator = function( node ) { return !( bookmark( node ) || whitespaces( node ) || node.type == CKEDITOR.NODE_ELEMENT && node.getName() in CKEDITOR.dtd.$inline && !( node.getName() in CKEDITOR.dtd.$empty ) ); }; node = walker.next(); if ( node && node.type == CKEDITOR.NODE_ELEMENT && node.is( 'ul', 'ol' ) ) ( CKEDITOR.env.needsBrFiller ? doc.createElement( 'br' ) : doc.createText( '\xa0' ) ).insertBefore( node ); } // Move the selection to the end block. if ( nextBlock ) range.moveToElementEditStart( nextBlock ); } // Handle differently when content of table cell was erased by pressing enter (#1816). // We don't want to add new block, because it was created with range.splitBlock(). else if ( preventExtraLineInsideTable( mode ) ) { range.moveToElementEditStart( range.getTouchedStartNode() ); } else { var newBlockDir; if ( previousBlock ) { // Do not enter this block if it's a header tag, or we are in // a Shift+Enter (https://dev.ckeditor.com/ticket/77). Create a new block element instead // (later in the code). if ( previousBlock.is( 'li' ) || !( headerTagRegex.test( previousBlock.getName() ) || previousBlock.is( 'pre' ) ) ) { // Otherwise, duplicate the previous block. newBlock = previousBlock.clone(); } } else if ( nextBlock ) { newBlock = nextBlock.clone(); } if ( !newBlock ) { // We have already created a new list item. (https://dev.ckeditor.com/ticket/6849) if ( node && node.is( 'li' ) ) newBlock = node; else { newBlock = doc.createElement( blockTag ); if ( previousBlock && ( newBlockDir = previousBlock.getDirection() ) ) newBlock.setAttribute( 'dir', newBlockDir ); } } // Force the enter block unless we're talking of a list item. else if ( forceMode && !newBlock.is( 'li' ) ) { newBlock.renameNode( blockTag ); } // Recreate the inline elements tree, which was available // before hitting enter, so the same styles will be available in // the new block. var elementPath = splitInfo.elementPath; if ( elementPath ) { for ( var i = 0, len = elementPath.elements.length; i < len; i++ ) { var element = elementPath.elements[ i ]; if ( element.equals( elementPath.block ) || element.equals( elementPath.blockLimit ) ) break; if ( CKEDITOR.dtd.$removeEmpty[ element.getName() ] ) { element = element.clone(); newBlock.moveChildren( element ); newBlock.append( element ); } } } newBlock.appendBogus(); if ( !newBlock.getParent() ) range.insertNode( newBlock ); // list item start number should not be duplicated (https://dev.ckeditor.com/ticket/7330), but we need // to remove the attribute after it's onto the DOM tree because of old IEs (https://dev.ckeditor.com/ticket/7581). newBlock.is( 'li' ) && newBlock.removeAttribute( 'value' ); // This is tricky, but to make the new block visible correctly // we must select it. // The previousBlock check has been included because it may be // empty if we have fixed a block-less space (like ENTER into an // empty table cell). if ( CKEDITOR.env.ie && isStartOfBlock && ( !isEndOfBlock || !previousBlock.getChildCount() ) ) { // Move the selection to the new block. range.moveToElementEditStart( isEndOfBlock ? previousBlock : newBlock ); range.select(); } // Move the selection to the new block. range.moveToElementEditStart( isStartOfBlock && !isEndOfBlock ? nextBlock : newBlock ); } range.select(); range.scrollIntoView(); // ===== HELPERS ===== function preventExtraLineInsideTable( mode ) { // #1816 // We want to have behaviour after pressing enter like this: // 1. ^ ->

    // 2. Foo^ ->

    Foo

    // 3. Foo^Bar ->

    Foo

    Bar

    // We need to separate 1. case to not add extra line. Like it happen for 2nd or 3rd option. var innerElement, bogus; if ( mode === CKEDITOR.ENTER_BR ) { return false; } if ( CKEDITOR.tools.indexOf( [ 'td', 'th' ], path.lastElement.getName() ) === -1 ) { return false; } if ( path.lastElement.getChildCount() !== 1 ) { return false; } innerElement = path.lastElement.getChild( 0 ).clone( true ); bogus = innerElement.getBogus(); if ( bogus ) { bogus.remove(); } if ( innerElement.getText().length ) { return false; } return true; } }, enterBr: function( editor, mode, range, forceMode ) { // Get the range for the current selection. range = range || getRange( editor ); // We may not have valid ranges to work on, like when inside a // contenteditable=false element. if ( !range ) return; var doc = range.document; var isEndOfBlock = range.checkEndOfBlock(); var elementPath = new CKEDITOR.dom.elementPath( editor.getSelection().getStartElement() ); var startBlock = elementPath.block, startBlockTag = startBlock && elementPath.block.getName(); if ( !forceMode && startBlockTag == 'li' ) { enterBlock( editor, mode, range, forceMode ); return; } // If we are at the end of a header block. if ( !forceMode && isEndOfBlock && headerTagRegex.test( startBlockTag ) ) { var newBlock, newBlockDir; if ( ( newBlockDir = startBlock.getDirection() ) ) { newBlock = doc.createElement( 'div' ); newBlock.setAttribute( 'dir', newBlockDir ); newBlock.insertAfter( startBlock ); range.setStart( newBlock, 0 ); } else { // Insert a
    after the current paragraph. doc.createElement( 'br' ).insertAfter( startBlock ); // A text node is required by Gecko only to make the cursor blink. if ( CKEDITOR.env.gecko ) doc.createText( '' ).insertAfter( startBlock ); // IE has different behaviors regarding position. range.setStartAt( startBlock.getNext(), CKEDITOR.env.ie ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_START ); } } else { var lineBreak; // IE<8 prefers text node as line-break inside of
     (https://dev.ckeditor.com/ticket/4711).
    				if ( startBlockTag == 'pre' && CKEDITOR.env.ie && CKEDITOR.env.version < 8 )
    					lineBreak = doc.createText( '\r' );
    				else
    					lineBreak = doc.createElement( 'br' );
    
    				range.deleteContents();
    				range.insertNode( lineBreak );
    
    				// Old IEs have different behavior regarding position.
    				if ( !CKEDITOR.env.needsBrFiller )
    					range.setStartAt( lineBreak, CKEDITOR.POSITION_AFTER_END );
    				else {
    					// A text node is required by Gecko only to make the cursor blink.
    					// We need some text inside of it, so the bogus 
    is properly // created. doc.createText( '\ufeff' ).insertAfter( lineBreak ); // If we are at the end of a block, we must be sure the bogus node is available in that block. if ( isEndOfBlock ) { // In most situations we've got an elementPath.block (e.g.

    ), but in a // blockless editor or when autoP is false that needs to be a block limit. ( startBlock || elementPath.blockLimit ).appendBogus(); } // Now we can remove the text node contents, so the caret doesn't // stop on it. lineBreak.getNext().$.nodeValue = ''; range.setStartAt( lineBreak.getNext(), CKEDITOR.POSITION_AFTER_START ); } } // This collapse guarantees the cursor will be blinking. range.collapse( true ); range.select(); range.scrollIntoView(); } }; plugin = CKEDITOR.plugins.enterkey; enterBr = plugin.enterBr; enterBlock = plugin.enterBlock; headerTagRegex = /^h[1-6]$/; function shiftEnter( editor ) { // On SHIFT+ENTER: // 1. We want to enforce the mode to be respected, instead // of cloning the current block. (https://dev.ckeditor.com/ticket/77) return enter( editor, editor.activeShiftEnterMode, 1 ); } function enter( editor, mode, forceMode ) { forceMode = editor.config.forceEnterMode || forceMode; // Only effective within document. if ( editor.mode != 'wysiwyg' ) return; if ( !mode ) mode = editor.activeEnterMode; // TODO this should be handled by setting editor.activeEnterMode on selection change. // Check path block specialities: // 1. Cannot be a un-splittable element, e.g. table caption; var path = editor.elementPath(); if ( path && !path.isContextFor( 'p' ) ) { mode = CKEDITOR.ENTER_BR; forceMode = 1; } editor.fire( 'saveSnapshot' ); // Save undo step. if ( mode == CKEDITOR.ENTER_BR ) enterBr( editor, mode, null, forceMode ); else enterBlock( editor, mode, null, forceMode ); editor.fire( 'saveSnapshot' ); } function getRange( editor ) { // Get the selection ranges. var ranges = editor.getSelection().getRanges( true ); // Delete the contents of all ranges except the first one. for ( var i = ranges.length - 1; i > 0; i-- ) { ranges[ i ].deleteContents(); } // Return the first range. return ranges[ 0 ]; } function replaceRangeWithClosestEditableRoot( range ) { var closestEditable = range.startContainer.getAscendant( function( node ) { return node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'true'; }, true ); if ( range.root.equals( closestEditable ) ) { return range; } else { var newRange = new CKEDITOR.dom.range( closestEditable ); newRange.moveToRange( range ); return newRange; } } } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ ( function() { var win = CKEDITOR.document.getWindow(), pixelate = CKEDITOR.tools.cssLength; CKEDITOR.plugins.add( 'floatingspace', { init: function( editor ) { // Add listener with lower priority than that in themedui creator. // Thereby floatingspace will be created only if themedui wasn't used. editor.on( 'loaded', function() { attach( this ); }, null, null, 20 ); } } ); function scrollOffset( side ) { var pageOffset = side == 'left' ? 'pageXOffset' : 'pageYOffset', docScrollOffset = side == 'left' ? 'scrollLeft' : 'scrollTop'; return ( pageOffset in win.$ ) ? win.$[ pageOffset ] : CKEDITOR.document.$.documentElement[ docScrollOffset ]; } function attach( editor ) { var config = editor.config, // Get the HTML for the predefined spaces. topHtml = editor.fire( 'uiSpace', { space: 'top', html: '' } ).html, // Re-positioning of the space. layout = ( function() { // Mode indicates the vertical aligning mode. var mode, editable, spaceRect, editorRect, viewRect, spaceHeight, pageScrollX, // Allow minor adjustments of the float space from custom configs. dockedOffsetX = config.floatSpaceDockedOffsetX || 0, dockedOffsetY = config.floatSpaceDockedOffsetY || 0, pinnedOffsetX = config.floatSpacePinnedOffsetX || 0, pinnedOffsetY = config.floatSpacePinnedOffsetY || 0; // Update the float space position. function updatePos( pos, prop, val ) { floatSpace.setStyle( prop, pixelate( val ) ); floatSpace.setStyle( 'position', pos ); } // Change the current mode and update float space position accordingly. function changeMode( newMode ) { var editorPos = editable.getDocumentPosition(); switch ( newMode ) { case 'top': updatePos( 'absolute', 'top', editorPos.y - spaceHeight - dockedOffsetY ); break; case 'pin': updatePos( 'fixed', 'top', pinnedOffsetY ); break; case 'bottom': updatePos( 'absolute', 'top', editorPos.y + ( editorRect.height || editorRect.bottom - editorRect.top ) + dockedOffsetY ); break; } mode = newMode; } return function( evt ) { // https://dev.ckeditor.com/ticket/10112 Do not fail on editable-less editor. if ( !( editable = editor.editable() ) ) return; var show = ( evt && evt.name == 'focus' ); // Show up the space on focus gain. if ( show ) { floatSpace.show(); } editor.fire( 'floatingSpaceLayout', { show: show } ); // Reset the horizontal position for below measurement. floatSpace.removeStyle( 'left' ); floatSpace.removeStyle( 'right' ); // Compute the screen position from the TextRectangle object would // be very simple, even though the "width"/"height" property is not // available for all, it's safe to figure that out from the rest. // http://help.dottoro.com/ljgupwlp.php spaceRect = floatSpace.getClientRect(); editorRect = editable.getClientRect(); viewRect = win.getViewPaneSize(); spaceHeight = spaceRect.height; pageScrollX = scrollOffset( 'left' ); // We initialize it as pin mode. if ( !mode ) { mode = 'pin'; changeMode( 'pin' ); // Call for a refresh to the actual layout. layout( evt ); return; } // +------------------------ Viewport -+ \ // | | |-> floatSpaceDockedOffsetY // | ................................. | / // | | // | +------ Space -+ | // | | | | // | +--------------+ | // | +------------------ Editor -+ | // | | | | // if ( spaceHeight + dockedOffsetY <= editorRect.top ) changeMode( 'top' ); // +- - - - - - - - - Editor -+ // | | // +------------------------ Viewport -+ \ // | | | | |-> floatSpacePinnedOffsetY // | ................................. | / // | +------ Space -+ | | // | | | | | // | +--------------+ | | // | | | | // | +---------------------------+ | // +-----------------------------------+ // else if ( spaceHeight + dockedOffsetY > viewRect.height - editorRect.bottom ) changeMode( 'pin' ); // +- - - - - - - - - Editor -+ // | | // +------------------------ Viewport -+ \ // | | | | |-> floatSpacePinnedOffsetY // | ................................. | / // | | | | // | | | | // | +---------------------------+ | // | +------ Space -+ | // | | | | // | +--------------+ | // else changeMode( 'bottom' ); var mid = viewRect.width / 2, alignSide, offset; if ( config.floatSpacePreferRight ) { alignSide = 'right'; } else if ( editorRect.left > 0 && editorRect.right < viewRect.width && editorRect.width > spaceRect.width ) { alignSide = config.contentsLangDirection == 'rtl' ? 'right' : 'left'; } else { alignSide = mid - editorRect.left > editorRect.right - mid ? 'left' : 'right'; } // (https://dev.ckeditor.com/ticket/9769) If viewport width is less than space width, // make sure space never cross the left boundary of the viewport. // In other words: top-left corner of the space is always visible. if ( spaceRect.width > viewRect.width ) { alignSide = 'left'; offset = 0; } else { if ( alignSide == 'left' ) { // If the space rect fits into viewport, align it // to the left edge of editor: // // +------------------------ Viewport -+ // | | // | +------------- Space -+ | // | | | | // | +---------------------+ | // | +------------------ Editor -+ | // | | | | // if ( editorRect.left > 0 ) offset = editorRect.left; // If the left part of the editor is cut off by the left // edge of the viewport, stick the space to the viewport: // // +------------------------ Viewport -+ // | | // +---------------- Space -+ | // | | | // +------------------------+ | // +----|------------- Editor -+ | // | | | | // else offset = 0; } else { // If the space rect fits into viewport, align it // to the right edge of editor: // // +------------------------ Viewport -+ // | | // | +------------- Space -+ | // | | | | // | +---------------------+ | // | +------------------ Editor -+ | // | | | | // if ( editorRect.right < viewRect.width ) offset = viewRect.width - editorRect.right; // If the right part of the editor is cut off by the right // edge of the viewport, stick the space to the viewport: // // +------------------------ Viewport -+ // | | // | +------------- Space -+ // | | | // | +---------------------+ // | +-----------------|- Editor -+ // | | | | // else offset = 0; } // (https://dev.ckeditor.com/ticket/9769) Finally, stick the space to the opposite side of // the viewport when it's cut off horizontally on the left/right // side like below. // // This trick reveals cut off space in some edge cases and // hence it improves accessibility. // // +------------------------ Viewport -+ // | | // | +--------------------|-- Space -+ // | | | | // | +--------------------|----------+ // | +------- Editor -+ | // | | | | // // becomes: // // +------------------------ Viewport -+ // | | // | +----------------------- Space -+ // | | | // | +-------------------------------+ // | +------- Editor -+ | // | | | | // if ( offset + spaceRect.width > viewRect.width ) { alignSide = alignSide == 'left' ? 'right' : 'left'; offset = 0; } } // Pin mode is fixed, so don't include scroll-x. // (https://dev.ckeditor.com/ticket/9903) For mode is "top" or "bottom", add opposite scroll-x for right-aligned space. var scroll = mode == 'pin' ? 0 : alignSide == 'left' ? pageScrollX : -pageScrollX; floatSpace.setStyle( alignSide, pixelate( ( mode == 'pin' ? pinnedOffsetX : dockedOffsetX ) + offset + scroll ) ); }; } )(); if ( topHtml ) { var floatSpaceTpl = new CKEDITOR.template( '' + ( editor.title ? '{voiceLabel}' : ' ' ) + '

    ' + '' + '
    ' + '
  • ' ), floatSpace = CKEDITOR.document.getBody().append( CKEDITOR.dom.element.createFromHtml( floatSpaceTpl.output( { content: topHtml, id: editor.id, langDir: editor.lang.dir, langCode: editor.langCode, name: editor.name, style: 'display:none;z-index:' + ( config.baseFloatZIndex - 1 ), topId: editor.ui.spaceId( 'top' ), voiceLabel: editor.title } ) ) ), // Use event buffers to reduce CPU load when tons of events are fired. changeBuffer = CKEDITOR.tools.eventsBuffer( 500, layout ), uiBuffer = CKEDITOR.tools.eventsBuffer( 100, layout ); // There's no need for the floatSpace to be selectable. floatSpace.unselectable(); // Prevent clicking on non-buttons area of the space from blurring editor. floatSpace.on( 'mousedown', function( evt ) { evt = evt.data; if ( !evt.getTarget().hasAscendant( 'a', 1 ) ) evt.preventDefault(); } ); editor.on( 'focus', function( evt ) { layout( evt ); editor.on( 'change', changeBuffer.input ); win.on( 'scroll', uiBuffer.input ); win.on( 'resize', uiBuffer.input ); } ); editor.on( 'blur', function() { floatSpace.hide(); editor.removeListener( 'change', changeBuffer.input ); win.removeListener( 'scroll', uiBuffer.input ); win.removeListener( 'resize', uiBuffer.input ); } ); editor.on( 'destroy', function() { win.removeListener( 'scroll', uiBuffer.input ); win.removeListener( 'resize', uiBuffer.input ); floatSpace.clearCustomData(); floatSpace.remove(); } ); // Handle initial focus. if ( editor.focusManager.hasFocus ) floatSpace.show(); // Register this UI space to the focus manager. editor.focusManager.add( floatSpace, 1 ); } } } )(); /** * Along with {@link #floatSpaceDockedOffsetY} it defines the * amount of offset (in pixels) between the float space and the editable left/right * boundaries when the space element is docked on either side of the editable. * * config.floatSpaceDockedOffsetX = 10; * * @cfg {Number} [floatSpaceDockedOffsetX=0] * @member CKEDITOR.config */ /** * Along with {@link #floatSpaceDockedOffsetX} it defines the * amount of offset (in pixels) between the float space and the editable top/bottom * boundaries when the space element is docked on either side of the editable. * * config.floatSpaceDockedOffsetY = 10; * * @cfg {Number} [floatSpaceDockedOffsetY=0] * @member CKEDITOR.config */ /** * Along with {@link #floatSpacePinnedOffsetY} it defines the * amount of offset (in pixels) between the float space and the viewport boundaries * when the space element is pinned. * * config.floatSpacePinnedOffsetX = 20; * * @cfg {Number} [floatSpacePinnedOffsetX=0] * @member CKEDITOR.config */ /** * Along with {@link #floatSpacePinnedOffsetX} it defines the * amount of offset (in pixels) between the float space and the viewport boundaries * when the space element is pinned. * * config.floatSpacePinnedOffsetY = 20; * * @cfg {Number} [floatSpacePinnedOffsetY=0] * @member CKEDITOR.config */ /** * Indicates that the float space should be aligned to the right side * of the editable area rather than to the left (if possible). * * config.floatSpacePreferRight = true; * * @since 4.5.0 * @cfg {Boolean} [floatSpacePreferRight=false] * @member CKEDITOR.config */ /** * Fired when the viewport or editor parameters change and the floating space needs to check and * eventually update its position and dimensions. * * @since 4.5.0 * @event floatingSpaceLayout * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor The editor instance. * @param data * @param {Boolean} data.show True if the float space should show up as a result of this event. */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'listblock', { requires: 'panel', onLoad: function() { var list = CKEDITOR.addTemplate( 'panel-list', '' ), listItem = CKEDITOR.addTemplate( 'panel-list-item', '' ), listGroup = CKEDITOR.addTemplate( 'panel-list-group', '

    {label}

    ' ), reSingleQuote = /\'/g, escapeSingleQuotes = function( str ) { return str.replace( reSingleQuote, '\\\'' ); }; CKEDITOR.ui.panel.prototype.addListBlock = function( name, definition ) { return this.addBlock( name, new CKEDITOR.ui.listBlock( this.getHolderElement(), definition ) ); }; CKEDITOR.ui.listBlock = CKEDITOR.tools.createClass( { base: CKEDITOR.ui.panel.block, $: function( blockHolder, blockDefinition ) { blockDefinition = blockDefinition || {}; var attribs = blockDefinition.attributes || ( blockDefinition.attributes = {} ); ( this.multiSelect = !!blockDefinition.multiSelect ) && ( attribs[ 'aria-multiselectable' ] = true ); // Provide default role of 'listbox'. !attribs.role && ( attribs.role = 'listbox' ); // Call the base contructor. this.base.apply( this, arguments ); // Set the proper a11y attributes. this.element.setAttribute( 'role', attribs.role ); var keys = this.keys; keys[ 40 ] = 'next'; // ARROW-DOWN keys[ 9 ] = 'next'; // TAB keys[ 38 ] = 'prev'; // ARROW-UP keys[ CKEDITOR.SHIFT + 9 ] = 'prev'; // SHIFT + TAB keys[ 32 ] = CKEDITOR.env.ie ? 'mouseup' : 'click'; // SPACE CKEDITOR.env.ie && ( keys[ 13 ] = 'mouseup' ); // Manage ENTER, since onclick is blocked in IE (https://dev.ckeditor.com/ticket/8041). this._.pendingHtml = []; this._.pendingList = []; this._.items = {}; this._.groups = {}; }, _: { close: function() { if ( this._.started ) { var output = list.output( { items: this._.pendingList.join( '' ) } ); this._.pendingList = []; this._.pendingHtml.push( output ); delete this._.started; } }, getClick: function() { if ( !this._.click ) { this._.click = CKEDITOR.tools.addFunction( function( value ) { var marked = this.toggle( value ); if ( this.onClick ) this.onClick( value, marked ); }, this ); } return this._.click; } }, proto: { add: function( value, html, title ) { var id = CKEDITOR.tools.getNextId(); if ( !this._.started ) { this._.started = 1; this._.size = this._.size || 0; } this._.items[ value ] = id; var data = { id: id, val: escapeSingleQuotes( CKEDITOR.tools.htmlEncodeAttr( value ) ), onclick: CKEDITOR.env.ie ? 'onclick="return false;" onmouseup' : 'onclick', clickFn: this._.getClick(), title: CKEDITOR.tools.htmlEncodeAttr( title || value ), text: html || value }; this._.pendingList.push( listItem.output( data ) ); }, startGroup: function( title ) { this._.close(); var id = CKEDITOR.tools.getNextId(); this._.groups[ title ] = id; this._.pendingHtml.push( listGroup.output( { id: id, label: title } ) ); }, commit: function() { this._.close(); this.element.appendHtml( this._.pendingHtml.join( '' ) ); delete this._.size; this._.pendingHtml = []; }, toggle: function( value ) { var isMarked = this.isMarked( value ); if ( isMarked ) this.unmark( value ); else this.mark( value ); return !isMarked; }, hideGroup: function( groupTitle ) { var group = this.element.getDocument().getById( this._.groups[ groupTitle ] ), list = group && group.getNext(); if ( group ) { group.setStyle( 'display', 'none' ); if ( list && list.getName() == 'ul' ) list.setStyle( 'display', 'none' ); } }, hideItem: function( value ) { this.element.getDocument().getById( this._.items[ value ] ).setStyle( 'display', 'none' ); }, showAll: function() { var items = this._.items, groups = this._.groups, doc = this.element.getDocument(); for ( var value in items ) { doc.getById( items[ value ] ).setStyle( 'display', '' ); } for ( var title in groups ) { var group = doc.getById( groups[ title ] ), list = group.getNext(); group.setStyle( 'display', '' ); if ( list && list.getName() == 'ul' ) list.setStyle( 'display', '' ); } }, mark: function( value ) { if ( !this.multiSelect ) this.unmarkAll(); var itemId = this._.items[ value ], item = this.element.getDocument().getById( itemId ); item.addClass( 'cke_selected' ); this.element.getDocument().getById( itemId + '_option' ).setAttribute( 'aria-selected', true ); this.onMark && this.onMark( item ); }, markFirstDisplayed: function() { var context = this; this._.markFirstDisplayed( function() { if ( !context.multiSelect ) context.unmarkAll(); } ); }, unmark: function( value ) { var doc = this.element.getDocument(), itemId = this._.items[ value ], item = doc.getById( itemId ); item.removeClass( 'cke_selected' ); doc.getById( itemId + '_option' ).removeAttribute( 'aria-selected' ); this.onUnmark && this.onUnmark( item ); }, unmarkAll: function() { var items = this._.items, doc = this.element.getDocument(); for ( var value in items ) { var itemId = items[ value ]; doc.getById( itemId ).removeClass( 'cke_selected' ); doc.getById( itemId + '_option' ).removeAttribute( 'aria-selected' ); } this.onUnmark && this.onUnmark(); }, isMarked: function( value ) { return this.element.getDocument().getById( this._.items[ value ] ).hasClass( 'cke_selected' ); }, focus: function( value ) { this._.focusIndex = -1; var links = this.element.getElementsByTag( 'a' ), link, selected, i = -1; if ( value ) { selected = this.element.getDocument().getById( this._.items[ value ] ).getFirst(); while ( ( link = links.getItem( ++i ) ) ) { if ( link.equals( selected ) ) { this._.focusIndex = i; break; } } } else { this.element.focus(); } selected && setTimeout( function() { selected.focus(); }, 0 ); } } } ); } } ); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'richcombo', { requires: 'floatpanel,listblock,button', beforeInit: function( editor ) { editor.ui.addHandler( CKEDITOR.UI_RICHCOMBO, CKEDITOR.ui.richCombo.handler ); } } ); ( function() { var template = '' + '{label}' + '' + '{label}' + '' + '' + // BLACK DOWN-POINTING TRIANGLE ( CKEDITOR.env.hc ? '▼' : CKEDITOR.env.air ? ' ' : '' ) + '' + '' + '' + ''; var rcomboTpl = CKEDITOR.addTemplate( 'combo', template ); /** * Button UI element. * * @readonly * @property {String} [='richcombo'] * @member CKEDITOR */ CKEDITOR.UI_RICHCOMBO = 'richcombo'; /** * @class * @todo */ CKEDITOR.ui.richCombo = CKEDITOR.tools.createClass( { $: function( definition ) { // Copy all definition properties to this object. CKEDITOR.tools.extend( this, definition, // Set defaults. { // The combo won't participate in toolbar grouping. canGroup: false, title: definition.label, modes: { wysiwyg: 1 }, editorFocus: 1 } ); // We don't want the panel definition in this object. var panelDefinition = this.panel || {}; delete this.panel; this.id = CKEDITOR.tools.getNextNumber(); this.document = ( panelDefinition.parent && panelDefinition.parent.getDocument() ) || CKEDITOR.document; panelDefinition.className = 'cke_combopanel'; panelDefinition.block = { multiSelect: panelDefinition.multiSelect, attributes: panelDefinition.attributes }; panelDefinition.toolbarRelated = true; this._ = { panelDefinition: panelDefinition, items: {}, listeners: [] }; }, proto: { renderHtml: function( editor ) { var output = []; this.render( editor, output ); return output.join( '' ); }, /** * Renders the rich combo. * * @param {CKEDITOR.editor} editor The editor instance which this button is * to be used by. * @param {Array} output The output array that the HTML relative * to this button will be appended to. */ render: function( editor, output ) { var env = CKEDITOR.env; var id = 'cke_' + this.id; var clickFn = CKEDITOR.tools.addFunction( function( el ) { // Restore locked selection in Opera. if ( selLocked ) { editor.unlockSelection( 1 ); selLocked = 0; } instance.execute( el ); }, this ); var combo = this; var instance = { id: id, combo: this, focus: function() { var element = CKEDITOR.document.getById( id ).getChild( 1 ); element.focus(); }, execute: function( el ) { var _ = combo._; if ( _.state == CKEDITOR.TRISTATE_DISABLED ) return; combo.createPanel( editor ); if ( _.on ) { _.panel.hide(); return; } combo.commit(); var value = combo.getValue(); if ( value ) _.list.mark( value ); else _.list.unmarkAll(); _.panel.showBlock( combo.id, new CKEDITOR.dom.element( el ), 4 ); }, clickFn: clickFn }; function updateState() { // Don't change state while richcombo is active (https://dev.ckeditor.com/ticket/11793). if ( this.getState() == CKEDITOR.TRISTATE_ON ) return; var state = this.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED; if ( editor.readOnly && !this.readOnly ) state = CKEDITOR.TRISTATE_DISABLED; this.setState( state ); this.setValue( '' ); // Let plugin to disable button. if ( state != CKEDITOR.TRISTATE_DISABLED && this.refresh ) this.refresh(); } // Update status when activeFilter, mode, selection or readOnly changes. this._.listeners.push( editor.on( 'activeFilterChange', updateState, this ) ); this._.listeners.push( editor.on( 'mode', updateState, this ) ); this._.listeners.push( editor.on( 'selectionChange', updateState, this ) ); // If this combo is sensitive to readOnly state, update it accordingly. !this.readOnly && this._.listeners.push( editor.on( 'readOnly', updateState, this ) ); var keyDownFn = CKEDITOR.tools.addFunction( function( ev, element ) { ev = new CKEDITOR.dom.event( ev ); var keystroke = ev.getKeystroke(); switch ( keystroke ) { case 13: // ENTER case 32: // SPACE case 40: // ARROW-DOWN // Show panel CKEDITOR.tools.callFunction( clickFn, element ); break; default: // Delegate the default behavior to toolbar button key handling. instance.onkey( instance, keystroke ); } // Avoid subsequent focus grab on editor document. ev.preventDefault(); } ); var focusFn = CKEDITOR.tools.addFunction( function() { instance.onfocus && instance.onfocus(); } ); var selLocked = 0; // For clean up instance.keyDownFn = keyDownFn; var params = { id: id, name: this.name || this.command, label: this.label, title: this.title, cls: this.className || '', titleJs: env.gecko && !env.hc ? '' : ( this.title || '' ).replace( "'", '' ), keydownFn: keyDownFn, focusFn: focusFn, clickFn: clickFn }; rcomboTpl.output( params, output ); if ( this.onRender ) this.onRender(); return instance; }, createPanel: function( editor ) { if ( this._.panel ) return; var panelDefinition = this._.panelDefinition, panelBlockDefinition = this._.panelDefinition.block, panelParentElement = panelDefinition.parent || CKEDITOR.document.getBody(), namedPanelCls = 'cke_combopanel__' + this.name, panel = new CKEDITOR.ui.floatPanel( editor, panelParentElement, panelDefinition ), list = panel.addListBlock( this.id, panelBlockDefinition ), me = this; panel.onShow = function() { this.element.addClass( namedPanelCls ); me.setState( CKEDITOR.TRISTATE_ON ); me._.on = 1; me.editorFocus && !editor.focusManager.hasFocus && editor.focus(); if ( me.onOpen ) me.onOpen(); }; panel.onHide = function( preventOnClose ) { this.element.removeClass( namedPanelCls ); me.setState( me.modes && me.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED ); me._.on = 0; if ( !preventOnClose && me.onClose ) me.onClose(); }; panel.onEscape = function() { // Hide drop-down with focus returned. panel.hide( 1 ); }; list.onClick = function( value, marked ) { if ( me.onClick ) me.onClick.call( me, value, marked ); panel.hide(); }; this._.panel = panel; this._.list = list; panel.getBlock( this.id ).onHide = function() { me._.on = 0; me.setState( CKEDITOR.TRISTATE_OFF ); }; if ( this.init ) this.init(); }, setValue: function( value, text ) { this._.value = value; var textElement = this.document.getById( 'cke_' + this.id + '_text' ); if ( textElement ) { if ( !( value || text ) ) { text = this.label; textElement.addClass( 'cke_combo_inlinelabel' ); } else { textElement.removeClass( 'cke_combo_inlinelabel' ); } textElement.setText( typeof text != 'undefined' ? text : value ); } }, getValue: function() { return this._.value || ''; }, unmarkAll: function() { this._.list.unmarkAll(); }, mark: function( value ) { this._.list.mark( value ); }, hideItem: function( value ) { this._.list.hideItem( value ); }, hideGroup: function( groupTitle ) { this._.list.hideGroup( groupTitle ); }, showAll: function() { this._.list.showAll(); }, add: function( value, html, text ) { this._.items[ value ] = text || value; this._.list.add( value, html, text ); }, startGroup: function( title ) { this._.list.startGroup( title ); }, commit: function() { if ( !this._.committed ) { this._.list.commit(); this._.committed = 1; CKEDITOR.ui.fire( 'ready', this ); } this._.committed = 1; }, setState: function( state ) { if ( this._.state == state ) return; var el = this.document.getById( 'cke_' + this.id ); el.setState( state, 'cke_combo' ); state == CKEDITOR.TRISTATE_DISABLED ? el.setAttribute( 'aria-disabled', true ) : el.removeAttribute( 'aria-disabled' ); this._.state = state; }, getState: function() { return this._.state; }, enable: function() { if ( this._.state == CKEDITOR.TRISTATE_DISABLED ) this.setState( this._.lastState ); }, disable: function() { if ( this._.state != CKEDITOR.TRISTATE_DISABLED ) { this._.lastState = this._.state; this.setState( CKEDITOR.TRISTATE_DISABLED ); } }, /** * Removes all listeners from a rich combo element. * * @since 4.11.0 */ destroy: function() { CKEDITOR.tools.array.forEach( this._.listeners, function( listener ) { listener.removeListener(); } ); this._.listeners = []; } }, /** * Represents a rich combo handler object. * * @class CKEDITOR.ui.richCombo.handler * @singleton * @extends CKEDITOR.ui.handlerDefinition */ statics: { handler: { /** * Transforms a rich combo definition into a {@link CKEDITOR.ui.richCombo} instance. * * @param {Object} definition * @returns {CKEDITOR.ui.richCombo} */ create: function( definition ) { return new CKEDITOR.ui.richCombo( definition ); } } } } ); /** * @param {String} name * @param {Object} definition * @member CKEDITOR.ui * @todo */ CKEDITOR.ui.prototype.addRichCombo = function( name, definition ) { this.add( name, CKEDITOR.UI_RICHCOMBO, definition ); }; } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'format', { requires: 'richcombo', // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { if ( editor.blockless ) return; var config = editor.config, lang = editor.lang.format; // Gets the list of tags from the settings. var tags = config.format_tags.split( ';' ); // Create style objects for all defined styles. var styles = {}, stylesCount = 0, allowedContent = []; for ( var i = 0; i < tags.length; i++ ) { var tag = tags[ i ]; var style = new CKEDITOR.style( config[ 'format_' + tag ] ); if ( !editor.filter.customConfig || editor.filter.check( style ) ) { stylesCount++; styles[ tag ] = style; styles[ tag ]._.enterMode = editor.config.enterMode; allowedContent.push( style ); } } // Hide entire combo when all formats are rejected. if ( stylesCount === 0 ) return; editor.ui.addRichCombo( 'Format', { label: lang.label, title: lang.panelTitle, toolbar: 'styles,20', allowedContent: allowedContent, panel: { css: [ CKEDITOR.skin.getPath( 'editor' ) ].concat( config.contentsCss ), multiSelect: false, attributes: { 'aria-label': lang.panelTitle } }, init: function() { this.startGroup( lang.panelTitle ); for ( var tag in styles ) { var label = lang[ 'tag_' + tag ]; // Add the tag entry to the panel list. this.add( tag, styles[ tag ].buildPreview( label ), label ); } }, onClick: function( value ) { editor.focus(); editor.fire( 'saveSnapshot' ); var style = styles[ value ], elementPath = editor.elementPath(); // Always apply style, do not allow to toggle it by clicking on corresponding list item (#584). if ( !style.checkActive( elementPath, editor ) ) { editor.applyStyle( style ); } // Save the undo snapshot after all changes are affected. (https://dev.ckeditor.com/ticket/4899) setTimeout( function() { editor.fire( 'saveSnapshot' ); }, 0 ); }, onRender: function() { editor.on( 'selectionChange', function( ev ) { var currentTag = this.getValue(), elementPath = ev.data.path; this.refresh(); for ( var tag in styles ) { if ( styles[ tag ].checkActive( elementPath, editor ) ) { if ( tag != currentTag ) this.setValue( tag, editor.lang.format[ 'tag_' + tag ] ); return; } } // If no styles match, just empty it. this.setValue( '' ); }, this ); }, onOpen: function() { this.showAll(); for ( var name in styles ) { var style = styles[ name ]; // Check if that style is enabled in activeFilter. if ( !editor.activeFilter.check( style ) ) this.hideItem( name ); } }, refresh: function() { var elementPath = editor.elementPath(); if ( !elementPath ) return; // Check if element path contains 'p' element. if ( !elementPath.isContextFor( 'p' ) ) { this.setState( CKEDITOR.TRISTATE_DISABLED ); return; } // Check if there is any available style. for ( var name in styles ) { if ( editor.activeFilter.check( styles[ name ] ) ) return; } this.setState( CKEDITOR.TRISTATE_DISABLED ); } } ); } } ); /** * A list of semicolon-separated style names (by default: tags) representing * the style definition for each entry to be displayed in the Format drop-down list * in the toolbar. Each entry must have a corresponding configuration in a * setting named `'format_(tagName)'`. For example, the `'p'` entry has its * definition taken from [config.format_p](#!/api/CKEDITOR.config-cfg-format_p). * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_tags = 'p;h2;h3;pre'; * * @cfg {String} [format_tags='p;h1;h2;h3;h4;h5;h6;pre;address;div'] * @member CKEDITOR.config */ CKEDITOR.config.format_tags = 'p;h1;h2;h3;h4;h5;h6;pre;address;div'; /** * The style definition to be used to apply the `Normal` format. * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_p = { element: 'p', attributes: { 'class': 'normalPara' } }; * * @cfg {Object} [format_p={ element: 'p' }] * @member CKEDITOR.config */ CKEDITOR.config.format_p = { element: 'p' }; /** * The style definition to be used to apply the `Normal (DIV)` format. * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_div = { element: 'div', attributes: { 'class': 'normalDiv' } }; * * @cfg {Object} [format_div={ element: 'div' }] * @member CKEDITOR.config */ CKEDITOR.config.format_div = { element: 'div' }; /** * The style definition to be used to apply the `Formatted` format. * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_pre = { element: 'pre', attributes: { 'class': 'code' } }; * * @cfg {Object} [format_pre={ element: 'pre' }] * @member CKEDITOR.config */ CKEDITOR.config.format_pre = { element: 'pre' }; /** * The style definition to be used to apply the `Address` format. * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_address = { element: 'address', attributes: { 'class': 'styledAddress' } }; * * @cfg {Object} [format_address={ element: 'address' }] * @member CKEDITOR.config */ CKEDITOR.config.format_address = { element: 'address' }; /** * The style definition to be used to apply the `Heading 1` format. * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_h1 = { element: 'h1', attributes: { 'class': 'contentTitle1' } }; * * @cfg {Object} [format_h1={ element: 'h1' }] * @member CKEDITOR.config */ CKEDITOR.config.format_h1 = { element: 'h1' }; /** * The style definition to be used to apply the `Heading 2` format. * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_h2 = { element: 'h2', attributes: { 'class': 'contentTitle2' } }; * * @cfg {Object} [format_h2={ element: 'h2' }] * @member CKEDITOR.config */ CKEDITOR.config.format_h2 = { element: 'h2' }; /** * The style definition to be used to apply the `Heading 3` format. * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_h3 = { element: 'h3', attributes: { 'class': 'contentTitle3' } }; * * @cfg {Object} [format_h3={ element: 'h3' }] * @member CKEDITOR.config */ CKEDITOR.config.format_h3 = { element: 'h3' }; /** * The style definition to be used to apply the `Heading 4` format. * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_h4 = { element: 'h4', attributes: { 'class': 'contentTitle4' } }; * * @cfg {Object} [format_h4={ element: 'h4' }] * @member CKEDITOR.config */ CKEDITOR.config.format_h4 = { element: 'h4' }; /** * The style definition to be used to apply the `Heading 5` format. * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_h5 = { element: 'h5', attributes: { 'class': 'contentTitle5' } }; * * @cfg {Object} [format_h5={ element: 'h5' }] * @member CKEDITOR.config */ CKEDITOR.config.format_h5 = { element: 'h5' }; /** * The style definition to be used to apply the `Heading 6` format. * * Read more in the {@glink features/format documentation} * and see the {@glink examples/format example}. * * config.format_h6 = { element: 'h6', attributes: { 'class': 'contentTitle6' } }; * * @cfg {Object} [format_h6={ element: 'h6' }] * @member CKEDITOR.config */ CKEDITOR.config.format_h6 = { element: 'h6' }; /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview Horizontal Rule plugin. */ ( function() { var horizontalruleCmd = { canUndo: false, // The undo snapshot will be handled by 'insertElement'. exec: function( editor ) { var hr = editor.document.createElement( 'hr' ); editor.insertElement( hr ); }, allowedContent: 'hr', requiredContent: 'hr' }; var pluginName = 'horizontalrule'; // Register a plugin named "horizontalrule". CKEDITOR.plugins.add( pluginName, { // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { if ( editor.blockless ) return; editor.addCommand( pluginName, horizontalruleCmd ); editor.ui.addButton && editor.ui.addButton( 'HorizontalRule', { label: editor.lang.horizontalrule.toolbar, command: pluginName, toolbar: 'insert,40' } ); } } ); } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'htmlwriter', { init: function( editor ) { var writer = new CKEDITOR.htmlWriter(); writer.forceSimpleAmpersand = editor.config.forceSimpleAmpersand; writer.indentationChars = editor.config.dataIndentationChars || '\t'; // Overwrite default basicWriter initialized in hmtlDataProcessor constructor. editor.dataProcessor.writer = writer; } } ); /** * The class used to write HTML data. * * var writer = new CKEDITOR.htmlWriter(); * writer.openTag( 'p' ); * writer.attribute( 'class', 'MyClass' ); * writer.openTagClose( 'p' ); * writer.text( 'Hello' ); * writer.closeTag( 'p' ); * alert( writer.getHtml() ); // '

    Hello

    ' * * @class * @extends CKEDITOR.htmlParser.basicWriter */ CKEDITOR.htmlWriter = CKEDITOR.tools.createClass( { base: CKEDITOR.htmlParser.basicWriter, /** * Creates an `htmlWriter` class instance. * * @constructor */ $: function() { // Call the base contructor. this.base(); /** * The characters to be used for each indentation step. * * // Use tab for indentation. * editorInstance.dataProcessor.writer.indentationChars = '\t'; */ this.indentationChars = '\t'; /** * The characters to be used to close "self-closing" elements, like `
    ` or ``. * * // Use HTML4 notation for self-closing elements. * editorInstance.dataProcessor.writer.selfClosingEnd = '>'; */ this.selfClosingEnd = ' />'; /** * The characters to be used for line breaks. * * // Use CRLF for line breaks. * editorInstance.dataProcessor.writer.lineBreakChars = '\r\n'; */ this.lineBreakChars = '\n'; this.sortAttributes = 1; this._.indent = 0; this._.indentation = ''; // Indicate preformatted block context status. (https://dev.ckeditor.com/ticket/5789) this._.inPre = 0; this._.rules = {}; var dtd = CKEDITOR.dtd; for ( var e in CKEDITOR.tools.extend( {}, dtd.$nonBodyContent, dtd.$block, dtd.$listItem, dtd.$tableContent ) ) { this.setRules( e, { indent: !dtd[ e ][ '#' ], breakBeforeOpen: 1, breakBeforeClose: !dtd[ e ][ '#' ], breakAfterClose: 1, needsSpace: ( e in dtd.$block ) && !( e in { li: 1, dt: 1, dd: 1 } ) } ); } this.setRules( 'br', { breakAfterOpen: 1 } ); this.setRules( 'title', { indent: 0, breakAfterOpen: 0 } ); this.setRules( 'style', { indent: 0, breakBeforeClose: 1 } ); this.setRules( 'pre', { breakAfterOpen: 1, // Keep line break after the opening tag indent: 0 // Disable indentation on
    .
    		} );
    	},
    
    	proto: {
    		/**
    		 * Writes the tag opening part for an opener tag.
    		 *
    		 *		// Writes ''.
    		 *		writer.openTagClose( 'p', false );
    		 *
    		 *		// Writes ' />'.
    		 *		writer.openTagClose( 'br', true );
    		 *
    		 * @param {String} tagName The element name for this tag.
    		 * @param {Boolean} isSelfClose Indicates that this is a self-closing tag,
    		 * like `
    ` or ``. */ openTagClose: function( tagName, isSelfClose ) { var rules = this._.rules[ tagName ]; if ( isSelfClose ) { this._.output.push( this.selfClosingEnd ); if ( rules && rules.breakAfterClose ) this._.needsSpace = rules.needsSpace; } else { this._.output.push( '>' ); if ( rules && rules.indent ) this._.indentation += this.indentationChars; } if ( rules && rules.breakAfterOpen ) this.lineBreak(); tagName == 'pre' && ( this._.inPre = 1 ); }, /** * Writes an attribute. This function should be called after opening the * tag with {@link #openTagClose}. * * // Writes ' class="MyClass"'. * writer.attribute( 'class', 'MyClass' ); * * @param {String} attName The attribute name. * @param {String} attValue The attribute value. */ attribute: function( attName, attValue ) { if ( typeof attValue == 'string' ) { // Browsers don't always escape special character in attribute values. (https://dev.ckeditor.com/ticket/4683, https://dev.ckeditor.com/ticket/4719). attValue = CKEDITOR.tools.htmlEncodeAttr( attValue ); // Run ampersand replacement after the htmlEncodeAttr, otherwise the results are overwritten (#965). if ( this.forceSimpleAmpersand ) { attValue = attValue.replace( /&/g, '&' ); } } this._.output.push( ' ', attName, '="', attValue, '"' ); }, /** * Writes a closer tag. * * // Writes '

    '. * writer.closeTag( 'p' ); * * @param {String} tagName The element name for this tag. */ closeTag: function( tagName ) { var rules = this._.rules[ tagName ]; if ( rules && rules.indent ) this._.indentation = this._.indentation.substr( this.indentationChars.length ); if ( this._.indent ) this.indentation(); // Do not break if indenting. else if ( rules && rules.breakBeforeClose ) { this.lineBreak(); this.indentation(); } this._.output.push( '' ); tagName == 'pre' && ( this._.inPre = 0 ); if ( rules && rules.breakAfterClose ) { this.lineBreak(); this._.needsSpace = rules.needsSpace; } this._.afterCloser = 1; }, /** * Writes text. * * // Writes 'Hello Word'. * writer.text( 'Hello Word' ); * * @param {String} text The text value */ text: function( text ) { if ( this._.indent ) { this.indentation(); !this._.inPre && ( text = CKEDITOR.tools.ltrim( text ) ); } this._.output.push( text ); }, /** * Writes a comment. * * // Writes "". * writer.comment( ' My comment ' ); * * @param {String} comment The comment text. */ comment: function( comment ) { if ( this._.indent ) this.indentation(); this._.output.push( '' ); }, /** * Writes a line break. It uses the {@link #lineBreakChars} property for it. * * // Writes '\n' (e.g.). * writer.lineBreak(); */ lineBreak: function() { if ( !this._.inPre && this._.output.length > 0 ) this._.output.push( this.lineBreakChars ); this._.indent = 1; }, /** * Writes the current indentation character. It uses the {@link #indentationChars} * property, repeating it for the current indentation steps. * * // Writes '\t' (e.g.). * writer.indentation(); */ indentation: function() { if ( !this._.inPre && this._.indentation ) this._.output.push( this._.indentation ); this._.indent = 0; }, /** * Empties the current output buffer. It also brings back the default * values of the writer flags. * * writer.reset(); */ reset: function() { this._.output = []; this._.indent = 0; this._.indentation = ''; this._.afterCloser = 0; this._.inPre = 0; this._.needsSpace = 0; }, /** * Sets formatting rules for a given element. Possible rules are: * * * `indent` – indent the element content. * * `breakBeforeOpen` – break line before the opener tag for this element. * * `breakAfterOpen` – break line after the opener tag for this element. * * `breakBeforeClose` – break line before the closer tag for this element. * * `breakAfterClose` – break line after the closer tag for this element. * * All rules default to `false`. Each function call overrides rules that are * already present, leaving the undefined ones untouched. * * By default, all elements available in the {@link CKEDITOR.dtd#$block}, * {@link CKEDITOR.dtd#$listItem}, and {@link CKEDITOR.dtd#$tableContent} * lists have all the above rules set to `true`. Additionaly, the `
    ` * element has the `breakAfterOpen` rule set to `true`. * * // Break line before and after "img" tags. * writer.setRules( 'img', { * breakBeforeOpen: true * breakAfterOpen: true * } ); * * // Reset the rules for the "h1" tag. * writer.setRules( 'h1', {} ); * * @param {String} tagName The name of the element for which the rules are set. * @param {Object} rules An object containing the element rules. */ setRules: function( tagName, rules ) { var currentRules = this._.rules[ tagName ]; if ( currentRules ) CKEDITOR.tools.extend( currentRules, rules, true ); else this._.rules[ tagName ] = rules; } } } ); /** * Whether to force using `'&'` instead of `'&'` in element attributes * values. It is not recommended to change this setting for compliance with the * W3C XHTML 1.0 standards ([C.12, XHTML 1.0](http://www.w3.org/TR/xhtml1/#C_12)). * * // Use `'&'` instead of `'&'` * CKEDITOR.config.forceSimpleAmpersand = true; * * @cfg {Boolean} [forceSimpleAmpersand=false] * @member CKEDITOR.config */ /** * The characters to be used for indenting HTML output produced by the editor. * Using characters different from `' '` (space) and `'\t'` (tab) is not recommended * as it will mess the code. * * // No indentation. * CKEDITOR.config.dataIndentationChars = ''; * * // Use two spaces for indentation. * CKEDITOR.config.dataIndentationChars = ' '; * * @cfg {String} [dataIndentationChars='\t'] * @member CKEDITOR.config */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview The Image plugin. */ ( function() { CKEDITOR.plugins.add( 'image', { requires: 'dialog', // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { var pluginName = 'image'; // Abort when Easyimage or Image2 are to be loaded since this plugins // share the same functionality (#1791). if ( editor.plugins.detectConflict( pluginName, [ 'easyimage', 'image2' ] ) ) { return; } // Register the dialog. CKEDITOR.dialog.add( pluginName, this.path + 'dialogs/image.js' ); var allowed = 'img[alt,!src]{border-style,border-width,float,height,margin,margin-bottom,margin-left,margin-right,margin-top,width}', required = 'img[alt,src]'; if ( CKEDITOR.dialog.isTabEnabled( editor, pluginName, 'advanced' ) ) allowed = 'img[alt,dir,id,lang,longdesc,!src,title]{*}(*)'; // Register the command. editor.addCommand( pluginName, new CKEDITOR.dialogCommand( pluginName, { allowedContent: allowed, requiredContent: required, contentTransformations: [ [ 'img{width}: sizeToStyle', 'img[width]: sizeToAttribute' ], [ 'img{float}: alignmentToStyle', 'img[align]: alignmentToAttribute' ] ] } ) ); // Register the toolbar button. editor.ui.addButton && editor.ui.addButton( 'Image', { label: editor.lang.common.image, command: pluginName, toolbar: 'insert,10' } ); editor.on( 'doubleclick', function( evt ) { var element = evt.data.element; if ( element.is( 'img' ) && !element.data( 'cke-realelement' ) && !element.isReadOnly() ) evt.data.dialog = 'image'; } ); // If the "menu" plugin is loaded, register the menu items. if ( editor.addMenuItems ) { editor.addMenuItems( { image: { label: editor.lang.image.menu, command: 'image', group: 'image' } } ); } // If the "contextmenu" plugin is loaded, register the listeners. if ( editor.contextMenu ) { editor.contextMenu.addListener( function( element ) { if ( getSelectedImage( editor, element ) ) return { image: CKEDITOR.TRISTATE_OFF }; } ); } }, afterInit: function( editor ) { // Abort when Image2 is to be loaded since both plugins // share the same button, command, etc. names (https://dev.ckeditor.com/ticket/11222). if ( editor.plugins.image2 ) return; // Customize the behavior of the alignment commands. (https://dev.ckeditor.com/ticket/7430) setupAlignCommand( 'left' ); setupAlignCommand( 'right' ); setupAlignCommand( 'center' ); setupAlignCommand( 'block' ); function setupAlignCommand( value ) { var command = editor.getCommand( 'justify' + value ); if ( command ) { if ( value == 'left' || value == 'right' ) { command.on( 'exec', function( evt ) { var img = getSelectedImage( editor ), align; if ( img ) { align = getImageAlignment( img ); if ( align == value ) { img.removeStyle( 'float' ); // Remove "align" attribute when necessary. if ( value == getImageAlignment( img ) ) img.removeAttribute( 'align' ); } else { img.setStyle( 'float', value ); } evt.cancel(); } } ); } command.on( 'refresh', function( evt ) { var img = getSelectedImage( editor ), align; if ( img ) { align = getImageAlignment( img ); this.setState( ( align == value ) ? CKEDITOR.TRISTATE_ON : ( value == 'right' || value == 'left' ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED ); evt.cancel(); } } ); } } } } ); function getSelectedImage( editor, element ) { if ( !element ) { var sel = editor.getSelection(); element = sel.getSelectedElement(); } if ( element && element.is( 'img' ) && !element.data( 'cke-realelement' ) && !element.isReadOnly() ) return element; } function getImageAlignment( element ) { var align = element.getStyle( 'float' ); if ( align == 'inherit' || align == 'none' ) align = 0; if ( !align ) align = element.getAttribute( 'align' ); return align; } } )(); /** * Determines whether dimension inputs should be automatically filled when the image URL changes in the Image plugin dialog window. * * config.image_prefillDimensions = false; * * @since 4.5.0 * @cfg {Boolean} [image_prefillDimensions=true] * @member CKEDITOR.config */ /** * Whether to remove links when emptying the link URL field in the Image dialog window. * * config.image_removeLinkByEmptyURL = false; * * @cfg {Boolean} [image_removeLinkByEmptyURL=true] * @member CKEDITOR.config */ CKEDITOR.config.image_removeLinkByEmptyURL = true; /** * Padding text to set off the image in the preview area. * * config.image_previewText = CKEDITOR.tools.repeat( '___ ', 100 ); * * @cfg {String} [image_previewText='Lorem ipsum dolor...' (placeholder text)] * @member CKEDITOR.config */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview Increase and Decrease Indent commands. */ ( function() { 'use strict'; var TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED, TRISTATE_OFF = CKEDITOR.TRISTATE_OFF; CKEDITOR.plugins.add( 'indent', { // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { var genericDefinition = CKEDITOR.plugins.indent.genericDefinition; // Register generic commands. setupGenericListeners( editor, editor.addCommand( 'indent', new genericDefinition( true ) ) ); setupGenericListeners( editor, editor.addCommand( 'outdent', new genericDefinition() ) ); // Create and register toolbar button if possible. if ( editor.ui.addButton ) { editor.ui.addButton( 'Indent', { label: editor.lang.indent.indent, command: 'indent', directional: true, toolbar: 'indent,20' } ); editor.ui.addButton( 'Outdent', { label: editor.lang.indent.outdent, command: 'outdent', directional: true, toolbar: 'indent,10' } ); } // Register dirChanged listener. editor.on( 'dirChanged', function( evt ) { var range = editor.createRange(), dataNode = evt.data.node; range.setStartBefore( dataNode ); range.setEndAfter( dataNode ); var walker = new CKEDITOR.dom.walker( range ), node; while ( ( node = walker.next() ) ) { if ( node.type == CKEDITOR.NODE_ELEMENT ) { // A child with the defined dir is to be ignored. if ( !node.equals( dataNode ) && node.getDirection() ) { range.setStartAfter( node ); walker = new CKEDITOR.dom.walker( range ); continue; } // Switch alignment classes. var classes = editor.config.indentClasses; if ( classes ) { var suffix = ( evt.data.dir == 'ltr' ) ? [ '_rtl', '' ] : [ '', '_rtl' ]; for ( var i = 0; i < classes.length; i++ ) { if ( node.hasClass( classes[ i ] + suffix[ 0 ] ) ) { node.removeClass( classes[ i ] + suffix[ 0 ] ); node.addClass( classes[ i ] + suffix[ 1 ] ); } } } // Switch the margins. var marginLeft = node.getStyle( 'margin-right' ), marginRight = node.getStyle( 'margin-left' ); marginLeft ? node.setStyle( 'margin-left', marginLeft ) : node.removeStyle( 'margin-left' ); marginRight ? node.setStyle( 'margin-right', marginRight ) : node.removeStyle( 'margin-right' ); } } } ); } } ); /** * Global command class definitions and global helpers. * * @class * @singleton */ CKEDITOR.plugins.indent = { /** * A base class for a generic command definition, responsible mainly for creating * Increase Indent and Decrease Indent toolbar buttons as well as for refreshing * UI states. * * Commands of this class do not perform any indentation by themselves. They * delegate this job to content-specific indentation commands (i.e. indentlist). * * @class CKEDITOR.plugins.indent.genericDefinition * @extends CKEDITOR.commandDefinition * @param {CKEDITOR.editor} editor The editor instance this command will be * applied to. * @param {String} name The name of the command. * @param {Boolean} [isIndent] Defines the command as indenting or outdenting. */ genericDefinition: function( isIndent ) { /** * Determines whether the command belongs to the indentation family. * Otherwise it is assumed to be an outdenting command. * * @readonly * @property {Boolean} [=false] */ this.isIndent = !!isIndent; // Mimic naive startDisabled behavior for outdent. this.startDisabled = !this.isIndent; }, /** * A base class for specific indentation command definitions responsible for * handling a pre-defined set of elements i.e. indentlist for lists or * indentblock for text block elements. * * Commands of this class perform indentation operations and modify the DOM structure. * They listen for events fired by {@link CKEDITOR.plugins.indent.genericDefinition} * and execute defined actions. * * **NOTE**: This is not an {@link CKEDITOR.command editor command}. * Context-specific commands are internal, for indentation system only. * * @class CKEDITOR.plugins.indent.specificDefinition * @param {CKEDITOR.editor} editor The editor instance this command will be * applied to. * @param {String} name The name of the command. * @param {Boolean} [isIndent] Defines the command as indenting or outdenting. */ specificDefinition: function( editor, name, isIndent ) { this.name = name; this.editor = editor; /** * An object of jobs handled by the command. Each job consists * of two functions: `refresh` and `exec` as well as the execution priority. * * * The `refresh` function determines whether a job is doable for * a particular context. These functions are executed in the * order of priorities, one by one, for all plugins that registered * jobs. As jobs are related to generic commands, refreshing * occurs when the global command is firing the `refresh` event. * * **Note**: This function must return either {@link CKEDITOR#TRISTATE_DISABLED} * or {@link CKEDITOR#TRISTATE_OFF}. * * * The `exec` function modifies the DOM if possible. Just like * `refresh`, `exec` functions are executed in the order of priorities * while the generic command is executed. This function is not executed * if `refresh` for this job returned {@link CKEDITOR#TRISTATE_DISABLED}. * * **Note**: This function must return a Boolean value, indicating whether it * was successful. If a job was successful, then no other jobs are being executed. * * Sample definition: * * command.jobs = { * // Priority = 20. * '20': { * refresh( editor, path ) { * if ( condition ) * return CKEDITOR.TRISTATE_OFF; * else * return CKEDITOR.TRISTATE_DISABLED; * }, * exec( editor ) { * // DOM modified! This was OK. * return true; * } * }, * // Priority = 60. This job is done later. * '60': { * // Another job. * } * }; * * For additional information, please check comments for * the `setupGenericListeners` function. * * @readonly * @property {Object} [={}] */ this.jobs = {}; /** * Determines whether the editor that the command belongs to has * {@link CKEDITOR.config#enterMode config.enterMode} set to {@link CKEDITOR#ENTER_BR}. * * @readonly * @see CKEDITOR.config#enterMode * @property {Boolean} [=false] */ this.enterBr = editor.config.enterMode == CKEDITOR.ENTER_BR; /** * Determines whether the command belongs to the indentation family. * Otherwise it is assumed to be an outdenting command. * * @readonly * @property {Boolean} [=false] */ this.isIndent = !!isIndent; /** * The name of the global command related to this one. * * @readonly */ this.relatedGlobal = isIndent ? 'indent' : 'outdent'; /** * A keystroke associated with this command (*Tab* or *Shift+Tab*). * * @readonly */ this.indentKey = isIndent ? 9 : CKEDITOR.SHIFT + 9; /** * Stores created markers for the command so they can eventually be * purged after the `exec` function is run. */ this.database = {}; }, /** * Registers content-specific commands as a part of the indentation system * directed by generic commands. Once a command is registered, * it listens for events of a related generic command. * * CKEDITOR.plugins.indent.registerCommands( editor, { * 'indentlist': new indentListCommand( editor, 'indentlist' ), * 'outdentlist': new indentListCommand( editor, 'outdentlist' ) * } ); * * Content-specific commands listen for the generic command's `exec` and * try to execute their own jobs, one after another. If some execution is * successful, `evt.data.done` is set so no more jobs (commands) are involved. * * Content-specific commands also listen for the generic command's `refresh` * and fill the `evt.data.states` object with states of jobs. A generic command * uses this data to determine its own state and to update the UI. * * @member CKEDITOR.plugins.indent * @param {CKEDITOR.editor} editor The editor instance this command is * applied to. * @param {Object} commands An object of {@link CKEDITOR.command}. */ registerCommands: function( editor, commands ) { editor.on( 'pluginsLoaded', function() { for ( var name in commands ) { ( function( editor, command ) { var relatedGlobal = editor.getCommand( command.relatedGlobal ); for ( var priority in command.jobs ) { // Observe generic exec event and execute command when necessary. // If the command was successfully handled by the command and // DOM has been modified, stop event propagation so no other plugin // will bother. Job is done. relatedGlobal.on( 'exec', function( evt ) { if ( evt.data.done ) return; // Make sure that anything this command will do is invisible // for undoManager. What undoManager only can see and // remember is the execution of the global command (relatedGlobal). editor.fire( 'lockSnapshot' ); if ( command.execJob( editor, priority ) ) evt.data.done = true; editor.fire( 'unlockSnapshot' ); // Clean up the markers. CKEDITOR.dom.element.clearAllMarkers( command.database ); }, this, null, priority ); // Observe generic refresh event and force command refresh. // Once refreshed, save command state in event data // so generic command plugin can update its own state and UI. relatedGlobal.on( 'refresh', function( evt ) { if ( !evt.data.states ) evt.data.states = {}; evt.data.states[ command.name + '@' + priority ] = command.refreshJob( editor, priority, evt.data.path ); }, this, null, priority ); } // Since specific indent commands have no UI elements, // they need to be manually registered as a editor feature. editor.addFeature( command ); } )( this, commands[ name ] ); } } ); } }; CKEDITOR.plugins.indent.genericDefinition.prototype = { context: 'p', exec: function() {} }; CKEDITOR.plugins.indent.specificDefinition.prototype = { /** * Executes the content-specific procedure if the context is correct. * It calls the `exec` function of a job of the given `priority` * that modifies the DOM. * * @param {CKEDITOR.editor} editor The editor instance this command * will be applied to. * @param {Number} priority The priority of the job to be executed. * @returns {Boolean} Indicates whether the job was successful. */ execJob: function( editor, priority ) { var job = this.jobs[ priority ]; if ( job.state != TRISTATE_DISABLED ) return job.exec.call( this, editor ); }, /** * Calls the `refresh` function of a job of the given `priority`. * The function returns the state of the job which can be either * {@link CKEDITOR#TRISTATE_DISABLED} or {@link CKEDITOR#TRISTATE_OFF}. * * @param {CKEDITOR.editor} editor The editor instance this command * will be applied to. * @param {Number} priority The priority of the job to be executed. * @returns {Number} The state of the job. */ refreshJob: function( editor, priority, path ) { var job = this.jobs[ priority ]; if ( !editor.activeFilter.checkFeature( this ) ) job.state = TRISTATE_DISABLED; else job.state = job.refresh.call( this, editor, path ); return job.state; }, /** * Checks if the element path contains the element handled * by this indentation command. * * @param {CKEDITOR.dom.elementPath} node A path to be checked. * @returns {CKEDITOR.dom.element} */ getContext: function( path ) { return path.contains( this.context ); } }; /** * Attaches event listeners for this generic command. Since the indentation * system is event-oriented, generic commands communicate with * content-specific commands using the `exec` and `refresh` events. * * Listener priorities are crucial. Different indentation phases * are executed with different priorities. * * For the `exec` event: * * * 0: Selection and bookmarks are saved by the generic command. * * 1-99: Content-specific commands try to indent the code by executing * their own jobs ({@link CKEDITOR.plugins.indent.specificDefinition#jobs}). * * 100: Bookmarks are re-selected by the generic command. * * The visual interpretation looks as follows: * * +------------------+ * | Exec event fired | * +------ + ---------+ * | * 0 -<----------+ Selection and bookmarks saved. * | * | * 25 -<---+ Exec 1st job of plugin#1 (return false, continuing...). * | * | * 50 -<---+ Exec 1st job of plugin#2 (return false, continuing...). * | * | * 75 -<---+ Exec 2nd job of plugin#1 (only if plugin#2 failed). * | * | * 100 -<-----------+ Re-select bookmarks, clean-up. * | * +-------- v ----------+ * | Exec event finished | * +---------------------+ * * For the `refresh` event: * * * <100: Content-specific commands refresh their job states according * to the given path. Jobs save their states in the `evt.data.states` object * passed along with the event. This can be either {@link CKEDITOR#TRISTATE_DISABLED} * or {@link CKEDITOR#TRISTATE_OFF}. * * 100: Command state is determined according to what states * have been returned by content-specific jobs (`evt.data.states`). * UI elements are updated at this stage. * * **Note**: If there is at least one job with the {@link CKEDITOR#TRISTATE_OFF} state, * then the generic command state is also {@link CKEDITOR#TRISTATE_OFF}. Otherwise, * the command state is {@link CKEDITOR#TRISTATE_DISABLED}. * * @param {CKEDITOR.command} command The command to be set up. * @private */ function setupGenericListeners( editor, command ) { var selection, bookmarks; // Set the command state according to content-specific // command states. command.on( 'refresh', function( evt ) { // If no state comes with event data, disable command. var states = [ TRISTATE_DISABLED ]; for ( var s in evt.data.states ) states.push( evt.data.states[ s ] ); this.setState( CKEDITOR.tools.search( states, TRISTATE_OFF ) ? TRISTATE_OFF : TRISTATE_DISABLED ); }, command, null, 100 ); // Initialization. Save bookmarks and mark event as not handled // by any plugin (command) yet. command.on( 'exec', function( evt ) { selection = editor.getSelection(); bookmarks = selection.createBookmarks( 1 ); // Mark execution as not handled yet. if ( !evt.data ) evt.data = {}; evt.data.done = false; }, command, null, 0 ); // Housekeeping. Make sure selectionChange will be called. // Also re-select previously saved bookmarks. command.on( 'exec', function() { editor.forceNextSelectionCheck(); selection.selectBookmarks( bookmarks ); }, command, null, 100 ); } } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview Handles the indentation of lists. */ ( function() { 'use strict'; var isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), isNotBookmark = CKEDITOR.dom.walker.bookmark( false, true ), TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED, TRISTATE_OFF = CKEDITOR.TRISTATE_OFF; CKEDITOR.plugins.add( 'indentlist', { requires: 'indent', init: function( editor ) { var globalHelpers = CKEDITOR.plugins.indent; // Register commands. globalHelpers.registerCommands( editor, { indentlist: new commandDefinition( editor, 'indentlist', true ), outdentlist: new commandDefinition( editor, 'outdentlist' ) } ); function commandDefinition( editor ) { globalHelpers.specificDefinition.apply( this, arguments ); // Require ul OR ol list. this.requiredContent = [ 'ul', 'ol' ]; // Indent and outdent lists with TAB/SHIFT+TAB key. Indenting can // be done for any list item that isn't the first child of the parent. editor.on( 'key', function( evt ) { var path = editor.elementPath(); if ( editor.mode != 'wysiwyg' ) return; if ( evt.data.keyCode == this.indentKey ) { // Prevent of getting context of empty path (#424)(https://dev.ckeditor.com/ticket/17028). if ( !path ) { return; } var list = this.getContext( path ); if ( list ) { // Don't indent if in first list item of the parent. // Outdent, however, can always be done to collapse // the list into a paragraph (div). if ( this.isIndent && CKEDITOR.plugins.indentList.firstItemInPath( this.context, path, list ) ) return; // Exec related global indentation command. Global // commands take care of bookmarks and selection, // so it's much easier to use them instead of // content-specific commands. editor.execCommand( this.relatedGlobal ); // Cancel the key event so editor doesn't lose focus. evt.cancel(); } } }, this ); // There are two different jobs for this plugin: // // * Indent job (priority=10), before indentblock. // // This job is before indentblock because, if this plugin is // loaded it has higher priority over indentblock. It means that, // if possible, nesting is performed, and then block manipulation, // if necessary. // // * Outdent job (priority=30), after outdentblock. // // This job got to be after outdentblock because in some cases // (margin, config#indentClass on list) outdent must be done on // block-level. this.jobs[ this.isIndent ? 10 : 30 ] = { refresh: this.isIndent ? function( editor, path ) { var list = this.getContext( path ), inFirstListItem = CKEDITOR.plugins.indentList.firstItemInPath( this.context, path, list ); if ( !list || !this.isIndent || inFirstListItem ) return TRISTATE_DISABLED; return TRISTATE_OFF; } : function( editor, path ) { var list = this.getContext( path ); if ( !list || this.isIndent ) return TRISTATE_DISABLED; return TRISTATE_OFF; }, exec: CKEDITOR.tools.bind( indentList, this ) }; } CKEDITOR.tools.extend( commandDefinition.prototype, globalHelpers.specificDefinition.prototype, { // Elements that, if in an elementpath, will be handled by this // command. They restrict the scope of the plugin. context: { ol: 1, ul: 1 } } ); } } ); function indentList( editor ) { var that = this, database = this.database, context = this.context, range; function indent( listNode ) { // Our starting and ending points of the range might be inside some blocks under a list item... // So before playing with the iterator, we need to expand the block to include the list items. var startContainer = range.startContainer, endContainer = range.endContainer; while ( startContainer && !startContainer.getParent().equals( listNode ) ) startContainer = startContainer.getParent(); while ( endContainer && !endContainer.getParent().equals( listNode ) ) endContainer = endContainer.getParent(); if ( !startContainer || !endContainer ) return false; // Now we can iterate over the individual items on the same tree depth. var block = startContainer, itemsToMove = [], stopFlag = false; while ( !stopFlag ) { if ( block.equals( endContainer ) ) stopFlag = true; itemsToMove.push( block ); block = block.getNext(); } if ( itemsToMove.length < 1 ) return false; // Do indent or outdent operations on the array model of the list, not the // list's DOM tree itself. The array model demands that it knows as much as // possible about the surrounding lists, we need to feed it the further // ancestor node that is still a list. var listParents = listNode.getParents( true ); for ( var i = 0; i < listParents.length; i++ ) { if ( listParents[ i ].getName && context[ listParents[ i ].getName() ] ) { listNode = listParents[ i ]; break; } } var indentOffset = that.isIndent ? 1 : -1, startItem = itemsToMove[ 0 ], lastItem = itemsToMove[ itemsToMove.length - 1 ], // Convert the list DOM tree into a one dimensional array. listArray = CKEDITOR.plugins.list.listToArray( listNode, database ), // Apply indenting or outdenting on the array. baseIndent = listArray[ lastItem.getCustomData( 'listarray_index' ) ].indent; for ( i = startItem.getCustomData( 'listarray_index' ); i <= lastItem.getCustomData( 'listarray_index' ); i++ ) { listArray[ i ].indent += indentOffset; // Make sure the newly created sublist get a brand-new element of the same type. (https://dev.ckeditor.com/ticket/5372) if ( indentOffset > 0 ) { var listRoot = listArray[ i ].parent; // Find previous list item which has the same indention offset as the new indention offset // of current item to copy its root tag (so the proper list-style-type is used) (#842). for ( var j = i - 1; j >= 0; j-- ) { if ( listArray[ j ].indent === indentOffset ) { listRoot = listArray[ j ].parent; break; } } listArray[ i ].parent = new CKEDITOR.dom.element( listRoot.getName(), listRoot.getDocument() ); } } for ( i = lastItem.getCustomData( 'listarray_index' ) + 1; i < listArray.length && listArray[ i ].indent > baseIndent; i++ ) listArray[ i ].indent += indentOffset; // Convert the array back to a DOM forest (yes we might have a few subtrees now). // And replace the old list with the new forest. var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode, listNode.getDirection() ); // Avoid nested
  • after outdent even they're visually same, // recording them for later refactoring.(https://dev.ckeditor.com/ticket/3982) if ( !that.isIndent ) { var parentLiElement; if ( ( parentLiElement = listNode.getParent() ) && parentLiElement.is( 'li' ) ) { var children = newList.listNode.getChildren(), pendingLis = [], count = children.count(), child; for ( i = count - 1; i >= 0; i-- ) { if ( ( child = children.getItem( i ) ) && child.is && child.is( 'li' ) ) pendingLis.push( child ); } } } if ( newList ) newList.listNode.replace( listNode ); // Move the nested
  • to be appeared after the parent. if ( pendingLis && pendingLis.length ) { for ( i = 0; i < pendingLis.length; i++ ) { var li = pendingLis[ i ], followingList = li; // Nest preceding
      /
        inside current
      1. if any. while ( ( followingList = followingList.getNext() ) && followingList.is && followingList.getName() in context ) { // IE requires a filler NBSP for nested list inside empty list item, // otherwise the list item will be inaccessiable. (https://dev.ckeditor.com/ticket/4476) if ( CKEDITOR.env.needsNbspFiller && !li.getFirst( neitherWhitespacesNorBookmark ) ) li.append( range.document.createText( '\u00a0' ) ); li.append( followingList ); } li.insertAfter( parentLiElement ); } } if ( newList ) editor.fire( 'contentDomInvalidated' ); return true; } var selection = editor.getSelection(), ranges = selection && selection.getRanges(), iterator = ranges.createIterator(); while ( ( range = iterator.getNextRange() ) ) { var nearestListBlock = range.getCommonAncestor(); while ( nearestListBlock && !( nearestListBlock.type == CKEDITOR.NODE_ELEMENT && context[ nearestListBlock.getName() ] ) ) { // Avoid having plugin propagate to parent of editor in inline mode by canceling the indentation. (https://dev.ckeditor.com/ticket/12796) if ( editor.editable().equals( nearestListBlock ) ) { nearestListBlock = false; break; } nearestListBlock = nearestListBlock.getParent(); } // Avoid having selection boundaries out of the list. //
        • [...

        ...]

        =>
        • [...]

        ...

        if ( !nearestListBlock ) { if ( ( nearestListBlock = range.startPath().contains( context ) ) ) range.setEndAt( nearestListBlock, CKEDITOR.POSITION_BEFORE_END ); } // Avoid having selection enclose the entire list. (https://dev.ckeditor.com/ticket/6138) // [
        • ...
        ] =>
        • [...]
        if ( !nearestListBlock ) { var selectedNode = range.getEnclosedNode(); if ( selectedNode && selectedNode.type == CKEDITOR.NODE_ELEMENT && selectedNode.getName() in context ) { range.setStartAt( selectedNode, CKEDITOR.POSITION_AFTER_START ); range.setEndAt( selectedNode, CKEDITOR.POSITION_BEFORE_END ); nearestListBlock = selectedNode; } } // Avoid selection anchors under list root. //
          [
        • ...
        • ]
        =>
        • [...]
        if ( nearestListBlock && range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in context ) { var walker = new CKEDITOR.dom.walker( range ); walker.evaluator = listItem; range.startContainer = walker.next(); } if ( nearestListBlock && range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in context ) { walker = new CKEDITOR.dom.walker( range ); walker.evaluator = listItem; range.endContainer = walker.previous(); } if ( nearestListBlock ) return indent( nearestListBlock ); } return 0; } // Determines whether a node is a list
      2. element. function listItem( node ) { return node.type == CKEDITOR.NODE_ELEMENT && node.is( 'li' ); } function neitherWhitespacesNorBookmark( node ) { return isNotWhitespaces( node ) && isNotBookmark( node ); } /** * Global namespace for methods exposed by the Indent List plugin. * * @singleton * @class */ CKEDITOR.plugins.indentList = {}; /** * Checks whether the first child of the list is in the path. * The list can be extracted from the path or given explicitly * e.g. for better performance if cached. * * @since 4.4.6 * @param {Object} query See the {@link CKEDITOR.dom.elementPath#contains} method arguments. * @param {CKEDITOR.dom.elementPath} path * @param {CKEDITOR.dom.element} [list] * @returns {Boolean} * @member CKEDITOR.plugins.indentList */ CKEDITOR.plugins.indentList.firstItemInPath = function( query, path, list ) { var firstListItemInPath = path.contains( listItem ); if ( !list ) list = path.contains( query ); return list && firstListItemInPath && firstListItemInPath.equals( list.getFirst( listItem ) ); }; } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview Handles the indentation of block elements. */ ( function() { 'use strict'; var $listItem = CKEDITOR.dtd.$listItem, $list = CKEDITOR.dtd.$list, TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED, TRISTATE_OFF = CKEDITOR.TRISTATE_OFF; CKEDITOR.plugins.add( 'indentblock', { requires: 'indent', init: function( editor ) { var globalHelpers = CKEDITOR.plugins.indent, classes = editor.config.indentClasses; // Register commands. globalHelpers.registerCommands( editor, { indentblock: new commandDefinition( editor, 'indentblock', true ), outdentblock: new commandDefinition( editor, 'outdentblock' ) } ); function commandDefinition() { globalHelpers.specificDefinition.apply( this, arguments ); this.allowedContent = { 'div h1 h2 h3 h4 h5 h6 ol p pre ul': { // Do not add elements, but only text-align style if element is validated by other rule. propertiesOnly: true, styles: !classes ? 'margin-left,margin-right' : null, classes: classes || null } }; this.contentTransformations = [ [ 'div: splitMarginShorthand' ], [ 'h1: splitMarginShorthand' ], [ 'h2: splitMarginShorthand' ], [ 'h3: splitMarginShorthand' ], [ 'h4: splitMarginShorthand' ], [ 'h5: splitMarginShorthand' ], [ 'h6: splitMarginShorthand' ], [ 'ol: splitMarginShorthand' ], [ 'p: splitMarginShorthand' ], [ 'pre: splitMarginShorthand' ], [ 'ul: splitMarginShorthand' ] ]; if ( this.enterBr ) this.allowedContent.div = true; this.requiredContent = ( this.enterBr ? 'div' : 'p' ) + ( classes ? '(' + classes.join( ',' ) + ')' : '{margin-left}' ); this.jobs = { '20': { refresh: function( editor, path ) { var firstBlock = path.block || path.blockLimit; // Switch context from somewhere inside list item to list item, // if not found just assign self (doing nothing). if ( !firstBlock.is( $listItem ) ) { var ascendant = firstBlock.getAscendant( $listItem ); firstBlock = ( ascendant && path.contains( ascendant ) ) || firstBlock; } // Switch context from list item to list // because indentblock can indent entire list // but not a single list element. if ( firstBlock.is( $listItem ) ) firstBlock = firstBlock.getParent(); // [-] Context in the path or ENTER_BR // // Don't try to indent if the element is out of // this plugin's scope. This assertion is omitted // if ENTER_BR is in use since there may be no block // in the path. if ( !this.enterBr && !this.getContext( path ) ) return TRISTATE_DISABLED; else if ( classes ) { // [+] Context in the path or ENTER_BR // [+] IndentClasses // // If there are indentation classes, check if reached // the highest level of indentation. If so, disable // the command. if ( indentClassLeft.call( this, firstBlock, classes ) ) return TRISTATE_OFF; else return TRISTATE_DISABLED; } else { // [+] Context in the path or ENTER_BR // [-] IndentClasses // [+] Indenting // // No indent-level limitations due to indent classes. // Indent-like command can always be executed. if ( this.isIndent ) return TRISTATE_OFF; // [+] Context in the path or ENTER_BR // [-] IndentClasses // [-] Indenting // [-] Block in the path // // No block in path. There's no element to apply indentation // so disable the command. else if ( !firstBlock ) return TRISTATE_DISABLED; // [+] Context in the path or ENTER_BR // [-] IndentClasses // [-] Indenting // [+] Block in path. // // Not using indentClasses but there is firstBlock. // We can calculate current indentation level and // try to increase/decrease it. else { return CKEDITOR[ ( getIndent( firstBlock ) || 0 ) <= 0 ? 'TRISTATE_DISABLED' : 'TRISTATE_OFF' ]; } } }, exec: function( editor ) { var selection = editor.getSelection(), range = selection && selection.getRanges()[ 0 ], nearestListBlock; // If there's some list in the path, then it will be // a full-list indent by increasing or decreasing margin property. if ( ( nearestListBlock = editor.elementPath().contains( $list ) ) ) indentElement.call( this, nearestListBlock, classes ); // If no list in the path, use iterator to indent all the possible // paragraphs in the range, creating them if necessary. else { var iterator = range.createIterator(), enterMode = editor.config.enterMode, block; iterator.enforceRealBlocks = true; iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR; while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) ) { if ( !block.isReadOnly() ) indentElement.call( this, block, classes ); } } return true; } } }; } CKEDITOR.tools.extend( commandDefinition.prototype, globalHelpers.specificDefinition.prototype, { // Elements that, if in an elementpath, will be handled by this // command. They restrict the scope of the plugin. context: { div: 1, dl: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, p: 1, pre: 1, table: 1 }, // A regex built on config#indentClasses to detect whether an // element has some indentClass or not. classNameRegex: classes ? new RegExp( '(?:^|\\s+)(' + classes.join( '|' ) + ')(?=$|\\s)' ) : null } ); } } ); // Generic indentation procedure for indentation of any element // either with margin property or config#indentClass. function indentElement( element, classes, dir ) { if ( element.getCustomData( 'indent_processed' ) ) return; var editor = this.editor, isIndent = this.isIndent; if ( classes ) { // Transform current class f to indent step index. var indentClass = element.$.className.match( this.classNameRegex ), indentStep = 0; if ( indentClass ) { indentClass = indentClass[ 1 ]; indentStep = CKEDITOR.tools.indexOf( classes, indentClass ) + 1; } // Operate on indent step index, transform indent step index // back to class name. if ( ( indentStep += isIndent ? 1 : -1 ) < 0 ) return; indentStep = Math.min( indentStep, classes.length ); indentStep = Math.max( indentStep, 0 ); element.$.className = CKEDITOR.tools.ltrim( element.$.className.replace( this.classNameRegex, '' ) ); if ( indentStep > 0 ) element.addClass( classes[ indentStep - 1 ] ); } else { var indentCssProperty = getIndentCss( element, dir ), currentOffset = parseInt( element.getStyle( indentCssProperty ), 10 ), indentOffset = editor.config.indentOffset || 40; if ( isNaN( currentOffset ) ) currentOffset = 0; currentOffset += ( isIndent ? 1 : -1 ) * indentOffset; if ( currentOffset < 0 ) return; currentOffset = Math.max( currentOffset, 0 ); currentOffset = Math.ceil( currentOffset / indentOffset ) * indentOffset; element.setStyle( indentCssProperty, currentOffset ? currentOffset + ( editor.config.indentUnit || 'px' ) : '' ); if ( element.getAttribute( 'style' ) === '' ) element.removeAttribute( 'style' ); } CKEDITOR.dom.element.setMarker( this.database, element, 'indent_processed', 1 ); return; } // Method that checks if current indentation level for an element // reached the limit determined by config#indentClasses. function indentClassLeft( node, classes ) { var indentClass = node.$.className.match( this.classNameRegex ), isIndent = this.isIndent; // If node has one of the indentClasses: // * If it holds the topmost indentClass, then // no more classes have left. // * If it holds any other indentClass, it can use the next one // or the previous one. // * Outdent is always possible. We can remove indentClass. if ( indentClass ) return isIndent ? indentClass[ 1 ] != classes.slice( -1 ) : true; // If node has no class which belongs to indentClasses, // then it is at 0-level. It can be indented but not outdented. else return isIndent; } // Determines indent CSS property for an element according to // what is the direction of such element. It can be either `margin-left` // or `margin-right`. function getIndentCss( element, dir ) { return ( dir || element.getComputedStyle( 'direction' ) ) == 'ltr' ? 'margin-left' : 'margin-right'; } // Return the numerical indent value of margin-left|right of an element, // considering element's direction. If element has no margin specified, // NaN is returned. function getIndent( element ) { return parseInt( element.getStyle( getIndentCss( element ) ), 10 ); } } )(); /** * A list of classes to use for indenting the contents. If set to `null`, no classes will be used * and instead the {@link #indentUnit} and {@link #indentOffset} properties will be used. * * // Use the 'Indent1', 'Indent2', 'Indent3' classes. * config.indentClasses = ['Indent1', 'Indent2', 'Indent3']; * * @cfg {Array} [indentClasses=null] * @member CKEDITOR.config */ /** * The size in {@link CKEDITOR.config#indentUnit indentation units} of each indentation step. * * config.indentOffset = 4; * * @cfg {Number} [indentOffset=40] * @member CKEDITOR.config */ /** * The unit used for {@link CKEDITOR.config#indentOffset indentation offset}. * * config.indentUnit = 'em'; * * @cfg {String} [indentUnit='px'] * @member CKEDITOR.config */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview Justify commands. */ ( function() { function getAlignment( element, useComputedState ) { useComputedState = useComputedState === undefined || useComputedState; var align; if ( useComputedState ) align = element.getComputedStyle( 'text-align' ); else { while ( !element.hasAttribute || !( element.hasAttribute( 'align' ) || element.getStyle( 'text-align' ) ) ) { var parent = element.getParent(); if ( !parent ) break; element = parent; } align = element.getStyle( 'text-align' ) || element.getAttribute( 'align' ) || ''; } // Sometimes computed values doesn't tell. align && ( align = align.replace( /(?:-(?:moz|webkit)-)?(?:start|auto)/i, '' ) ); !align && useComputedState && ( align = element.getComputedStyle( 'direction' ) == 'rtl' ? 'right' : 'left' ); return align; } function justifyCommand( editor, name, value ) { this.editor = editor; this.name = name; this.value = value; this.context = 'p'; var classes = editor.config.justifyClasses, blockTag = editor.config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div'; if ( classes ) { switch ( value ) { case 'left': this.cssClassName = classes[ 0 ]; break; case 'center': this.cssClassName = classes[ 1 ]; break; case 'right': this.cssClassName = classes[ 2 ]; break; case 'justify': this.cssClassName = classes[ 3 ]; break; } this.cssClassRegex = new RegExp( '(?:^|\\s+)(?:' + classes.join( '|' ) + ')(?=$|\\s)' ); this.requiredContent = blockTag + '(' + this.cssClassName + ')'; } else { this.requiredContent = blockTag + '{text-align}'; } this.allowedContent = { 'caption div h1 h2 h3 h4 h5 h6 p pre td th li': { // Do not add elements, but only text-align style if element is validated by other rule. propertiesOnly: true, styles: this.cssClassName ? null : 'text-align', classes: this.cssClassName || null } }; // In enter mode BR we need to allow here for div, because when non other // feature allows div justify is the only plugin that uses it. if ( editor.config.enterMode == CKEDITOR.ENTER_BR ) this.allowedContent.div = true; } function onDirChanged( e ) { var editor = e.editor; var range = editor.createRange(); range.setStartBefore( e.data.node ); range.setEndAfter( e.data.node ); var walker = new CKEDITOR.dom.walker( range ), node; while ( ( node = walker.next() ) ) { if ( node.type == CKEDITOR.NODE_ELEMENT ) { // A child with the defined dir is to be ignored. if ( !node.equals( e.data.node ) && node.getDirection() ) { range.setStartAfter( node ); walker = new CKEDITOR.dom.walker( range ); continue; } // Switch the alignment. var classes = editor.config.justifyClasses; if ( classes ) { // The left align class. if ( node.hasClass( classes[ 0 ] ) ) { node.removeClass( classes[ 0 ] ); node.addClass( classes[ 2 ] ); } // The right align class. else if ( node.hasClass( classes[ 2 ] ) ) { node.removeClass( classes[ 2 ] ); node.addClass( classes[ 0 ] ); } } // Always switch CSS margins. var style = 'text-align'; var align = node.getStyle( style ); if ( align == 'left' ) node.setStyle( style, 'right' ); else if ( align == 'right' ) node.setStyle( style, 'left' ); } } } justifyCommand.prototype = { exec: function( editor ) { var selection = editor.getSelection(), enterMode = editor.config.enterMode; if ( !selection ) return; var bookmarks = selection.createBookmarks(), ranges = selection.getRanges(); var cssClassName = this.cssClassName, iterator, block; var useComputedState = editor.config.useComputedState; useComputedState = useComputedState === undefined || useComputedState; for ( var i = ranges.length - 1; i >= 0; i-- ) { iterator = ranges[ i ].createIterator(); iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR; while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) ) { if ( block.isReadOnly() ) continue; // Check if style or class might be applied to currently processed element (#455). var tag = block.getName(), isAllowedTextAlign, isAllowedCssClass; isAllowedTextAlign = editor.activeFilter.check( tag + '{text-align}' ); isAllowedCssClass = editor.activeFilter.check( tag + '(' + cssClassName + ')' ); if ( !isAllowedCssClass && !isAllowedTextAlign ) { continue; } block.removeAttribute( 'align' ); block.removeStyle( 'text-align' ); // Remove any of the alignment classes from the className. var className = cssClassName && ( block.$.className = CKEDITOR.tools.ltrim( block.$.className.replace( this.cssClassRegex, '' ) ) ); var apply = ( this.state == CKEDITOR.TRISTATE_OFF ) && ( !useComputedState || ( getAlignment( block, true ) != this.value ) ); if ( cssClassName && isAllowedCssClass ) { // Append the desired class name. if ( apply ) block.addClass( cssClassName ); else if ( !className ) block.removeAttribute( 'class' ); } else if ( apply && isAllowedTextAlign ) { block.setStyle( 'text-align', this.value ); } } } editor.focus(); editor.forceNextSelectionCheck(); selection.selectBookmarks( bookmarks ); }, refresh: function( editor, path ) { var firstBlock = path.block || path.blockLimit, name = firstBlock.getName(), isEditable = firstBlock.equals( editor.editable() ), isStylable = this.cssClassName ? editor.activeFilter.check( name + '(' + this.cssClassName + ')' ) : editor.activeFilter.check( name + '{text-align}' ); // #455 // 1. Check if we are directly in editbale. Justification should be always allowed, and not highlighted. // Checking situation `body > ul` where ul is selected and path.blockLimit returns editable. // 2. Check if current element can have applied specific class. // 3. Check if current element can have applied text-align style. if ( isEditable && !CKEDITOR.dtd.$list[ path.lastElement.getName() ] ) { this.setState( CKEDITOR.TRISTATE_OFF ); } else if ( !isEditable && isStylable ) { // 2 & 3 in one condition. this.setState( getAlignment( firstBlock, this.editor.config.useComputedState ) == this.value ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF ); } else { this.setState( CKEDITOR.TRISTATE_DISABLED ); } } }; CKEDITOR.plugins.add( 'justify', { init: function( editor ) { if ( editor.blockless ) return; var left = new justifyCommand( editor, 'justifyleft', 'left' ), center = new justifyCommand( editor, 'justifycenter', 'center' ), right = new justifyCommand( editor, 'justifyright', 'right' ), justify = new justifyCommand( editor, 'justifyblock', 'justify' ); editor.addCommand( 'justifyleft', left ); editor.addCommand( 'justifycenter', center ); editor.addCommand( 'justifyright', right ); editor.addCommand( 'justifyblock', justify ); if ( editor.ui.addButton ) { editor.ui.addButton( 'JustifyLeft', { label: editor.lang.common.alignLeft, command: 'justifyleft', toolbar: 'align,10' } ); editor.ui.addButton( 'JustifyCenter', { label: editor.lang.common.center, command: 'justifycenter', toolbar: 'align,20' } ); editor.ui.addButton( 'JustifyRight', { label: editor.lang.common.alignRight, command: 'justifyright', toolbar: 'align,30' } ); editor.ui.addButton( 'JustifyBlock', { label: editor.lang.common.justify, command: 'justifyblock', toolbar: 'align,40' } ); } editor.on( 'dirChanged', onDirChanged ); } } ); } )(); /** * List of classes to use for aligning the contents. If it's `null`, no classes will be used * and instead the corresponding CSS values will be used. * * The array should contain 4 members, in the following order: left, center, right, justify. * * // Use the classes 'AlignLeft', 'AlignCenter', 'AlignRight', 'AlignJustify' * config.justifyClasses = [ 'AlignLeft', 'AlignCenter', 'AlignRight', 'AlignJustify' ]; * * @cfg {Array} [justifyClasses=null] * @member CKEDITOR.config */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ ( function() { var cssStyle = CKEDITOR.htmlParser.cssStyle, cssLength = CKEDITOR.tools.cssLength; var cssLengthRegex = /^((?:\d*(?:\.\d+))|(?:\d+))(.*)?$/i; // Replacing the former CSS length value with the later one, with // adjustment to the length unit. function replaceCssLength( length1, length2 ) { var parts1 = cssLengthRegex.exec( length1 ), parts2 = cssLengthRegex.exec( length2 ); // Omit pixel length unit when necessary, // e.g. replaceCssLength( 10, '20px' ) -> 20 if ( parts1 ) { if ( !parts1[ 2 ] && parts2[ 2 ] == 'px' ) return parts2[ 1 ]; if ( parts1[ 2 ] == 'px' && !parts2[ 2 ] ) return parts2[ 1 ] + 'px'; } return length2; } var htmlFilterRules = { elements: { $: function( element ) { var attributes = element.attributes, realHtml = attributes && attributes[ 'data-cke-realelement' ], realFragment = realHtml && new CKEDITOR.htmlParser.fragment.fromHtml( decodeURIComponent( realHtml ) ), realElement = realFragment && realFragment.children[ 0 ]; // Width/height in the fake object are subjected to clone into the real element. if ( realElement && element.attributes[ 'data-cke-resizable' ] ) { var styles = new cssStyle( element ).rules, realAttrs = realElement.attributes, width = styles.width, height = styles.height; width && ( realAttrs.width = replaceCssLength( realAttrs.width, width ) ); height && ( realAttrs.height = replaceCssLength( realAttrs.height, height ) ); } return realElement; } } }; CKEDITOR.plugins.add( 'fakeobjects', { // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { // Allow image with all styles and classes plus src, alt and title attributes. // We need them when fakeobject is pasted. editor.filter.allow( 'img[!data-cke-realelement,src,alt,title](*){*}', 'fakeobjects' ); }, afterInit: function( editor ) { var dataProcessor = editor.dataProcessor, htmlFilter = dataProcessor && dataProcessor.htmlFilter; if ( htmlFilter ) { htmlFilter.addRules( htmlFilterRules, { applyToAll: true } ); } } } ); /** * Creates fake {@link CKEDITOR.dom.element} based on real element. * Fake element is an img with special attributes, which keep real element properties. * * @member CKEDITOR.editor * @param {CKEDITOR.dom.element} realElement Real element to transform. * @param {String} className Class name which will be used as class of fake element. * @param {String} realElementType Stores type of fake element. * @param {Boolean} isResizable Keeps information if element is resizable. * @returns {CKEDITOR.dom.element} Fake element. */ CKEDITOR.editor.prototype.createFakeElement = function( realElement, className, realElementType, isResizable ) { var lang = this.lang.fakeobjects, label = lang[ realElementType ] || lang.unknown; var attributes = { 'class': className, 'data-cke-realelement': encodeURIComponent( realElement.getOuterHtml() ), 'data-cke-real-node-type': realElement.type, alt: label, title: label, align: realElement.getAttribute( 'align' ) || '' }; // Do not set "src" on high-contrast so the alt text is displayed. (https://dev.ckeditor.com/ticket/8945) if ( !CKEDITOR.env.hc ) attributes.src = CKEDITOR.tools.transparentImageData; if ( realElementType ) attributes[ 'data-cke-real-element-type' ] = realElementType; if ( isResizable ) { attributes[ 'data-cke-resizable' ] = isResizable; var fakeStyle = new cssStyle(); var width = realElement.getAttribute( 'width' ), height = realElement.getAttribute( 'height' ); width && ( fakeStyle.rules.width = cssLength( width ) ); height && ( fakeStyle.rules.height = cssLength( height ) ); fakeStyle.populate( attributes ); } return this.document.createElement( 'img', { attributes: attributes } ); }; /** * Creates fake {@link CKEDITOR.htmlParser.element} based on real element. * * @member CKEDITOR.editor * @param {CKEDITOR.dom.element} realElement Real element to transform. * @param {String} className Class name which will be used as class of fake element. * @param {String} realElementType Store type of fake element. * @param {Boolean} isResizable Keep information if element is resizable. * @returns {CKEDITOR.htmlParser.element} Fake htmlParser element. */ CKEDITOR.editor.prototype.createFakeParserElement = function( realElement, className, realElementType, isResizable ) { var lang = this.lang.fakeobjects, label = lang[ realElementType ] || lang.unknown, html; var writer = new CKEDITOR.htmlParser.basicWriter(); realElement.writeHtml( writer ); html = writer.getHtml(); var attributes = { 'class': className, 'data-cke-realelement': encodeURIComponent( html ), 'data-cke-real-node-type': realElement.type, alt: label, title: label, align: realElement.attributes.align || '' }; // Do not set "src" on high-contrast so the alt text is displayed. (https://dev.ckeditor.com/ticket/8945) if ( !CKEDITOR.env.hc ) attributes.src = CKEDITOR.tools.transparentImageData; if ( realElementType ) attributes[ 'data-cke-real-element-type' ] = realElementType; if ( isResizable ) { attributes[ 'data-cke-resizable' ] = isResizable; var realAttrs = realElement.attributes, fakeStyle = new cssStyle(); var width = realAttrs.width, height = realAttrs.height; width !== undefined && ( fakeStyle.rules.width = cssLength( width ) ); height !== undefined && ( fakeStyle.rules.height = cssLength( height ) ); fakeStyle.populate( attributes ); } return new CKEDITOR.htmlParser.element( 'img', attributes ); }; /** * Creates {@link CKEDITOR.dom.element} from fake element. * * @member CKEDITOR.editor * @param {CKEDITOR.dom.element} fakeElement Fake element to transform. * @returns {CKEDITOR.dom.element/null} Returns real element or `null` if transformed element wasn't fake. */ CKEDITOR.editor.prototype.restoreRealElement = function( fakeElement ) { if ( fakeElement.data( 'cke-real-node-type' ) != CKEDITOR.NODE_ELEMENT ) return null; var element = CKEDITOR.dom.element.createFromHtml( decodeURIComponent( fakeElement.data( 'cke-realelement' ) ), this.document ); if ( fakeElement.data( 'cke-resizable' ) ) { var width = fakeElement.getStyle( 'width' ), height = fakeElement.getStyle( 'height' ); width && element.setAttribute( 'width', replaceCssLength( element.getAttribute( 'width' ), width ) ); height && element.setAttribute( 'height', replaceCssLength( element.getAttribute( 'height' ), height ) ); } return element; }; } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ 'use strict'; ( function() { CKEDITOR.plugins.add( 'link', { requires: 'dialog,fakeobjects', // jscs:disable maximumLineLength // jscs:enable maximumLineLength onLoad: function() { // Add the CSS styles for anchor placeholders. var iconPath = CKEDITOR.getUrl( this.path + 'images' + ( CKEDITOR.env.hidpi ? '/hidpi' : '' ) + '/anchor.png' ), baseStyle = 'background:url(' + iconPath + ') no-repeat %1 center;border:1px dotted #00f;background-size:16px;'; var template = '.%2 a.cke_anchor,' + '.%2 a.cke_anchor_empty' + ',.cke_editable.%2 a[name]' + ',.cke_editable.%2 a[data-cke-saved-name]' + '{' + baseStyle + 'padding-%1:18px;' + // Show the arrow cursor for the anchor image (FF at least). 'cursor:auto;' + '}' + '.%2 img.cke_anchor' + '{' + baseStyle + 'width:16px;' + 'min-height:15px;' + // The default line-height on IE. 'height:1.15em;' + // Opera works better with "middle" (even if not perfect) 'vertical-align:text-bottom;' + '}'; // Styles with contents direction awareness. function cssWithDir( dir ) { return template.replace( /%1/g, dir == 'rtl' ? 'right' : 'left' ).replace( /%2/g, 'cke_contents_' + dir ); } CKEDITOR.addCss( cssWithDir( 'ltr' ) + cssWithDir( 'rtl' ) ); }, init: function( editor ) { var allowed = 'a[!href]', required = 'a[href]'; if ( CKEDITOR.dialog.isTabEnabled( editor, 'link', 'advanced' ) ) allowed = allowed.replace( ']', ',accesskey,charset,dir,id,lang,name,rel,tabindex,title,type,download]{*}(*)' ); if ( CKEDITOR.dialog.isTabEnabled( editor, 'link', 'target' ) ) allowed = allowed.replace( ']', ',target,onclick]' ); // Add the link and unlink buttons. editor.addCommand( 'link', new CKEDITOR.dialogCommand( 'link', { allowedContent: allowed, requiredContent: required } ) ); editor.addCommand( 'anchor', new CKEDITOR.dialogCommand( 'anchor', { allowedContent: 'a[!name,id]', requiredContent: 'a[name]' } ) ); editor.addCommand( 'unlink', new CKEDITOR.unlinkCommand() ); editor.addCommand( 'removeAnchor', new CKEDITOR.removeAnchorCommand() ); editor.setKeystroke( CKEDITOR.CTRL + 76 /*L*/, 'link' ); // (#2478) editor.setKeystroke( CKEDITOR.CTRL + 75 /*K*/, 'link' ); if ( editor.ui.addButton ) { editor.ui.addButton( 'Link', { label: editor.lang.link.toolbar, command: 'link', toolbar: 'links,10' } ); editor.ui.addButton( 'Unlink', { label: editor.lang.link.unlink, command: 'unlink', toolbar: 'links,20' } ); editor.ui.addButton( 'Anchor', { label: editor.lang.link.anchor.toolbar, command: 'anchor', toolbar: 'links,30' } ); } CKEDITOR.dialog.add( 'link', this.path + 'dialogs/link.js' ); CKEDITOR.dialog.add( 'anchor', this.path + 'dialogs/anchor.js' ); editor.on( 'doubleclick', function( evt ) { // If the link has descendants and the last part of it is also a part of a word partially // unlinked, clicked element may be a descendant of the link, not the link itself (https://dev.ckeditor.com/ticket/11956). // The evt.data.element.getAscendant( 'img', 1 ) condition allows opening anchor dialog if the anchor is empty (#501). var element = evt.data.element.getAscendant( { a: 1, img: 1 }, true ); if ( element && !element.isReadOnly() ) { if ( element.is( 'a' ) ) { evt.data.dialog = ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) ? 'anchor' : 'link'; // Pass the link to be selected along with event data. evt.data.link = element; } else if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) ) { evt.data.dialog = 'anchor'; } } }, null, null, 0 ); // If event was cancelled, link passed in event data will not be selected. editor.on( 'doubleclick', function( evt ) { // Make sure both links and anchors are selected (https://dev.ckeditor.com/ticket/11822). if ( evt.data.dialog in { link: 1, anchor: 1 } && evt.data.link ) editor.getSelection().selectElement( evt.data.link ); }, null, null, 20 ); // If the "menu" plugin is loaded, register the menu items. if ( editor.addMenuItems ) { editor.addMenuItems( { anchor: { label: editor.lang.link.anchor.menu, command: 'anchor', group: 'anchor', order: 1 }, removeAnchor: { label: editor.lang.link.anchor.remove, command: 'removeAnchor', group: 'anchor', order: 5 }, link: { label: editor.lang.link.menu, command: 'link', group: 'link', order: 1 }, unlink: { label: editor.lang.link.unlink, command: 'unlink', group: 'link', order: 5 } } ); } // If the "contextmenu" plugin is loaded, register the listeners. if ( editor.contextMenu ) { editor.contextMenu.addListener( function( element ) { if ( !element || element.isReadOnly() ) return null; var anchor = CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ); if ( !anchor && !( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) ) return null; var menu = {}; if ( anchor.getAttribute( 'href' ) && anchor.getChildCount() ) menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF }; if ( anchor && anchor.hasAttribute( 'name' ) ) menu.anchor = menu.removeAnchor = CKEDITOR.TRISTATE_OFF; return menu; } ); } this.compiledProtectionFunction = getCompiledProtectionFunction( editor ); }, afterInit: function( editor ) { // Empty anchors upcasting to fake objects. editor.dataProcessor.dataFilter.addRules( { elements: { a: function( element ) { if ( !element.attributes.name ) return null; if ( !element.children.length ) return editor.createFakeParserElement( element, 'cke_anchor', 'anchor' ); return null; } } } ); var pathFilters = editor._.elementsPath && editor._.elementsPath.filters; if ( pathFilters ) { pathFilters.push( function( element, name ) { if ( name == 'a' ) { if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) || ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) ) return 'anchor'; } } ); } } } ); // Loads the parameters in a selected link to the link dialog fields. var javascriptProtocolRegex = /^javascript:/, emailRegex = /^mailto:([^?]+)(?:\?(.+))?$/, emailSubjectRegex = /subject=([^;?:@&=$,\/]*)/i, emailBodyRegex = /body=([^;?:@&=$,\/]*)/i, anchorRegex = /^#(.*)$/, urlRegex = /^((?:http|https|ftp|news):\/\/)?(.*)$/, selectableTargets = /^(_(?:self|top|parent|blank))$/, encodedEmailLinkRegex = /^javascript:void\(location\.href='mailto:'\+String\.fromCharCode\(([^)]+)\)(?:\+'(.*)')?\)$/, functionCallProtectedEmailLinkRegex = /^javascript:([^(]+)\(([^)]+)\)$/, popupRegex = /\s*window.open\(\s*this\.href\s*,\s*(?:'([^']*)'|null)\s*,\s*'([^']*)'\s*\)\s*;\s*return\s*false;*\s*/, popupFeaturesRegex = /(?:^|,)([^=]+)=(\d+|yes|no)/gi, telRegex = /^tel:(.*)$/; var advAttrNames = { id: 'advId', dir: 'advLangDir', accessKey: 'advAccessKey', // 'data-cke-saved-name': 'advName', name: 'advName', lang: 'advLangCode', tabindex: 'advTabIndex', title: 'advTitle', type: 'advContentType', 'class': 'advCSSClasses', charset: 'advCharset', style: 'advStyles', rel: 'advRel' }; function unescapeSingleQuote( str ) { return str.replace( /\\'/g, '\'' ); } function escapeSingleQuote( str ) { return str.replace( /'/g, '\\$&' ); } function protectEmailAddressAsEncodedString( address ) { var charCode, length = address.length, encodedChars = []; for ( var i = 0; i < length; i++ ) { charCode = address.charCodeAt( i ); encodedChars.push( charCode ); } return 'String.fromCharCode(' + encodedChars.join( ',' ) + ')'; } function protectEmailLinkAsFunction( editor, email ) { var plugin = editor.plugins.link, name = plugin.compiledProtectionFunction.name, params = plugin.compiledProtectionFunction.params, paramName, paramValue, retval; retval = [ name, '(' ]; for ( var i = 0; i < params.length; i++ ) { paramName = params[ i ].toLowerCase(); paramValue = email[ paramName ]; i > 0 && retval.push( ',' ); retval.push( '\'', paramValue ? escapeSingleQuote( encodeURIComponent( email[ paramName ] ) ) : '', '\'' ); } retval.push( ')' ); return retval.join( '' ); } function getCompiledProtectionFunction( editor ) { var emailProtection = editor.config.emailProtection || '', compiledProtectionFunction; // Compile the protection function pattern. if ( emailProtection && emailProtection != 'encode' ) { compiledProtectionFunction = {}; emailProtection.replace( /^([^(]+)\(([^)]+)\)$/, function( match, funcName, params ) { compiledProtectionFunction.name = funcName; compiledProtectionFunction.params = []; params.replace( /[^,\s]+/g, function( param ) { compiledProtectionFunction.params.push( param ); } ); } ); } return compiledProtectionFunction; } /** * Set of Link plugin helpers. * * @class * @singleton */ CKEDITOR.plugins.link = { /** * Get the surrounding link element of the current selection. * * CKEDITOR.plugins.link.getSelectedLink( editor ); * * // The following selections will all return the link element. * * li^nk * [link] * text[link] * li[nk] * [li]nk] * [li]nk * * @since 3.2.1 * @param {CKEDITOR.editor} editor * @param {Boolean} [returnMultiple=false] Indicates whether the function should return only the first selected link or all of them. * @returns {CKEDITOR.dom.element/CKEDITOR.dom.element[]/null} A single link element or an array of link * elements relevant to the current selection. */ getSelectedLink: function( editor, returnMultiple ) { var selection = editor.getSelection(), selectedElement = selection.getSelectedElement(), ranges = selection.getRanges(), links = [], link, range, i; if ( !returnMultiple && selectedElement && selectedElement.is( 'a' ) ) { return selectedElement; } for ( i = 0; i < ranges.length; i++ ) { range = selection.getRanges()[ i ]; // Skip bogus to cover cases of multiple selection inside tables (#tp2245). // Shrink to element to prevent losing anchor (#859). range.shrink( CKEDITOR.SHRINK_ELEMENT, true, { skipBogus: true } ); link = editor.elementPath( range.getCommonAncestor() ).contains( 'a', 1 ); if ( link && returnMultiple ) { links.push( link ); } else if ( link ) { return link; } } return returnMultiple ? links : null; }, /** * Collects anchors available in the editor (i.e. used by the Link plugin). * Note that the scope of search is different for inline (the "global" document) and * classic (`iframe`-based) editors (the "inner" document). * * @since 4.3.3 * @param {CKEDITOR.editor} editor * @returns {CKEDITOR.dom.element[]} An array of anchor elements. */ getEditorAnchors: function( editor ) { var editable = editor.editable(), // The scope of search for anchors is the entire document for inline editors // and editor's editable for classic editor/divarea (https://dev.ckeditor.com/ticket/11359). scope = ( editable.isInline() && !editor.plugins.divarea ) ? editor.document : editable, links = scope.getElementsByTag( 'a' ), imgs = scope.getElementsByTag( 'img' ), anchors = [], i = 0, item; // Retrieve all anchors within the scope. while ( ( item = links.getItem( i++ ) ) ) { if ( item.data( 'cke-saved-name' ) || item.hasAttribute( 'name' ) ) { anchors.push( { name: item.data( 'cke-saved-name' ) || item.getAttribute( 'name' ), id: item.getAttribute( 'id' ) } ); } } // Retrieve all "fake anchors" within the scope. i = 0; while ( ( item = imgs.getItem( i++ ) ) ) { if ( ( item = this.tryRestoreFakeAnchor( editor, item ) ) ) { anchors.push( { name: item.getAttribute( 'name' ), id: item.getAttribute( 'id' ) } ); } } return anchors; }, /** * Opera and WebKit do not make it possible to select empty anchors. Fake * elements must be used for them. * * @readonly * @deprecated 4.3.3 It is set to `true` in every browser. * @property {Boolean} */ fakeAnchor: true, /** * For browsers that do not support CSS3 `a[name]:empty()`. Note that IE9 is included because of https://dev.ckeditor.com/ticket/7783. * * @readonly * @deprecated 4.3.3 It is set to `false` in every browser. * @property {Boolean} synAnchorSelector */ /** * For browsers that have editing issues with an empty anchor. * * @readonly * @deprecated 4.3.3 It is set to `false` in every browser. * @property {Boolean} emptyAnchorFix */ /** * Returns an element representing a real anchor restored from a fake anchor. * * @param {CKEDITOR.editor} editor * @param {CKEDITOR.dom.element} element * @returns {CKEDITOR.dom.element} Restored anchor element or nothing if the * passed element was not a fake anchor. */ tryRestoreFakeAnchor: function( editor, element ) { if ( element && element.data( 'cke-real-element-type' ) && element.data( 'cke-real-element-type' ) == 'anchor' ) { var link = editor.restoreRealElement( element ); if ( link.data( 'cke-saved-name' ) ) return link; } }, /** * Parses attributes of the link element and returns an object representing * the current state (data) of the link. This data format is a plain object accepted * e.g. by the Link dialog window and {@link #getLinkAttributes}. * * **Note:** Data model format produced by the parser must be compatible with the Link * plugin dialog because it is passed directly to {@link CKEDITOR.dialog#setupContent}. * * @since 4.4.0 * @param {CKEDITOR.editor} editor * @param {CKEDITOR.dom.element} element * @returns {Object} An object of link data. */ parseLinkAttributes: function( editor, element ) { var href = ( element && ( element.data( 'cke-saved-href' ) || element.getAttribute( 'href' ) ) ) || '', compiledProtectionFunction = editor.plugins.link.compiledProtectionFunction, emailProtection = editor.config.emailProtection, javascriptMatch, emailMatch, anchorMatch, urlMatch, telMatch, retval = {}; if ( ( javascriptMatch = href.match( javascriptProtocolRegex ) ) ) { if ( emailProtection == 'encode' ) { href = href.replace( encodedEmailLinkRegex, function( match, protectedAddress, rest ) { // Without it 'undefined' is appended to e-mails without subject and body (https://dev.ckeditor.com/ticket/9192). rest = rest || ''; return 'mailto:' + String.fromCharCode.apply( String, protectedAddress.split( ',' ) ) + unescapeSingleQuote( rest ); } ); } // Protected email link as function call. else if ( emailProtection ) { href.replace( functionCallProtectedEmailLinkRegex, function( match, funcName, funcArgs ) { if ( funcName == compiledProtectionFunction.name ) { retval.type = 'email'; var email = retval.email = {}; var paramRegex = /[^,\s]+/g, paramQuoteRegex = /(^')|('$)/g, paramsMatch = funcArgs.match( paramRegex ), paramsMatchLength = paramsMatch.length, paramName, paramVal; for ( var i = 0; i < paramsMatchLength; i++ ) { paramVal = decodeURIComponent( unescapeSingleQuote( paramsMatch[ i ].replace( paramQuoteRegex, '' ) ) ); paramName = compiledProtectionFunction.params[ i ].toLowerCase(); email[ paramName ] = paramVal; } email.address = [ email.name, email.domain ].join( '@' ); } } ); } } if ( !retval.type ) { if ( ( anchorMatch = href.match( anchorRegex ) ) ) { retval.type = 'anchor'; retval.anchor = {}; retval.anchor.name = retval.anchor.id = anchorMatch[ 1 ]; } else if ( ( telMatch = href.match( telRegex ) ) ) { retval.type = 'tel'; retval.tel = telMatch[ 1 ]; } // Protected email link as encoded string. else if ( ( emailMatch = href.match( emailRegex ) ) ) { var subjectMatch = href.match( emailSubjectRegex ), bodyMatch = href.match( emailBodyRegex ); retval.type = 'email'; var email = ( retval.email = {} ); email.address = emailMatch[ 1 ]; subjectMatch && ( email.subject = decodeURIComponent( subjectMatch[ 1 ] ) ); bodyMatch && ( email.body = decodeURIComponent( bodyMatch[ 1 ] ) ); } // urlRegex matches empty strings, so need to check for href as well. else if ( href && ( urlMatch = href.match( urlRegex ) ) ) { retval.type = 'url'; retval.url = {}; retval.url.protocol = urlMatch[ 1 ]; retval.url.url = urlMatch[ 2 ]; } } // Load target and popup settings. if ( element ) { var target = element.getAttribute( 'target' ); // IE BUG: target attribute is an empty string instead of null in IE if it's not set. if ( !target ) { var onclick = element.data( 'cke-pa-onclick' ) || element.getAttribute( 'onclick' ), onclickMatch = onclick && onclick.match( popupRegex ); if ( onclickMatch ) { retval.target = { type: 'popup', name: onclickMatch[ 1 ] }; var featureMatch; while ( ( featureMatch = popupFeaturesRegex.exec( onclickMatch[ 2 ] ) ) ) { // Some values should remain numbers (https://dev.ckeditor.com/ticket/7300) if ( ( featureMatch[ 2 ] == 'yes' || featureMatch[ 2 ] == '1' ) && !( featureMatch[ 1 ] in { height: 1, width: 1, top: 1, left: 1 } ) ) retval.target[ featureMatch[ 1 ] ] = true; else if ( isFinite( featureMatch[ 2 ] ) ) retval.target[ featureMatch[ 1 ] ] = featureMatch[ 2 ]; } } } else { retval.target = { type: target.match( selectableTargets ) ? target : 'frame', name: target }; } var download = element.getAttribute( 'download' ); if ( download !== null ) { retval.download = true; } var advanced = {}; for ( var a in advAttrNames ) { var val = element.getAttribute( a ); if ( val ) advanced[ advAttrNames[ a ] ] = val; } var advName = element.data( 'cke-saved-name' ) || advanced.advName; if ( advName ) advanced.advName = advName; if ( !CKEDITOR.tools.isEmpty( advanced ) ) retval.advanced = advanced; } return retval; }, /** * Converts link data produced by {@link #parseLinkAttributes} into an object which consists * of attributes to be set (with their values) and an array of attributes to be removed. * This method can be used to compose or to update any link element with the given data. * * @since 4.4.0 * @param {CKEDITOR.editor} editor * @param {Object} data Data in {@link #parseLinkAttributes} format. * @returns {Object} An object consisting of two keys, i.e.: * * { * // Attributes to be set. * set: { * href: 'http://foo.bar', * target: 'bang' * }, * // Attributes to be removed. * removed: [ * 'id', 'style' * ] * } * */ getLinkAttributes: function( editor, data ) { var emailProtection = editor.config.emailProtection || '', set = {}; // Compose the URL. switch ( data.type ) { case 'url': var protocol = ( data.url && data.url.protocol !== undefined ) ? data.url.protocol : 'http://', url = ( data.url && CKEDITOR.tools.trim( data.url.url ) ) || ''; set[ 'data-cke-saved-href' ] = ( url.indexOf( '/' ) === 0 ) ? url : protocol + url; break; case 'anchor': var name = ( data.anchor && data.anchor.name ), id = ( data.anchor && data.anchor.id ); set[ 'data-cke-saved-href' ] = '#' + ( name || id || '' ); break; case 'email': var email = data.email, address = email.address, linkHref; switch ( emailProtection ) { case '': case 'encode': var subject = encodeURIComponent( email.subject || '' ), body = encodeURIComponent( email.body || '' ), argList = []; // Build the e-mail parameters first. subject && argList.push( 'subject=' + subject ); body && argList.push( 'body=' + body ); argList = argList.length ? '?' + argList.join( '&' ) : ''; if ( emailProtection == 'encode' ) { linkHref = [ 'javascript:void(location.href=\'mailto:\'+', // jshint ignore:line protectEmailAddressAsEncodedString( address ) ]; // parameters are optional. argList && linkHref.push( '+\'', escapeSingleQuote( argList ), '\'' ); linkHref.push( ')' ); } else { linkHref = [ 'mailto:', address, argList ]; } break; default: // Separating name and domain. var nameAndDomain = address.split( '@', 2 ); email.name = nameAndDomain[ 0 ]; email.domain = nameAndDomain[ 1 ]; linkHref = [ 'javascript:', protectEmailLinkAsFunction( editor, email ) ]; // jshint ignore:line } set[ 'data-cke-saved-href' ] = linkHref.join( '' ); break; case 'tel': set[ 'data-cke-saved-href' ] = 'tel:' + data.tel; break; } // Popups and target. if ( data.target ) { if ( data.target.type == 'popup' ) { var onclickList = [ 'window.open(this.href, \'', data.target.name || '', '\', \'' ], featureList = [ 'resizable', 'status', 'location', 'toolbar', 'menubar', 'fullscreen', 'scrollbars', 'dependent' ], featureLength = featureList.length, addFeature = function( featureName ) { if ( data.target[ featureName ] ) featureList.push( featureName + '=' + data.target[ featureName ] ); }; for ( var i = 0; i < featureLength; i++ ) featureList[ i ] = featureList[ i ] + ( data.target[ featureList[ i ] ] ? '=yes' : '=no' ); addFeature( 'width' ); addFeature( 'left' ); addFeature( 'height' ); addFeature( 'top' ); onclickList.push( featureList.join( ',' ), '\'); return false;' ); set[ 'data-cke-pa-onclick' ] = onclickList.join( '' ); } else if ( data.target.type != 'notSet' && data.target.name ) { set.target = data.target.name; } } // Force download attribute. if ( data.download ) { set.download = ''; } // Advanced attributes. if ( data.advanced ) { for ( var a in advAttrNames ) { var val = data.advanced[ advAttrNames[ a ] ]; if ( val ) set[ a ] = val; } if ( set.name ) set[ 'data-cke-saved-name' ] = set.name; } // Browser need the "href" fro copy/paste link to work. (https://dev.ckeditor.com/ticket/6641) if ( set[ 'data-cke-saved-href' ] ) set.href = set[ 'data-cke-saved-href' ]; var removed = { target: 1, onclick: 1, 'data-cke-pa-onclick': 1, 'data-cke-saved-name': 1, 'download': 1 }; if ( data.advanced ) CKEDITOR.tools.extend( removed, advAttrNames ); // Remove all attributes which are not currently set. for ( var s in set ) delete removed[ s ]; return { set: set, removed: CKEDITOR.tools.object.keys( removed ) }; }, /** * Determines whether an element should have a "Display Text" field in the Link dialog. * * @since 4.5.11 * @param {CKEDITOR.dom.element/null} element Selected element, `null` if none selected or if a ranged selection * is made. * @param {CKEDITOR.editor} editor The editor instance for which the check is performed. * @returns {Boolean} */ showDisplayTextForElement: function( element, editor ) { var undesiredElements = { img: 1, table: 1, tbody: 1, thead: 1, tfoot: 1, input: 1, select: 1, textarea: 1 }, selection = editor.getSelection(); // Widget duck typing, we don't want to show display text for widgets. if ( editor.widgets && editor.widgets.focused ) { return false; } if ( selection && selection.getRanges().length > 1 ) { return false; } return !element || !element.getName || !element.is( undesiredElements ); } }; // TODO Much probably there's no need to expose these as public objects. CKEDITOR.unlinkCommand = function() {}; CKEDITOR.unlinkCommand.prototype = { exec: function( editor ) { // IE/Edge removes link from selection while executing "unlink" command when cursor // is right before/after link's text. Therefore whole link must be selected and the // position of cursor must be restored to its initial state after unlinking. (https://dev.ckeditor.com/ticket/13062) if ( CKEDITOR.env.ie ) { var range = editor.getSelection().getRanges()[ 0 ], link = ( range.getPreviousEditableNode() && range.getPreviousEditableNode().getAscendant( 'a', true ) ) || ( range.getNextEditableNode() && range.getNextEditableNode().getAscendant( 'a', true ) ), bookmark; if ( range.collapsed && link ) { bookmark = range.createBookmark(); range.selectNodeContents( link ); range.select(); } } var style = new CKEDITOR.style( { element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 } ); editor.removeStyle( style ); if ( bookmark ) { range.moveToBookmark( bookmark ); range.select(); } }, refresh: function( editor, path ) { // Despite our initial hope, document.queryCommandEnabled() does not work // for this in Firefox. So we must detect the state by element paths. var element = path.lastElement && path.lastElement.getAscendant( 'a', true ); if ( element && element.getName() == 'a' && element.getAttribute( 'href' ) && element.getChildCount() ) this.setState( CKEDITOR.TRISTATE_OFF ); else this.setState( CKEDITOR.TRISTATE_DISABLED ); }, contextSensitive: 1, startDisabled: 1, requiredContent: 'a[href]', editorFocus: 1 }; CKEDITOR.removeAnchorCommand = function() {}; CKEDITOR.removeAnchorCommand.prototype = { exec: function( editor ) { var sel = editor.getSelection(), bms = sel.createBookmarks(), anchor; if ( sel && ( anchor = sel.getSelectedElement() ) && ( !anchor.getChildCount() ? CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, anchor ) : anchor.is( 'a' ) ) ) anchor.remove( 1 ); else { if ( ( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) ) { if ( anchor.hasAttribute( 'href' ) ) { anchor.removeAttributes( { name: 1, 'data-cke-saved-name': 1 } ); anchor.removeClass( 'cke_anchor' ); } else { anchor.remove( 1 ); } } } sel.selectBookmarks( bms ); }, requiredContent: 'a[name]' }; CKEDITOR.tools.extend( CKEDITOR.config, { /** * Whether to show the Advanced tab in the Link dialog window. * * @cfg {Boolean} [linkShowAdvancedTab=true] * @member CKEDITOR.config */ linkShowAdvancedTab: true, /** * Whether to show the Target tab in the Link dialog window. * * @cfg {Boolean} [linkShowTargetTab=true] * @member CKEDITOR.config */ linkShowTargetTab: true /** * Whether JavaScript code is allowed as a `href` attribute in an anchor tag. * With this option enabled it is possible to create links like: * * hello world * * By default JavaScript links are not allowed and will not pass * the Link dialog window validation. * * @since 4.4.1 * @cfg {Boolean} [linkJavaScriptLinksAllowed=false] * @member CKEDITOR.config */ /** * Optional JavaScript regular expression used whenever phone numbers in the Link dialog should be validated. * * config.linkPhoneRegExp = /^[0-9]{9}$/; * * @since 4.11.0 * @cfg {RegExp} [linkPhoneRegExp] * @member CKEDITOR.config */ /** * Optional message for the alert popup used when the phone number in the Link dialog does not pass the validation. * * config.linkPhoneMsg = "Invalid number"; * * @since 4.11.0 * @cfg {String} [linkPhoneMsg] * @member CKEDITOR.config */ } ); } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview Insert and remove numbered and bulleted lists. */ ( function() { var listNodeNames = { ol: 1, ul: 1 }; var whitespaces = CKEDITOR.dom.walker.whitespaces(), bookmarks = CKEDITOR.dom.walker.bookmark(), nonEmpty = function( node ) { return !( whitespaces( node ) || bookmarks( node ) ); }, blockBogus = CKEDITOR.dom.walker.bogus(); function cleanUpDirection( element ) { var dir, parent, parentDir; if ( ( dir = element.getDirection() ) ) { parent = element.getParent(); while ( parent && !( parentDir = parent.getDirection() ) ) parent = parent.getParent(); if ( dir == parentDir ) element.removeAttribute( 'dir' ); } } // Inherit inline styles from another element. function inheritInlineStyles( parent, el ) { var style = parent.getAttribute( 'style' ); // Put parent styles before child styles. style && el.setAttribute( 'style', style.replace( /([^;])$/, '$1;' ) + ( el.getAttribute( 'style' ) || '' ) ); } CKEDITOR.plugins.list = { /** * Convert a DOM list tree into a data structure that is easier to * manipulate. This operation should be non-intrusive in the sense that it * does not change the DOM tree, with the exception that it may add some * markers to the list item nodes when database is specified. * * @member CKEDITOR.plugins.list * @todo params */ listToArray: function( listNode, database, baseArray, baseIndentLevel, grandparentNode ) { if ( !listNodeNames[ listNode.getName() ] ) return []; if ( !baseIndentLevel ) baseIndentLevel = 0; if ( !baseArray ) baseArray = []; // Iterate over all list items to and look for inner lists. for ( var i = 0, count = listNode.getChildCount(); i < count; i++ ) { var listItem = listNode.getChild( i ); // Fixing malformed nested lists by moving it into a previous list item. (https://dev.ckeditor.com/ticket/6236) if ( listItem.type == CKEDITOR.NODE_ELEMENT && listItem.getName() in CKEDITOR.dtd.$list ) CKEDITOR.plugins.list.listToArray( listItem, database, baseArray, baseIndentLevel + 1 ); // It may be a text node or some funny stuff. if ( listItem.$.nodeName.toLowerCase() != 'li' ) continue; var itemObj = { 'parent': listNode, indent: baseIndentLevel, element: listItem, contents: [] }; if ( !grandparentNode ) { itemObj.grandparent = listNode.getParent(); if ( itemObj.grandparent && itemObj.grandparent.$.nodeName.toLowerCase() == 'li' ) itemObj.grandparent = itemObj.grandparent.getParent(); } else { itemObj.grandparent = grandparentNode; } if ( database ) CKEDITOR.dom.element.setMarker( database, listItem, 'listarray_index', baseArray.length ); baseArray.push( itemObj ); for ( var j = 0, itemChildCount = listItem.getChildCount(), child; j < itemChildCount; j++ ) { child = listItem.getChild( j ); if ( child.type == CKEDITOR.NODE_ELEMENT && listNodeNames[ child.getName() ] ) // Note the recursion here, it pushes inner list items with // +1 indentation in the correct order. CKEDITOR.plugins.list.listToArray( child, database, baseArray, baseIndentLevel + 1, itemObj.grandparent ); else itemObj.contents.push( child ); } } return baseArray; }, /** * Convert our internal representation of a list back to a DOM forest. * * @member CKEDITOR.plugins.list * @todo params */ arrayToList: function( listArray, database, baseIndex, paragraphMode, dir ) { if ( !baseIndex ) baseIndex = 0; if ( !listArray || listArray.length < baseIndex + 1 ) return null; var i, doc = listArray[ baseIndex ].parent.getDocument(), retval = new CKEDITOR.dom.documentFragment( doc ), rootNode = null, currentIndex = baseIndex, indentLevel = Math.max( listArray[ baseIndex ].indent, 0 ), currentListItem = null, orgDir, block, paragraphName = ( paragraphMode == CKEDITOR.ENTER_P ? 'p' : 'div' ); while ( 1 ) { var item = listArray[ currentIndex ], itemGrandParent = item.grandparent; orgDir = item.element.getDirection( 1 ); if ( item.indent == indentLevel ) { if ( !rootNode || listArray[ currentIndex ].parent.getName() != rootNode.getName() ) { rootNode = listArray[ currentIndex ].parent.clone( false, 1 ); dir && rootNode.setAttribute( 'dir', dir ); retval.append( rootNode ); } currentListItem = rootNode.append( item.element.clone( 0, 1 ) ); if ( orgDir != rootNode.getDirection( 1 ) ) currentListItem.setAttribute( 'dir', orgDir ); for ( i = 0; i < item.contents.length; i++ ) currentListItem.append( item.contents[ i ].clone( 1, 1 ) ); currentIndex++; } else if ( item.indent == Math.max( indentLevel, 0 ) + 1 ) { // Maintain original direction (https://dev.ckeditor.com/ticket/6861). var currDir = listArray[ currentIndex - 1 ].element.getDirection( 1 ), listData = CKEDITOR.plugins.list.arrayToList( listArray, null, currentIndex, paragraphMode, currDir != orgDir ? orgDir : null ); // If the next block is an
      3. with another list tree as the first // child, we'll need to append a filler (
        /NBSP) or the list item // wouldn't be editable. (https://dev.ckeditor.com/ticket/6724) if ( !currentListItem.getChildCount() && CKEDITOR.env.needsNbspFiller && doc.$.documentMode <= 7 ) currentListItem.append( doc.createText( '\xa0' ) ); currentListItem.append( listData.listNode ); currentIndex = listData.nextIndex; } else if ( item.indent == -1 && !baseIndex && itemGrandParent ) { if ( listNodeNames[ itemGrandParent.getName() ] ) { currentListItem = item.element.clone( false, true ); if ( orgDir != itemGrandParent.getDirection( 1 ) ) currentListItem.setAttribute( 'dir', orgDir ); } else { currentListItem = new CKEDITOR.dom.documentFragment( doc ); } // Migrate all children to the new container, // apply the proper text direction. var dirLoose = itemGrandParent.getDirection( 1 ) != orgDir, li = item.element, className = li.getAttribute( 'class' ), style = li.getAttribute( 'style' ); var needsBlock = currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT && ( paragraphMode != CKEDITOR.ENTER_BR || dirLoose || style || className ); var child, count = item.contents.length, cachedBookmark; for ( i = 0; i < count; i++ ) { child = item.contents[ i ]; // Append bookmark if we can, or cache it and append it when we'll know // what to do with it. Generally - we want to keep it next to its original neighbour. // Exception: if bookmark is the only child it hasn't got any neighbour, so handle it normally // (wrap with block if needed). if ( bookmarks( child ) && count > 1 ) { // If we don't need block, it's simple - append bookmark directly to the current list item. if ( !needsBlock ) currentListItem.append( child.clone( 1, 1 ) ); else cachedBookmark = child.clone( 1, 1 ); } // Block content goes directly to the current list item, without wrapping. else if ( child.type == CKEDITOR.NODE_ELEMENT && child.isBlockBoundary() ) { // Apply direction on content blocks. if ( dirLoose && !child.getDirection() ) child.setAttribute( 'dir', orgDir ); inheritInlineStyles( li, child ); className && child.addClass( className ); // Close the block which we started for inline content. block = null; // Append bookmark directly before current child. if ( cachedBookmark ) { currentListItem.append( cachedBookmark ); cachedBookmark = null; } // Append this block element to the list item. currentListItem.append( child.clone( 1, 1 ) ); } // Some inline content was found - wrap it with block and append that // block to the current list item or append it to the block previously created. else if ( needsBlock ) { // Establish new block to hold text direction and styles. if ( !block ) { block = doc.createElement( paragraphName ); currentListItem.append( block ); dirLoose && block.setAttribute( 'dir', orgDir ); } // Copy over styles to new block; style && block.setAttribute( 'style', style ); className && block.setAttribute( 'class', className ); // Append bookmark directly before current child. if ( cachedBookmark ) { block.append( cachedBookmark ); cachedBookmark = null; } block.append( child.clone( 1, 1 ) ); } // E.g. BR mode - inline content appended directly to the list item. else { currentListItem.append( child.clone( 1, 1 ) ); } } // No content after bookmark - append it to the block if we had one // or directly to the current list item if we finished directly in the current list item. if ( cachedBookmark ) { ( block || currentListItem ).append( cachedBookmark ); cachedBookmark = null; } if ( currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT && currentIndex != listArray.length - 1 ) { var last; // Remove bogus
        if this browser uses them. if ( CKEDITOR.env.needsBrFiller ) { last = currentListItem.getLast(); if ( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( 'br' ) ) last.remove(); } // If the last element is not a block, append
        to separate merged list items. last = currentListItem.getLast( nonEmpty ); if ( !( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( CKEDITOR.dtd.$block ) ) ) currentListItem.append( doc.createElement( 'br' ) ); } var currentListItemName = currentListItem.$.nodeName.toLowerCase(); if ( currentListItemName == 'div' || currentListItemName == 'p' ) { currentListItem.appendBogus(); } retval.append( currentListItem ); rootNode = null; currentIndex++; } else { return null; } block = null; if ( listArray.length <= currentIndex || Math.max( listArray[ currentIndex ].indent, 0 ) < indentLevel ) break; } if ( database ) { var currentNode = retval.getFirst(); while ( currentNode ) { if ( currentNode.type == CKEDITOR.NODE_ELEMENT ) { // Clear marker attributes for the new list tree made of cloned nodes, if any. CKEDITOR.dom.element.clearMarkers( database, currentNode ); // Clear redundant direction attribute specified on list items. if ( currentNode.getName() in CKEDITOR.dtd.$listItem ) cleanUpDirection( currentNode ); } currentNode = currentNode.getNextSourceNode(); } } return { listNode: retval, nextIndex: currentIndex }; } }; function changeListType( editor, groupObj, database, listsCreated ) { // This case is easy... // 1. Convert the whole list into a one-dimensional array. // 2. Change the list type by modifying the array. // 3. Recreate the whole list by converting the array to a list. // 4. Replace the original list with the recreated list. var listArray = CKEDITOR.plugins.list.listToArray( groupObj.root, database ), selectedListItems = []; for ( var i = 0; i < groupObj.contents.length; i++ ) { var itemNode = groupObj.contents[ i ]; itemNode = itemNode.getAscendant( 'li', true ); if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) ) continue; selectedListItems.push( itemNode ); CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true ); } var root = groupObj.root, doc = root.getDocument(), listNode, newListNode; for ( i = 0; i < selectedListItems.length; i++ ) { var listIndex = selectedListItems[ i ].getCustomData( 'listarray_index' ); listNode = listArray[ listIndex ].parent; // Switch to new list node for this particular item. if ( !listNode.is( this.type ) ) { newListNode = doc.createElement( this.type ); // Copy all attributes, except from 'start' and 'type'. listNode.copyAttributes( newListNode, { start: 1, type: 1 } ); // The list-style-type property should be ignored. newListNode.removeStyle( 'list-style-type' ); listArray[ listIndex ].parent = newListNode; } } var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode ); var child, length = newList.listNode.getChildCount(); for ( i = 0; i < length && ( child = newList.listNode.getChild( i ) ); i++ ) { if ( child.getName() == this.type ) listsCreated.push( child ); } newList.listNode.replace( groupObj.root ); editor.fire( 'contentDomInvalidated' ); } function createList( editor, groupObj, listsCreated ) { var contents = groupObj.contents, doc = groupObj.root.getDocument(), listContents = []; // It is possible to have the contents returned by DomRangeIterator to be the same as the root. // e.g. when we're running into table cells. // In such a case, enclose the childNodes of contents[0] into a
        . if ( contents.length == 1 && contents[ 0 ].equals( groupObj.root ) ) { var divBlock = doc.createElement( 'div' ); contents[ 0 ].moveChildren && contents[ 0 ].moveChildren( divBlock ); contents[ 0 ].append( divBlock ); contents[ 0 ] = divBlock; } // Calculate the common parent node of all content blocks. var commonParent = groupObj.contents[ 0 ].getParent(); for ( var i = 0; i < contents.length; i++ ) commonParent = commonParent.getCommonAncestor( contents[ i ].getParent() ); var useComputedState = editor.config.useComputedState, listDir, explicitDirection; useComputedState = useComputedState === undefined || useComputedState; // We want to insert things that are in the same tree level only, so calculate the contents again // by expanding the selected blocks to the same tree level. for ( i = 0; i < contents.length; i++ ) { var contentNode = contents[ i ], parentNode; while ( ( parentNode = contentNode.getParent() ) ) { if ( parentNode.equals( commonParent ) ) { listContents.push( contentNode ); // Determine the lists's direction. if ( !explicitDirection && contentNode.getDirection() ) explicitDirection = 1; var itemDir = contentNode.getDirection( useComputedState ); if ( listDir !== null ) { // If at least one LI have a different direction than current listDir, we can't have listDir. if ( listDir && listDir != itemDir ) listDir = null; else listDir = itemDir; } break; } contentNode = parentNode; } } if ( listContents.length < 1 ) return; // Insert the list to the DOM tree. var insertAnchor = listContents[ listContents.length - 1 ].getNext(), listNode = doc.createElement( this.type ); listsCreated.push( listNode ); var contentBlock, listItem; while ( listContents.length ) { contentBlock = listContents.shift(); listItem = doc.createElement( 'li' ); // If current block should be preserved, append it to list item instead of // transforming it to
      4. element. if ( shouldPreserveBlock( contentBlock ) ) contentBlock.appendTo( listItem ); else { contentBlock.copyAttributes( listItem ); // Remove direction attribute after it was merged into list root. (https://dev.ckeditor.com/ticket/7657) if ( listDir && contentBlock.getDirection() ) { listItem.removeStyle( 'direction' ); listItem.removeAttribute( 'dir' ); } contentBlock.moveChildren( listItem ); contentBlock.remove(); } listItem.appendTo( listNode ); } // Apply list root dir only if it has been explicitly declared. if ( listDir && explicitDirection ) listNode.setAttribute( 'dir', listDir ); if ( insertAnchor ) listNode.insertBefore( insertAnchor ); else listNode.appendTo( commonParent ); } function removeList( editor, groupObj, database ) { // This is very much like the change list type operation. // Except that we're changing the selected items' indent to -1 in the list array. var listArray = CKEDITOR.plugins.list.listToArray( groupObj.root, database ), selectedListItems = []; for ( var i = 0; i < groupObj.contents.length; i++ ) { var itemNode = groupObj.contents[ i ]; itemNode = itemNode.getAscendant( 'li', true ); if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) ) continue; selectedListItems.push( itemNode ); CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true ); } var lastListIndex = null; for ( i = 0; i < selectedListItems.length; i++ ) { var listIndex = selectedListItems[ i ].getCustomData( 'listarray_index' ); listArray[ listIndex ].indent = -1; lastListIndex = listIndex; } // After cutting parts of the list out with indent=-1, we still have to maintain the array list // model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the // list cannot be converted back to a real DOM list. for ( i = lastListIndex + 1; i < listArray.length; i++ ) { if ( listArray[ i ].indent > listArray[ i - 1 ].indent + 1 ) { var indentOffset = listArray[ i - 1 ].indent + 1 - listArray[ i ].indent; var oldIndent = listArray[ i ].indent; while ( listArray[ i ] && listArray[ i ].indent >= oldIndent ) { listArray[ i ].indent += indentOffset; i++; } i--; } } var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode, groupObj.root.getAttribute( 'dir' ) ); // Compensate
        before/after the list node if the surrounds are non-blocks.(https://dev.ckeditor.com/ticket/3836) var docFragment = newList.listNode, boundaryNode, siblingNode; function compensateBrs( isStart ) { if ( ( boundaryNode = docFragment[ isStart ? 'getFirst' : 'getLast' ]() ) && !( boundaryNode.is && boundaryNode.isBlockBoundary() ) && ( siblingNode = groupObj.root[ isStart ? 'getPrevious' : 'getNext' ]( CKEDITOR.dom.walker.invisible( true ) ) ) && !( siblingNode.is && siblingNode.isBlockBoundary( { br: 1 } ) ) ) { editor.document.createElement( 'br' )[ isStart ? 'insertBefore' : 'insertAfter' ]( boundaryNode ); } } compensateBrs( true ); compensateBrs(); docFragment.replace( groupObj.root ); editor.fire( 'contentDomInvalidated' ); } var headerTagRegex = /^h[1-6]$/; // Checks wheather this block should be element preserved (not transformed to
      5. ) when creating list. function shouldPreserveBlock( block ) { return ( // https://dev.ckeditor.com/ticket/5335 block.is( 'pre' ) || // https://dev.ckeditor.com/ticket/5271 - this is a header. headerTagRegex.test( block.getName() ) || // 11083 - this is a non-editable element. block.getAttribute( 'contenteditable' ) == 'false' ); } function listCommand( name, type ) { this.name = name; this.type = type; this.context = type; this.allowedContent = type + ' li'; this.requiredContent = type; } var elementType = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ); // Merge child nodes with direction preserved. (https://dev.ckeditor.com/ticket/7448) function mergeChildren( from, into, refNode, forward ) { var child, itemDir; while ( ( child = from[ forward ? 'getLast' : 'getFirst' ]( elementType ) ) ) { if ( ( itemDir = child.getDirection( 1 ) ) !== into.getDirection( 1 ) ) child.setAttribute( 'dir', itemDir ); child.remove(); refNode ? child[ forward ? 'insertBefore' : 'insertAfter' ]( refNode ) : into.append( child, forward ); refNode = child; } } listCommand.prototype = { exec: function( editor ) { // Run state check first of all. this.refresh( editor, editor.elementPath() ); var config = editor.config, selection = editor.getSelection(), ranges = selection && selection.getRanges(); // Midas lists rule #1 says we can create a list even in an empty document. // But DOM iterator wouldn't run if the document is really empty. // So create a paragraph if the document is empty and we're going to create a list. if ( this.state == CKEDITOR.TRISTATE_OFF ) { var editable = editor.editable(); if ( !editable.getFirst( nonEmpty ) ) { config.enterMode == CKEDITOR.ENTER_BR ? editable.appendBogus() : ranges[ 0 ].fixBlock( 1, config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ); selection.selectRanges( ranges ); } // Maybe a single range there enclosing the whole list, // turn on the list state manually(https://dev.ckeditor.com/ticket/4129). else { var range = ranges.length == 1 && ranges[ 0 ], enclosedNode = range && range.getEnclosedNode(); if ( enclosedNode && enclosedNode.is && this.type == enclosedNode.getName() ) this.setState( CKEDITOR.TRISTATE_ON ); } } var bookmarks = selection.createBookmarks( true ); // Group the blocks up because there are many cases where multiple lists have to be created, // or multiple lists have to be cancelled. var listGroups = [], database = {}, rangeIterator = ranges.createIterator(), index = 0; while ( ( range = rangeIterator.getNextRange() ) && ++index ) { var boundaryNodes = range.getBoundaryNodes(), startNode = boundaryNodes.startNode, endNode = boundaryNodes.endNode; if ( startNode.type == CKEDITOR.NODE_ELEMENT && startNode.getName() == 'td' ) range.setStartAt( boundaryNodes.startNode, CKEDITOR.POSITION_AFTER_START ); if ( endNode.type == CKEDITOR.NODE_ELEMENT && endNode.getName() == 'td' ) range.setEndAt( boundaryNodes.endNode, CKEDITOR.POSITION_BEFORE_END ); var iterator = range.createIterator(), block; iterator.forceBrBreak = ( this.state == CKEDITOR.TRISTATE_OFF ); while ( ( block = iterator.getNextParagraph() ) ) { // Avoid duplicate blocks get processed across ranges. // Avoid processing comments, we don't want to touch it. if ( block.getCustomData( 'list_block' ) || hasCommentsChildOnly( block ) ) continue; else CKEDITOR.dom.element.setMarker( database, block, 'list_block', 1 ); var path = editor.elementPath( block ), pathElements = path.elements, pathElementsCount = pathElements.length, processedFlag = 0, blockLimit = path.blockLimit, element; // First, try to group by a list ancestor. for ( var i = pathElementsCount - 1; i >= 0 && ( element = pathElements[ i ] ); i-- ) { // Don't leak outside block limit (https://dev.ckeditor.com/ticket/3940). if ( listNodeNames[ element.getName() ] && blockLimit.contains( element ) ) { // If we've encountered a list inside a block limit // The last group object of the block limit element should // no longer be valid. Since paragraphs after the list // should belong to a different group of paragraphs before // the list. (Bug https://dev.ckeditor.com/ticket/1309) blockLimit.removeCustomData( 'list_group_object_' + index ); var groupObj = element.getCustomData( 'list_group_object' ); if ( groupObj ) groupObj.contents.push( block ); else { groupObj = { root: element, contents: [ block ] }; listGroups.push( groupObj ); CKEDITOR.dom.element.setMarker( database, element, 'list_group_object', groupObj ); } processedFlag = 1; break; } } if ( processedFlag ) continue; // No list ancestor? Group by block limit, but don't mix contents from different ranges. var root = blockLimit; if ( root.getCustomData( 'list_group_object_' + index ) ) root.getCustomData( 'list_group_object_' + index ).contents.push( block ); else { groupObj = { root: root, contents: [ block ] }; CKEDITOR.dom.element.setMarker( database, root, 'list_group_object_' + index, groupObj ); listGroups.push( groupObj ); } } } // Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element. // We either have to build lists or remove lists, for removing a list does not makes sense when we are looking // at the group that's not rooted at lists. So we have three cases to handle. var listsCreated = []; while ( listGroups.length > 0 ) { groupObj = listGroups.shift(); if ( this.state == CKEDITOR.TRISTATE_OFF ) { if ( isEmptyList( groupObj ) ) { continue; } else if ( listNodeNames[ groupObj.root.getName() ] ) { changeListType.call( this, editor, groupObj, database, listsCreated ); } else { createList.call( this, editor, groupObj, listsCreated ); } } else if ( this.state == CKEDITOR.TRISTATE_ON && listNodeNames[ groupObj.root.getName() ] && !isEmptyList( groupObj ) ) { removeList.call( this, editor, groupObj, database ); } } // For all new lists created, merge into adjacent, same type lists. for ( i = 0; i < listsCreated.length; i++ ) mergeListSiblings( listsCreated[ i ] ); // Clean up, restore selection and update toolbar button states. CKEDITOR.dom.element.clearAllMarkers( database ); selection.selectBookmarks( bookmarks ); editor.focus(); function isEmptyList( groupObj ) { // If list is without any li item, then ignore such element from transformation, because it throws errors in console (#2411, #2438). return listNodeNames[ groupObj.root.getName() ] && !getChildCount( groupObj.root, [ CKEDITOR.NODE_COMMENT ] ); } function getChildCount( element, excludeTypes ) { return CKEDITOR.tools.array.filter( element.getChildren().toArray(), function( node ) { return CKEDITOR.tools.array.indexOf( excludeTypes, node.type ) === -1; } ).length; } function hasCommentsChildOnly( element ) { var ret = true; if ( element.getChildCount() === 0 ) { return false; } element.forEach( function( node ) { if ( node.type !== CKEDITOR.NODE_COMMENT ) { ret = false; return false; } }, null, true ); return ret; } }, refresh: function( editor, path ) { var list = path.contains( listNodeNames, 1 ), limit = path.blockLimit || path.root; // 1. Only a single type of list activate. // 2. Do not show list outside of block limit. if ( list && limit.contains( list ) ) this.setState( list.is( this.type ) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF ); else this.setState( CKEDITOR.TRISTATE_OFF ); } }; // Merge list adjacent, of same type lists. function mergeListSiblings( listNode ) { function mergeSibling( rtl ) { var sibling = listNode[ rtl ? 'getPrevious' : 'getNext' ]( nonEmpty ); if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT && sibling.is( listNode.getName() ) ) { // Move children order by merge direction.(https://dev.ckeditor.com/ticket/3820) mergeChildren( listNode, sibling, null, !rtl ); listNode.remove(); listNode = sibling; } } mergeSibling(); mergeSibling( 1 ); } // Check if node is block element that recieves text. function isTextBlock( node ) { return node.type == CKEDITOR.NODE_ELEMENT && ( node.getName() in CKEDITOR.dtd.$block || node.getName() in CKEDITOR.dtd.$listItem ) && CKEDITOR.dtd[ node.getName() ][ '#' ]; } // Join visually two block lines. function joinNextLineToCursor( editor, cursor, nextCursor ) { editor.fire( 'saveSnapshot' ); // Merge with previous block's content. nextCursor.enlarge( CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ); var frag = nextCursor.extractContents(); cursor.trim( false, true ); var bm = cursor.createBookmark(); // Kill original bogus; var currentPath = new CKEDITOR.dom.elementPath( cursor.startContainer ), pathBlock = currentPath.block, currentBlock = currentPath.lastElement.getAscendant( 'li', 1 ) || pathBlock, nextPath = new CKEDITOR.dom.elementPath( nextCursor.startContainer ), nextLi = nextPath.contains( CKEDITOR.dtd.$listItem ), nextList = nextPath.contains( CKEDITOR.dtd.$list ), last; // Remove bogus node the current block/pseudo block. if ( pathBlock ) { var bogus = pathBlock.getBogus(); bogus && bogus.remove(); } else if ( nextList ) { last = nextList.getPrevious( nonEmpty ); if ( last && blockBogus( last ) ) last.remove(); } // Kill the tail br in extracted. last = frag.getLast(); if ( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( 'br' ) ) last.remove(); // Insert fragment at the range position. var nextNode = cursor.startContainer.getChild( cursor.startOffset ); if ( nextNode ) frag.insertBefore( nextNode ); else cursor.startContainer.append( frag ); // Move the sub list nested in the next list item. if ( nextLi ) { var sublist = getSubList( nextLi ); if ( sublist ) { // If next line is in the sub list of the current list item. if ( currentBlock.contains( nextLi ) ) { mergeChildren( sublist, nextLi.getParent(), nextLi ); sublist.remove(); } // Migrate the sub list to current list item. else { currentBlock.append( sublist ); } } } var nextBlock, parent; // Remove any remaining zombies path blocks at the end after line merged. while ( nextCursor.checkStartOfBlock() && nextCursor.checkEndOfBlock() ) { nextPath = nextCursor.startPath(); nextBlock = nextPath.block; // Abort when nothing to be removed (https://dev.ckeditor.com/ticket/10890). if ( !nextBlock ) break; // Check if also to remove empty list. if ( nextBlock.is( 'li' ) ) { parent = nextBlock.getParent(); if ( nextBlock.equals( parent.getLast( nonEmpty ) ) && nextBlock.equals( parent.getFirst( nonEmpty ) ) ) nextBlock = parent; } nextCursor.moveToPosition( nextBlock, CKEDITOR.POSITION_BEFORE_START ); nextBlock.remove(); } // Check if need to further merge with the list resides after the merged block. (https://dev.ckeditor.com/ticket/9080) var walkerRng = nextCursor.clone(), editable = editor.editable(); walkerRng.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END ); var walker = new CKEDITOR.dom.walker( walkerRng ); walker.evaluator = function( node ) { return nonEmpty( node ) && !blockBogus( node ); }; var next = walker.next(); if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getName() in CKEDITOR.dtd.$list ) mergeListSiblings( next ); cursor.moveToBookmark( bm ); // Make fresh selection. cursor.select(); editor.fire( 'saveSnapshot' ); } function getSubList( li ) { var last = li.getLast( nonEmpty ); return last && last.type == CKEDITOR.NODE_ELEMENT && last.getName() in listNodeNames ? last : null; } CKEDITOR.plugins.add( 'list', { // jscs:disable maximumLineLength // jscs:enable maximumLineLength requires: 'indentlist', init: function( editor ) { if ( editor.blockless ) return; // Register commands. editor.addCommand( 'numberedlist', new listCommand( 'numberedlist', 'ol' ) ); editor.addCommand( 'bulletedlist', new listCommand( 'bulletedlist', 'ul' ) ); // Register the toolbar button. if ( editor.ui.addButton ) { editor.ui.addButton( 'NumberedList', { label: editor.lang.list.numberedlist, command: 'numberedlist', directional: true, toolbar: 'list,10' } ); editor.ui.addButton( 'BulletedList', { label: editor.lang.list.bulletedlist, command: 'bulletedlist', directional: true, toolbar: 'list,20' } ); } // Handled backspace/del key to join list items. (https://dev.ckeditor.com/ticket/8248,https://dev.ckeditor.com/ticket/9080) editor.on( 'key', function( evt ) { // Use getKey directly in order to ignore modifiers. // Justification: https://dev.ckeditor.com/ticket/11861#comment:13 var key = evt.data.domEvent.getKey(), li; // DEl/BACKSPACE if ( editor.mode == 'wysiwyg' && key in { 8: 1, 46: 1 } ) { var sel = editor.getSelection(), range = sel.getRanges()[ 0 ], path = range && range.startPath(); if ( !range || !range.collapsed ) return; var isBackspace = key == 8; var editable = editor.editable(); var walker = new CKEDITOR.dom.walker( range.clone() ); walker.evaluator = function( node ) { return nonEmpty( node ) && !blockBogus( node ); }; // Backspace/Del behavior at the start/end of table is handled in core. walker.guard = function( node, isOut ) { return !( isOut && node.type == CKEDITOR.NODE_ELEMENT && node.is( 'table' ) ); }; var cursor = range.clone(); if ( isBackspace ) { var previous, joinWith; // Join a sub list's first line, with the previous visual line in parent. if ( ( previous = path.contains( listNodeNames ) ) && range.checkBoundaryOfElement( previous, CKEDITOR.START ) && ( previous = previous.getParent() ) && previous.is( 'li' ) && ( previous = getSubList( previous ) ) ) { joinWith = previous; previous = previous.getPrevious( nonEmpty ); // Place cursor before the nested list. cursor.moveToPosition( previous && blockBogus( previous ) ? previous : joinWith, CKEDITOR.POSITION_BEFORE_START ); } // Join any line following a list, with the last visual line of the list. else { walker.range.setStartAt( editable, CKEDITOR.POSITION_AFTER_START ); walker.range.setEnd( range.startContainer, range.startOffset ); previous = walker.previous(); if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && ( previous.getName() in listNodeNames || previous.is( 'li' ) ) ) { if ( !previous.is( 'li' ) ) { walker.range.selectNodeContents( previous ); walker.reset(); walker.evaluator = isTextBlock; previous = walker.previous(); } joinWith = previous; // Place cursor at the end of previous block. cursor.moveToElementEditEnd( joinWith ); // And then just before end of closest block element (https://dev.ckeditor.com/ticket/12729). cursor.moveToPosition( cursor.endPath().block, CKEDITOR.POSITION_BEFORE_END ); } } if ( joinWith ) { joinNextLineToCursor( editor, cursor, range ); evt.cancel(); } else { var list = path.contains( listNodeNames ); // Backspace pressed at the start of list outdents the first list item. (https://dev.ckeditor.com/ticket/9129) if ( list && range.checkBoundaryOfElement( list, CKEDITOR.START ) ) { li = list.getFirst( nonEmpty ); if ( range.checkBoundaryOfElement( li, CKEDITOR.START ) ) { previous = list.getPrevious( nonEmpty ); // Only if the list item contains a sub list, do nothing but // simply move cursor backward one character. if ( getSubList( li ) ) { if ( previous ) { range.moveToElementEditEnd( previous ); range.select(); } evt.cancel(); } else { editor.execCommand( 'outdent' ); evt.cancel(); } } } } } else { var next, nextLine; li = path.contains( 'li' ); if ( li ) { walker.range.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END ); var last = li.getLast( nonEmpty ); var block = last && isTextBlock( last ) ? last : li; // Indicate cursor at the visual end of an list item. var isAtEnd = 0; next = walker.next(); // When list item contains a sub list. if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getName() in listNodeNames && next.equals( last ) ) { isAtEnd = 1; // Move to the first item in sub list. next = walker.next(); } // Right at the end of list item. else if ( range.checkBoundaryOfElement( block, CKEDITOR.END ) ) { isAtEnd = 2; } if ( isAtEnd && next ) { // Put cursor range there. nextLine = range.clone(); nextLine.moveToElementEditStart( next ); // https://dev.ckeditor.com/ticket/13409 // For the following case and similar // //
          //
        • //

          x^

          //
            //
          • y
          • //
          //
        • //
        if ( isAtEnd == 1 ) { // Move the cursor to if attached to "x" text node. cursor.optimize(); // Abort if the range is attached directly in
      6. , like // //
          //
        • // x^ //
            //
          • y
          • //
          //
        • //
        if ( !cursor.startContainer.equals( li ) ) { var node = cursor.startContainer, farthestInlineAscendant; // Find , which is farthest from but still inline element. while ( node.is( CKEDITOR.dtd.$inline ) ) { farthestInlineAscendant = node; node = node.getParent(); } // Move the range so it does not contain inline elements. // It prevents from being included in . // // // // so instead of // //
          //
        • //

          x^y

          //
        • //
        // // pressing DELETE produces // //
          //
        • //

          x^y

          //
        • //
        if ( farthestInlineAscendant ) { cursor.moveToPosition( farthestInlineAscendant, CKEDITOR.POSITION_AFTER_END ); } } } // Moving `cursor` and `next line` only when at the end literally (https://dev.ckeditor.com/ticket/12729). if ( isAtEnd == 2 ) { cursor.moveToPosition( cursor.endPath().block, CKEDITOR.POSITION_BEFORE_END ); // Next line might be text node not wrapped in block element. if ( nextLine.endPath().block ) { nextLine.moveToPosition( nextLine.endPath().block, CKEDITOR.POSITION_AFTER_START ); } } joinNextLineToCursor( editor, cursor, nextLine ); evt.cancel(); } } else { // Handle Del key pressed before the list. walker.range.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END ); next = walker.next(); if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.is( listNodeNames ) ) { // The start
      7. next = next.getFirst( nonEmpty ); // Simply remove the current empty block, move cursor to the // subsequent list. if ( path.block && range.checkStartOfBlock() && range.checkEndOfBlock() ) { path.block.remove(); range.moveToElementEditStart( next ); range.select(); evt.cancel(); } // Preventing the default (merge behavior), but simply move // the cursor one character forward if subsequent list item // contains sub list. else if ( getSubList( next ) ) { range.moveToElementEditStart( next ); range.select(); evt.cancel(); } // Merge the first list item with the current line. else { nextLine = range.clone(); nextLine.moveToElementEditStart( next ); joinNextLineToCursor( editor, cursor, nextLine ); evt.cancel(); } } } } // The backspace/del could potentially put cursor at a bad position, // being it handled or not, check immediately the selection to have it fixed. setTimeout( function() { editor.selectionChange( 1 ); } ); } } ); } } ); } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'menu', { requires: 'floatpanel', beforeInit: function( editor ) { var groups = editor.config.menu_groups.split( ',' ), groupsOrder = editor._.menuGroups = {}, menuItems = editor._.menuItems = {}; for ( var i = 0; i < groups.length; i++ ) groupsOrder[ groups[ i ] ] = i + 1; /** * Registers an item group to the editor context menu in order to make it * possible to associate it with menu items later. * * @param {String} name Specify a group name. * @param {Number} [order=100] Define the display sequence of this group * inside the menu. A smaller value gets displayed first. * @member CKEDITOR.editor */ editor.addMenuGroup = function( name, order ) { groupsOrder[ name ] = order || 100; }; /** * Adds an item from the specified definition to the editor context menu. * * @method * @param {String} name The menu item name. * @param {Object} definition The menu item definition. * @member CKEDITOR.editor */ editor.addMenuItem = function( name, definition ) { if ( groupsOrder[ definition.group ] ) menuItems[ name ] = new CKEDITOR.menuItem( this, name, definition ); }; /** * Adds one or more items from the specified definition object to the editor context menu. * * @method * @param {Object} definitions Object where keys are used as itemName and corresponding values as definition for a {@link #addMenuItem} call. * @member CKEDITOR.editor */ editor.addMenuItems = function( definitions ) { for ( var itemName in definitions ) { this.addMenuItem( itemName, definitions[ itemName ] ); } }; /** * Retrieves a particular menu item definition from the editor context menu. * * @method * @param {String} name The name of the desired menu item. * @returns {Object} * @member CKEDITOR.editor */ editor.getMenuItem = function( name ) { return menuItems[ name ]; }; /** * Removes a particular menu item added before from the editor context menu. * * @since 3.6.1 * @method * @param {String} name The name of the desired menu item. * @member CKEDITOR.editor */ editor.removeMenuItem = function( name ) { delete menuItems[ name ]; }; } } ); ( function() { var menuItemSource = '' + ''; menuItemSource += //'' + '' + '' + '' + '' + '' + '{label}' + '' + '{shortcutHtml}' + '{arrowHtml}' + '' + '{ariaShortcut}'; var menuArrowSource = '' + '{label}' + ''; var menuShortcutSource = '' + '{shortcut}' + ''; var menuItemTpl = CKEDITOR.addTemplate( 'menuItem', menuItemSource ), menuArrowTpl = CKEDITOR.addTemplate( 'menuArrow', menuArrowSource ), menuShortcutTpl = CKEDITOR.addTemplate( 'menuShortcut', menuShortcutSource ); /** * @class * @todo */ CKEDITOR.menu = CKEDITOR.tools.createClass( { /** * @constructor */ $: function( editor, definition ) { definition = this._.definition = definition || {}; this.id = CKEDITOR.tools.getNextId(); this.editor = editor; this.items = []; this._.listeners = []; this._.level = definition.level || 1; var panelDefinition = CKEDITOR.tools.extend( {}, definition.panel, { css: [ CKEDITOR.skin.getPath( 'editor' ) ], level: this._.level - 1, block: {} } ); var attrs = panelDefinition.block.attributes = ( panelDefinition.attributes || {} ); // Provide default role of 'menu'. !attrs.role && ( attrs.role = 'menu' ); this._.panelDefinition = panelDefinition; }, _: { onShow: function() { var selection = this.editor.getSelection(), start = selection && selection.getStartElement(), path = this.editor.elementPath(), listeners = this._.listeners; this.removeAll(); // Call all listeners, filling the list of items to be displayed. for ( var i = 0; i < listeners.length; i++ ) { var listenerItems = listeners[ i ]( start, selection, path ); if ( listenerItems ) { for ( var itemName in listenerItems ) { var item = this.editor.getMenuItem( itemName ); if ( item && ( !item.command || this.editor.getCommand( item.command ).state ) ) { item.state = listenerItems[ itemName ]; this.add( item ); } } } } }, onClick: function( item ) { this.hide(); if ( item.onClick ) item.onClick(); else if ( item.command ) this.editor.execCommand( item.command ); }, onEscape: function( keystroke ) { var parent = this.parent; // 1. If it's sub-menu, close it, with focus restored on this. // 2. In case of a top-menu, close it, with focus returned to page. if ( parent ) parent._.panel.hideChild( 1 ); else if ( keystroke == 27 ) this.hide( 1 ); return false; }, onHide: function() { this.onHide && this.onHide(); }, showSubMenu: function( index ) { var menu = this._.subMenu, item = this.items[ index ], subItemDefs = item.getItems && item.getItems(); // If this item has no subitems, we just hide the submenu, if // available, and return back. if ( !subItemDefs ) { // Hide sub menu with focus returned. this._.panel.hideChild( 1 ); return; } // Create the submenu, if not available, or clean the existing // one. if ( menu ) menu.removeAll(); else { menu = this._.subMenu = new CKEDITOR.menu( this.editor, CKEDITOR.tools.extend( {}, this._.definition, { level: this._.level + 1 }, true ) ); menu.parent = this; menu._.onClick = CKEDITOR.tools.bind( this._.onClick, this ); } // Add all submenu items to the menu. for ( var subItemName in subItemDefs ) { var subItem = this.editor.getMenuItem( subItemName ); if ( subItem ) { subItem.state = subItemDefs[ subItemName ]; menu.add( subItem ); } } // Get the element representing the current item. var element = this._.panel.getBlock( this.id ).element.getDocument().getById( this.id + String( index ) ); // Show the submenu. // This timeout is needed to give time for the sub-menu get // focus when JAWS is running. (https://dev.ckeditor.com/ticket/9844) setTimeout( function() { menu.show( element, 2 ); }, 0 ); } }, proto: { /** * Adds an item. * * @param item */ add: function( item ) { // Later we may sort the items, but Array#sort is not stable in // some browsers, here we're forcing the original sequence with // 'order' attribute if it hasn't been assigned. (https://dev.ckeditor.com/ticket/3868) if ( !item.order ) item.order = this.items.length; this.items.push( item ); }, /** * Removes all items. */ removeAll: function() { this.items = []; }, /** * Shows the menu in given location. * * @param {CKEDITOR.dom.element} offsetParent * @param {Number} [corner] * @param {Number} [offsetX] * @param {Number} [offsetY] */ show: function( offsetParent, corner, offsetX, offsetY ) { // Not for sub menu. if ( !this.parent ) { this._.onShow(); // Don't menu with zero items. if ( !this.items.length ) return; } corner = corner || ( this.editor.lang.dir == 'rtl' ? 2 : 1 ); var items = this.items, editor = this.editor, panel = this._.panel, element = this._.element; // Create the floating panel for this menu. if ( !panel ) { panel = this._.panel = new CKEDITOR.ui.floatPanel( this.editor, CKEDITOR.document.getBody(), this._.panelDefinition, this._.level ); panel.onEscape = CKEDITOR.tools.bind( function( keystroke ) { if ( this._.onEscape( keystroke ) === false ) return false; }, this ); panel.onShow = function() { // Menu need CSS resets, compensate class name. var holder = panel._.panel.getHolderElement(); holder.getParent().addClass( 'cke' ).addClass( 'cke_reset_all' ); }; panel.onHide = CKEDITOR.tools.bind( function() { this._.onHide && this._.onHide(); }, this ); // Create an autosize block inside the panel. var block = panel.addBlock( this.id, this._.panelDefinition.block ); block.autoSize = true; var keys = block.keys; keys[ 40 ] = 'next'; // ARROW-DOWN keys[ 9 ] = 'next'; // TAB keys[ 38 ] = 'prev'; // ARROW-UP keys[ CKEDITOR.SHIFT + 9 ] = 'prev'; // SHIFT + TAB keys[ ( editor.lang.dir == 'rtl' ? 37 : 39 ) ] = CKEDITOR.env.ie ? 'mouseup' : 'click'; // ARROW-RIGHT/ARROW-LEFT(rtl) keys[ 32 ] = CKEDITOR.env.ie ? 'mouseup' : 'click'; // SPACE CKEDITOR.env.ie && ( keys[ 13 ] = 'mouseup' ); // Manage ENTER, since onclick is blocked in IE (https://dev.ckeditor.com/ticket/8041). element = this._.element = block.element; var elementDoc = element.getDocument(); elementDoc.getBody().setStyle( 'overflow', 'hidden' ); elementDoc.getElementsByTag( 'html' ).getItem( 0 ).setStyle( 'overflow', 'hidden' ); this._.itemOverFn = CKEDITOR.tools.addFunction( function( index ) { clearTimeout( this._.showSubTimeout ); this._.showSubTimeout = CKEDITOR.tools.setTimeout( this._.showSubMenu, editor.config.menu_subMenuDelay || 400, this, [ index ] ); }, this ); this._.itemOutFn = CKEDITOR.tools.addFunction( function() { clearTimeout( this._.showSubTimeout ); }, this ); this._.itemClickFn = CKEDITOR.tools.addFunction( function( index ) { var item = this.items[ index ]; if ( item.state == CKEDITOR.TRISTATE_DISABLED ) { this.hide( 1 ); return; } if ( item.getItems ) this._.showSubMenu( index ); else this._.onClick( item ); }, this ); } // Put the items in the right order. sortItems( items ); // Apply the editor mixed direction status to menu. var path = editor.elementPath(), mixedDirCls = ( path && path.direction() != editor.lang.dir ) ? ' cke_mixed_dir_content' : ''; // Build the HTML that composes the menu and its items. var output = [ '' ); // Inject the HTML inside the panel. element.setHtml( output.join( '' ) ); CKEDITOR.ui.fire( 'ready', this ); // Show the panel. if ( this.parent ) { this.parent._.panel.showAsChild( panel, this.id, offsetParent, corner, offsetX, offsetY ); } else { panel.showBlock( this.id, offsetParent, corner, offsetX, offsetY ); } var data = [ panel ]; editor.fire( 'menuShow', data ); }, /** * Adds a callback executed on opening the menu. Items * returned by that callback are added to the menu. * * @param {Function} listenerFn * @param {CKEDITOR.dom.element} listenerFn.startElement The selection start anchor element. * @param {CKEDITOR.dom.selection} listenerFn.selection The current selection. * @param {CKEDITOR.dom.elementPath} listenerFn.path The current elements path. * @param listenerFn.return Object (`commandName` => `state`) of items that should be added to the menu. */ addListener: function( listenerFn ) { this._.listeners.push( listenerFn ); }, /** * Hides the menu. * * @param {Boolean} [returnFocus] */ hide: function( returnFocus ) { this._.onHide && this._.onHide(); this._.panel && this._.panel.hide( returnFocus ); }, /** * Finds the menu item corresponding to a given command. * * **Notice**: Keep in mind that the menu is re-rendered on each opening, so caching items (especially DOM elements) * may not work. Also executing this method when the menu is not visible may give unexpected results as the * items may not be rendered. * * @since 4.9.0 * @param {String} commandName * @returns {Object/null} return An object containing a given item. If the item was not found, `null` is returned. * @returns {CKEDITOR.menuItem} return.item The item definition. * @returns {CKEDITOR.dom.element} return.element The rendered element representing the item in the menu. */ findItemByCommandName: function( commandName ) { var commands = CKEDITOR.tools.array.filter( this.items, function( item ) { return commandName === item.command; } ); if ( commands.length ) { var commandItem = commands[ 0 ]; return { item: commandItem, element: this._.element.findOne( '.' + commandItem.className ) }; } return null; } } } ); function sortItems( items ) { items.sort( function( itemA, itemB ) { if ( itemA.group < itemB.group ) return -1; else if ( itemA.group > itemB.group ) return 1; return itemA.order < itemB.order ? -1 : itemA.order > itemB.order ? 1 : 0; } ); } /** * @class * @todo */ CKEDITOR.menuItem = CKEDITOR.tools.createClass( { $: function( editor, name, definition ) { CKEDITOR.tools.extend( this, definition, // Defaults { order: 0, className: 'cke_menubutton__' + name } ); // Transform the group name into its order number. this.group = editor._.menuGroups[ this.group ]; this.editor = editor; this.name = name; }, proto: { render: function( menu, index, output ) { var id = menu.id + String( index ), state = ( typeof this.state == 'undefined' ) ? CKEDITOR.TRISTATE_OFF : this.state, ariaChecked = '', editor = this.editor, keystroke, command, shortcut; var stateName = state == CKEDITOR.TRISTATE_ON ? 'on' : state == CKEDITOR.TRISTATE_DISABLED ? 'disabled' : 'off'; if ( this.role in { menuitemcheckbox: 1, menuitemradio: 1 } ) ariaChecked = ' aria-checked="' + ( state == CKEDITOR.TRISTATE_ON ? 'true' : 'false' ) + '"'; var hasSubMenu = this.getItems; // ltr: BLACK LEFT-POINTING POINTER // rtl: BLACK RIGHT-POINTING POINTER var arrowLabel = '&#' + ( this.editor.lang.dir == 'rtl' ? '9668' : '9658' ) + ';'; var iconName = this.name; if ( this.icon && !( /\./ ).test( this.icon ) ) iconName = this.icon; if ( this.command ) { command = editor.getCommand( this.command ); keystroke = editor.getCommandKeystroke( command ); if ( keystroke ) { shortcut = CKEDITOR.tools.keystrokeToString( editor.lang.common.keyboard, keystroke ); } } var params = { id: id, name: this.name, iconName: iconName, label: this.label, cls: this.className || '', state: stateName, hasPopup: hasSubMenu ? 'true' : 'false', disabled: state == CKEDITOR.TRISTATE_DISABLED, title: this.label + ( shortcut ? ' (' + shortcut.display + ')' : '' ), ariaShortcut: shortcut ? editor.lang.common.keyboardShortcut + ' ' + shortcut.aria : '', href: 'javascript:void(\'' + ( this.label || '' ).replace( "'" + '' ) + '\')', // jshint ignore:line hoverFn: menu._.itemOverFn, moveOutFn: menu._.itemOutFn, clickFn: menu._.itemClickFn, index: index, iconStyle: CKEDITOR.skin.getIconStyle( iconName, ( this.editor.lang.dir == 'rtl' ), iconName == this.icon ? null : this.icon, this.iconOffset ), shortcutHtml: shortcut ? menuShortcutTpl.output( { shortcut: shortcut.display } ) : '', arrowHtml: hasSubMenu ? menuArrowTpl.output( { label: arrowLabel } ) : '', role: this.role ? this.role : 'menuitem', ariaChecked: ariaChecked }; menuItemTpl.output( params, output ); } } } ); } )(); /** * The amount of time, in milliseconds, the editor waits before displaying submenu * options when moving the mouse over options that contain submenus, like the * "Cell Properties" entry for tables. * * // Remove the submenu delay. * config.menu_subMenuDelay = 0; * * @cfg {Number} [menu_subMenuDelay=400] * @member CKEDITOR.config */ /** * Fired when a menu is shown. * * @event menuShow * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param {CKEDITOR.ui.panel[]} data */ /** * A comma separated list of items group names to be displayed in the context * menu. The order of items will reflect the order specified in this list if * no priority was defined in the groups. * * config.menu_groups = 'clipboard,table,anchor,link,image'; * * @cfg {String} [menu_groups=see source] * @member CKEDITOR.config */ CKEDITOR.config.menu_groups = 'clipboard,' + 'form,' + 'tablecell,tablecellproperties,tablerow,tablecolumn,table,' + 'anchor,link,image,flash,' + 'checkbox,radio,textfield,hiddenfield,imagebutton,button,select,textarea,div'; /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'contextmenu', { requires: 'menu', // jscs:disable maximumLineLength // jscs:enable maximumLineLength // Make sure the base class (CKEDITOR.menu) is loaded before it (https://dev.ckeditor.com/ticket/3318). onLoad: function() { /** * Class replacing the non-configurable native context menu with a configurable CKEditor's equivalent. * * @class * @extends CKEDITOR.menu */ CKEDITOR.plugins.contextMenu = CKEDITOR.tools.createClass( { base: CKEDITOR.menu, /** * Creates the CKEDITOR.plugins.contextMenu class instance. * * @constructor * @param {CKEDITOR.editor} editor */ $: function( editor ) { this.base.call( this, editor, { panel: { // Allow adding custom CSS (#2202). css: editor.config.contextmenu_contentsCss, className: 'cke_menu_panel', attributes: { 'aria-label': editor.lang.contextmenu.options } } } ); }, proto: { /** * Starts watching on native context menu triggers (Option key, right click) on the given element. * * @param {CKEDITOR.dom.element} element * @param {Boolean} [nativeContextMenuOnCtrl] Whether to open native context menu if the * Ctrl key is held on opening the context menu. See {@link CKEDITOR.config#browserContextMenuOnCtrl}. */ addTarget: function( element, nativeContextMenuOnCtrl ) { var holdCtrlKey, keystrokeActive; element.on( 'contextmenu', function( event ) { var domEvent = event.data, isCtrlKeyDown = // Safari on Windows always show 'ctrlKey' as true in 'contextmenu' event, // which make this property unreliable. (https://dev.ckeditor.com/ticket/4826) ( CKEDITOR.env.webkit ? holdCtrlKey : ( CKEDITOR.env.mac ? domEvent.$.metaKey : domEvent.$.ctrlKey ) ); if ( nativeContextMenuOnCtrl && isCtrlKeyDown ) { return; } // Cancel the browser context menu. domEvent.preventDefault(); // Do not react to this event, as it might open context menu in wrong position (#2548). if ( keystrokeActive ) { return; } // Fix selection when non-editable element in Webkit/Blink (Mac) (https://dev.ckeditor.com/ticket/11306). if ( CKEDITOR.env.mac && CKEDITOR.env.webkit ) { var editor = this.editor, contentEditableParent = new CKEDITOR.dom.elementPath( domEvent.getTarget(), editor.editable() ).contains( function( el ) { // Return when non-editable or nested editable element is found. return el.hasAttribute( 'contenteditable' ); }, true ); // Exclude editor's editable. // Fake selection for non-editables only (to exclude nested editables). if ( contentEditableParent && contentEditableParent.getAttribute( 'contenteditable' ) == 'false' ) { editor.getSelection().fake( contentEditableParent ); } } var doc = domEvent.getTarget().getDocument(), offsetParent = domEvent.getTarget().getDocument().getDocumentElement(), fromFrame = !doc.equals( CKEDITOR.document ), scroll = doc.getWindow().getScrollPosition(), offsetX = fromFrame ? domEvent.$.clientX : domEvent.$.pageX || scroll.x + domEvent.$.clientX, offsetY = fromFrame ? domEvent.$.clientY : domEvent.$.pageY || scroll.y + domEvent.$.clientY; CKEDITOR.tools.setTimeout( function() { this.open( offsetParent, null, offsetX, offsetY ); // IE needs a short while to allow selection change before opening menu. (https://dev.ckeditor.com/ticket/7908) }, CKEDITOR.env.ie ? 200 : 0, this ); }, this ); if ( CKEDITOR.env.webkit ) { var onKeyDown = function( event ) { holdCtrlKey = CKEDITOR.env.mac ? event.data.$.metaKey : event.data.$.ctrlKey; }, resetOnKeyUp = function() { holdCtrlKey = 0; }; element.on( 'keydown', onKeyDown ); element.on( 'keyup', resetOnKeyUp ); element.on( 'contextmenu', resetOnKeyUp ); } // Block subsequent contextmenu event, when Shift + F10 is pressed (#2548). if ( CKEDITOR.env.gecko && !CKEDITOR.env.mac ) { element.on( 'keydown', function( evt ) { if ( evt.data.$.shiftKey && evt.data.$.keyCode === 121 ) { keystrokeActive = true; } }, null, null, 0 ); element.on( 'keyup', resetKeystrokeState ); element.on( 'contextmenu', resetKeystrokeState ); } function resetKeystrokeState() { keystrokeActive = false; } }, /** * Opens the context menu in the given location. See the {@link CKEDITOR.menu#show} method. * * @param {CKEDITOR.dom.element} offsetParent * @param {Number} [corner] * @param {Number} [offsetX] * @param {Number} [offsetY] */ open: function( offsetParent, corner, offsetX, offsetY ) { // Do not open context menu if it's disabled or there is no selection in the editor (#1181). if ( this.editor.config.enableContextMenu === false || this.editor.getSelection().getType() === CKEDITOR.SELECTION_NONE ) { return; } this.editor.focus(); offsetParent = offsetParent || CKEDITOR.document.getDocumentElement(); // https://dev.ckeditor.com/ticket/9362: Force selection check to update commands' states in the new context. this.editor.selectionChange( 1 ); this.show( offsetParent, corner, offsetX, offsetY ); } } } ); }, beforeInit: function( editor ) { /** * @readonly * @property {CKEDITOR.plugins.contextMenu} contextMenu * @member CKEDITOR.editor */ var contextMenu = editor.contextMenu = new CKEDITOR.plugins.contextMenu( editor ); editor.on( 'contentDom', function() { contextMenu.addTarget( editor.editable(), editor.config.browserContextMenuOnCtrl !== false ); } ); editor.addCommand( 'contextMenu', { exec: function( editor ) { var offsetX = 0, offsetY = 0, ranges = editor.getSelection().getRanges(), rects, rect; // When opening context menu via keystroke there is no offsetX and Y passed (#1451). rects = ranges[ ranges.length - 1 ].getClientRects( editor.editable().isInline() ); rect = rects[ rects.length - 1 ]; if ( rect ) { offsetX = rect[ editor.lang.dir === 'rtl' ? 'left' : 'right' ]; offsetY = rect.bottom; } editor.contextMenu.open( editor.document.getBody().getParent(), null, offsetX, offsetY ); } } ); editor.setKeystroke( CKEDITOR.SHIFT + 121 /*F10*/, 'contextMenu' ); editor.setKeystroke( CKEDITOR.CTRL + CKEDITOR.SHIFT + 121 /*F10*/, 'contextMenu' ); } } ); /** * Whether to show the browser native context menu when the Ctrl or * Meta (Mac) key is pressed on opening the context menu with the * right mouse button click or the Menu key. * * ```javascript * config.browserContextMenuOnCtrl = false; * ``` * * @since 3.0.2 * @cfg {Boolean} [browserContextMenuOnCtrl=true] * @member CKEDITOR.config */ /** * Whether to enable the context menu. Regardless of the setting the [Context Menu](https://ckeditor.com/cke4/addon/contextmenu) * plugin is still loaded. * * ```javascript * config.enableContextMenu = false; * ``` * * @since 4.7.0 * @cfg {Boolean} [enableContextMenu=true] * @member CKEDITOR.config */ /** * The CSS file(s) to be used to apply the style to the context menu content. * * ```javascript * config.contextmenu_contentsCss = '/css/myfile.css'; * config.contextmenu_contentsCss = [ '/css/myfile.css', '/css/anotherfile.css' ]; * ``` * * @since 4.11.0 * @cfg {String/String[]} [contextmenu_contentsCss=CKEDITOR.skin.getPath( 'editor' )] * @member CKEDITOR.config */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ ( function() { CKEDITOR.plugins.liststyle = { requires: 'dialog,contextmenu', // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { if ( editor.blockless ) return; var def, cmd; def = new CKEDITOR.dialogCommand( 'numberedListStyle', { requiredContent: 'ol', allowedContent: 'ol{list-style-type}[start]; li{list-style-type}[value]', contentTransformations: [ [ 'ol: listTypeToStyle' ] ] } ); cmd = editor.addCommand( 'numberedListStyle', def ); editor.addFeature( cmd ); CKEDITOR.dialog.add( 'numberedListStyle', this.path + 'dialogs/liststyle.js' ); def = new CKEDITOR.dialogCommand( 'bulletedListStyle', { requiredContent: 'ul', allowedContent: 'ul{list-style-type}', contentTransformations: [ [ 'ul: listTypeToStyle' ] ] } ); cmd = editor.addCommand( 'bulletedListStyle', def ); editor.addFeature( cmd ); CKEDITOR.dialog.add( 'bulletedListStyle', this.path + 'dialogs/liststyle.js' ); //Register map group; editor.addMenuGroup( 'list', 108 ); editor.addMenuItems( { numberedlist: { label: editor.lang.liststyle.numberedTitle, group: 'list', command: 'numberedListStyle' }, bulletedlist: { label: editor.lang.liststyle.bulletedTitle, group: 'list', command: 'bulletedListStyle' } } ); editor.contextMenu.addListener( function( element ) { if ( !element || element.isReadOnly() ) return null; while ( element ) { var name = element.getName(); if ( name == 'ol' ) return { numberedlist: CKEDITOR.TRISTATE_OFF }; else if ( name == 'ul' ) return { bulletedlist: CKEDITOR.TRISTATE_OFF }; element = element.getParent(); } return null; } ); } }; CKEDITOR.plugins.add( 'liststyle', CKEDITOR.plugins.liststyle ); } )(); /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview The [Magic Line](https://ckeditor.com/cke4/addon/magicline) plugin that makes it easier to access some document areas that * are difficult to focus. */ 'use strict'; ( function() { CKEDITOR.plugins.add( 'magicline', { init: initPlugin } ); // Activates the box inside of an editor. function initPlugin( editor ) { // Configurables var config = editor.config, triggerOffset = config.magicline_triggerOffset || 30, enterMode = config.enterMode, that = { // Global stuff is being initialized here. editor: editor, enterMode: enterMode, triggerOffset: triggerOffset, holdDistance: 0 | triggerOffset * ( config.magicline_holdDistance || 0.5 ), boxColor: config.magicline_color || '#ff0000', rtl: config.contentsLangDirection == 'rtl', tabuList: [ 'data-cke-hidden-sel' ].concat( config.magicline_tabuList || [] ), triggers: config.magicline_everywhere ? DTD_BLOCK : { table: 1, hr: 1, div: 1, ul: 1, ol: 1, dl: 1, form: 1, blockquote: 1 } }, scrollTimeout, checkMouseTimeoutPending, checkMouseTimer; // Simple irrelevant elements filter. that.isRelevant = function( node ) { return isHtml( node ) && // -> Node must be an existing HTML element. !isLine( that, node ) && // -> Node can be neither the box nor its child. !isFlowBreaker( node ); // -> Node can be neither floated nor positioned nor aligned. }; editor.on( 'contentDom', addListeners, this ); function addListeners() { var editable = editor.editable(), doc = editor.document, win = editor.window; // Global stuff is being initialized here. extend( that, { editable: editable, inInlineMode: editable.isInline(), doc: doc, win: win, hotNode: null }, true ); // This is the boundary of the editor. For inline the boundary is editable itself. // For classic (`iframe`-based) editor, the HTML element is a real boundary. that.boundary = that.inInlineMode ? that.editable : that.doc.getDocumentElement(); // Enabling the box inside of inline editable is pointless. // There's no need to access spaces inside paragraphs, links, spans, etc. if ( editable.is( dtd.$inline ) ) return; // Handle in-line editing by setting appropriate position. // If current position is static, make it relative and clear top/left coordinates. if ( that.inInlineMode && !isPositioned( editable ) ) { editable.setStyles( { position: 'relative', top: null, left: null } ); } // Enable the box. Let it produce children elements, initialize // event handlers and own methods. initLine.call( this, that ); // Get view dimensions and scroll positions. // At this stage (before any checkMouse call) it is used mostly // by tests. Nevertheless it a crucial thing. updateWindowSize( that ); // Remove the box before an undo image is created. // This is important. If we didn't do that, the *undo thing* would revert the box into an editor. // Thanks to that, undo doesn't even know about the existence of the box. editable.attachListener( editor, 'beforeUndoImage', function() { that.line.detach(); } ); // Removes the box HTML from editor data string if getData is called. // Thanks to that, an editor never yields data polluted by the box. // Listen with very high priority, so line will be removed before other // listeners will see it. editable.attachListener( editor, 'beforeGetData', function() { // If the box is in editable, remove it. if ( that.line.wrap.getParent() ) { that.line.detach(); // Restore line in the last listener for 'getData'. editor.once( 'getData', function() { that.line.attach(); }, null, null, 1000 ); } }, null, null, 0 ); // Hide the box on mouseout if mouse leaves document. editable.attachListener( that.inInlineMode ? doc : doc.getWindow().getFrame(), 'mouseout', function( event ) { if ( editor.mode != 'wysiwyg' ) return; // Check for inline-mode editor. If so, check mouse position // and remove the box if mouse outside of an editor. if ( that.inInlineMode ) { var mouse = { x: event.data.$.clientX, y: event.data.$.clientY }; updateWindowSize( that ); updateEditableSize( that, true ); var size = that.view.editable, scroll = that.view.scroll; // If outside of an editor... if ( !inBetween( mouse.x, size.left - scroll.x, size.right - scroll.x ) || !inBetween( mouse.y, size.top - scroll.y, size.bottom - scroll.y ) ) { clearTimeout( checkMouseTimer ); checkMouseTimer = null; that.line.detach(); } } else { clearTimeout( checkMouseTimer ); checkMouseTimer = null; that.line.detach(); } } ); // This one deactivates hidden mode of an editor which // prevents the box from being shown. editable.attachListener( editable, 'keyup', function() { that.hiddenMode = 0; } ); editable.attachListener( editable, 'keydown', function( event ) { if ( editor.mode != 'wysiwyg' ) return; var keyStroke = event.data.getKeystroke(); switch ( keyStroke ) { // Shift pressed case 2228240: // IE case 16: that.hiddenMode = 1; that.line.detach(); } } ); // This method ensures that checkMouse aren't executed // in parallel and no more frequently than specified in timeout function. // In classic (`iframe`-based) editor, document is used as a trigger, to provide magicline // functionality when mouse is below the body (short content, short body). editable.attachListener( that.inInlineMode ? editable : doc, 'mousemove', function( event ) { checkMouseTimeoutPending = true; if ( editor.mode != 'wysiwyg' || editor.readOnly || checkMouseTimer ) return; // IE<9 requires this event-driven object to be created // outside of the setTimeout statement. // Otherwise it loses the event object with its properties. var mouse = { x: event.data.$.clientX, y: event.data.$.clientY }; checkMouseTimer = setTimeout( function() { checkMouse( mouse ); }, 30 ); // balances performance and accessibility } ); // This one removes box on scroll event. // It is to avoid box displacement. editable.attachListener( win, 'scroll', function() { if ( editor.mode != 'wysiwyg' ) return; that.line.detach(); // To figure this out just look at the mouseup // event handler below. if ( env.webkit ) { that.hiddenMode = 1; clearTimeout( scrollTimeout ); scrollTimeout = setTimeout( function() { // Don't leave hidden mode until mouse remains pressed and // scroll is being used, i.e. when dragging something. if ( !that.mouseDown ) that.hiddenMode = 0; }, 50 ); } } ); // Those event handlers remove the box on mousedown // and don't reveal it until the mouse is released. // It is to prevent box insertion e.g. while scrolling // (w/ scrollbar), selecting and so on. editable.attachListener( env_ie8 ? doc : win, 'mousedown', function() { if ( editor.mode != 'wysiwyg' ) return; that.line.detach(); that.hiddenMode = 1; that.mouseDown = 1; } ); // Google Chrome doesn't trigger this on the scrollbar (since 2009...) // so it is totally useless to check for scroll finish // see: http://code.google.com/p/chromium/issues/detail?id=14204 editable.attachListener( env_ie8 ? doc : win, 'mouseup', function() { that.hiddenMode = 0; that.mouseDown = 0; } ); // Editor commands for accessing difficult focus spaces. editor.addCommand( 'accessPreviousSpace', accessFocusSpaceCmd( that ) ); editor.addCommand( 'accessNextSpace', accessFocusSpaceCmd( that, true ) ); editor.setKeystroke( [ [ config.magicline_keystrokePrevious, 'accessPreviousSpace' ], [ config.magicline_keystrokeNext, 'accessNextSpace' ] ] ); // Revert magicline hot node on undo/redo. editor.on( 'loadSnapshot', function() { var elements, element, i; for ( var t in { p: 1, br: 1, div: 1 } ) { // document.find is not available in QM (https://dev.ckeditor.com/ticket/11149). elements = editor.document.getElementsByTag( t ); for ( i = elements.count(); i--; ) { if ( ( element = elements.getItem( i ) ).data( 'cke-magicline-hot' ) ) { // Restore hotNode that.hotNode = element; // Restore last access direction that.lastCmdDirection = element.data( 'cke-magicline-dir' ) === 'true' ? true : false; return; } } } } ); // This method handles mousemove mouse for box toggling. // It uses mouse position to determine underlying element, then // it tries to use different trigger type in order to place the box // in correct place. The following procedure is executed periodically. function checkMouse( mouse ) { that.mouse = mouse; that.trigger = null; checkMouseTimer = null; updateWindowSize( that ); if ( checkMouseTimeoutPending && // There must be an event pending. !that.hiddenMode && // Can't be in hidden mode. editor.focusManager.hasFocus && // Editor must have focus. !that.line.mouseNear() && // Mouse pointer can't be close to the box. ( that.element = elementFromMouse( that, true ) ) // There must be valid element. ) { // If trigger exists, and trigger is correct -> show the box. // Don't show the line if trigger is a descendant of some tabu-list element. if ( ( that.trigger = triggerEditable( that ) || triggerEdge( that ) || triggerExpand( that ) ) && !isInTabu( that, that.trigger.upper || that.trigger.lower ) ) { that.line.attach().place(); } // Otherwise remove the box else { that.trigger = null; that.line.detach(); } checkMouseTimeoutPending = false; } } // This one allows testing and debugging. It reveals some // inner methods to the world. editor._.magiclineBackdoor = { accessFocusSpace: accessFocusSpace, boxTrigger: boxTrigger, isLine: isLine, getAscendantTrigger: getAscendantTrigger, getNonEmptyNeighbour: getNonEmptyNeighbour, getSize: getSize, that: that, triggerEdge: triggerEdge, triggerEditable: triggerEditable, triggerExpand: triggerExpand }; } } // Some shorthands for common methods to save bytes var extend = CKEDITOR.tools.extend, newElement = CKEDITOR.dom.element, newElementFromHtml = newElement.createFromHtml, env = CKEDITOR.env, env_ie8 = CKEDITOR.env.ie && CKEDITOR.env.version < 9, dtd = CKEDITOR.dtd, // Global object associating enter modes with elements. enterElements = {}, // Constant values, types and so on. EDGE_TOP = 128, EDGE_BOTTOM = 64, EDGE_MIDDLE = 32, TYPE_EDGE = 16, TYPE_EXPAND = 8, LOOK_TOP = 4, LOOK_BOTTOM = 2, LOOK_NORMAL = 1, WHITE_SPACE = '\u00A0', DTD_LISTITEM = dtd.$listItem, DTD_TABLECONTENT = dtd.$tableContent, DTD_NONACCESSIBLE = extend( {}, dtd.$nonEditable, dtd.$empty ), DTD_BLOCK = dtd.$block, // Minimum time that must elapse between two update*Size calls. // It prevents constant getComuptedStyle calls and improves performance. CACHE_TIME = 100, // Shared CSS stuff for box elements CSS_COMMON = 'width:0px;height:0px;padding:0px;margin:0px;display:block;' + 'z-index:9999;color:#fff;position:absolute;font-size: 0px;line-height:0px;', CSS_TRIANGLE = CSS_COMMON + 'border-color:transparent;display:block;border-style:solid;', TRIANGLE_HTML = '' + WHITE_SPACE + ''; enterElements[ CKEDITOR.ENTER_BR ] = 'br'; enterElements[ CKEDITOR.ENTER_P ] = 'p'; enterElements[ CKEDITOR.ENTER_DIV ] = 'div'; function areSiblings( that, upper, lower ) { return isHtml( upper ) && isHtml( lower ) && lower.equals( upper.getNext( function( node ) { return !( isEmptyTextNode( node ) || isComment( node ) || isFlowBreaker( node ) ); } ) ); } // boxTrigger is an abstract type which describes // the relationship between elements that may result // in showing the box. // // The following type is used by numerous methods // to share information about the hypothetical box placement // and look by referring to boxTrigger properties. function boxTrigger( triggerSetup ) { this.upper = triggerSetup[ 0 ]; this.lower = triggerSetup[ 1 ]; this.set.apply( this, triggerSetup.slice( 2 ) ); } boxTrigger.prototype = { set: function( edge, type, look ) { this.properties = edge + type + ( look || LOOK_NORMAL ); return this; }, is: function( property ) { return ( this.properties & property ) == property; } }; var elementFromMouse = ( function() { function elementFromPoint( doc, mouse ) { var pointedElement = doc.$.elementFromPoint( mouse.x, mouse.y ); // IE9QM: from times to times it will return an empty object on scroll bar hover. (https://dev.ckeditor.com/ticket/12185) return pointedElement && pointedElement.nodeType ? new CKEDITOR.dom.element( pointedElement ) : null; } return function( that, ignoreBox, forceMouse ) { if ( !that.mouse ) return null; var doc = that.doc, lineWrap = that.line.wrap, mouse = forceMouse || that.mouse, // Note: element might be null. element = elementFromPoint( doc, mouse ); // If ignoreBox is set and element is the box, it means that we // need to hide the box for a while, repeat elementFromPoint // and show it again. if ( ignoreBox && isLine( that, element ) ) { lineWrap.hide(); element = elementFromPoint( doc, mouse ); lineWrap.show(); } // Return nothing if: // \-> Element is not HTML. if ( !( element && element.type == CKEDITOR.NODE_ELEMENT && element.$ ) ) return null; // Also return nothing if: // \-> We're IE<9 and element is out of the top-level element (editable for inline and HTML for classic (`iframe`-based)). // This is due to the bug which allows IE<9 firing mouse events on element // with contenteditable=true while doing selection out (far, away) of the element. // Thus we must always be sure that we stay in editable or HTML. if ( env.ie && env.version < 9 ) { if ( !( that.boundary.equals( element ) || that.boundary.contains( element ) ) ) return null; } return element; }; } )(); // Gets the closest parent node that belongs to triggers group. function getAscendantTrigger( that ) { var node = that.element, trigger; if ( node && isHtml( node ) ) { trigger = node.getAscendant( that.triggers, true ); // If trigger is an element, neither editable nor editable's ascendant. if ( trigger && that.editable.contains( trigger ) ) { // Check for closest editable limit. // Don't consider trigger as a limit as it may be nested editable (includeSelf=false) (https://dev.ckeditor.com/ticket/12009). var limit = getClosestEditableLimit( trigger ); // Trigger in nested editable area. if ( limit.getAttribute( 'contenteditable' ) == 'true' ) return trigger; // Trigger in non-editable area. else if ( limit.is( that.triggers ) ) return limit; else return null; } else { return null; } } return null; } function getMidpoint( that, upper, lower ) { updateSize( that, upper ); updateSize( that, lower ); var upperSizeBottom = upper.size.bottom, lowerSizeTop = lower.size.top; return upperSizeBottom && lowerSizeTop ? 0 | ( upperSizeBottom + lowerSizeTop ) / 2 : upperSizeBottom || lowerSizeTop; } // Get nearest node (either text or HTML), but: // \-> Omit all empty text nodes (containing white characters only). // \-> Omit BR elements // \-> Omit flow breakers. function getNonEmptyNeighbour( that, node, goBack ) { node = node[ goBack ? 'getPrevious' : 'getNext' ]( function( node ) { return ( isTextNode( node ) && !isEmptyTextNode( node ) ) || ( isHtml( node ) && !isFlowBreaker( node ) && !isLine( that, node ) ); } ); return node; } function inBetween( val, lower, upper ) { return val > lower && val < upper; } // Returns the closest ancestor that has contenteditable attribute. // Such ancestor is the limit of (non-)editable DOM branch that element // belongs to. This method omits editor editable. function getClosestEditableLimit( element, includeSelf ) { if ( element.data( 'cke-editable' ) ) return null; if ( !includeSelf ) element = element.getParent(); while ( element ) { if ( element.data( 'cke-editable' ) ) return null; if ( element.hasAttribute( 'contenteditable' ) ) return element; element = element.getParent(); } return null; } // Access space line consists of a few elements (spans): // \-> Line wrapper. // \-> Line. // \-> Line triangles: left triangle (LT), right triangle (RT). // \-> Button handler (BTN). // // +--------------------------------------------------- line.wrap (span) -----+ // | +---------------------------------------------------- line (span) -----+ | // | | +- LT \ +- BTN -+ / RT -+ | | // | | | \ | | | / | | | // | | | / | <__| | \ | | | // | | +-----/ +-------+ \-----+ | | // | +----------------------------------------------------------------------+ | // +--------------------------------------------------------------------------+ // function initLine( that ) { var doc = that.doc, // This the main box element that holds triangles and the insertion button line = newElementFromHtml( '', doc ), iconPath = CKEDITOR.getUrl( this.path + 'images/' + ( env.hidpi ? 'hidpi/' : '' ) + 'icon' + ( that.rtl ? '-rtl' : '' ) + '.png' ); extend( line, { attach: function() { // Only if not already attached if ( !this.wrap.getParent() ) this.wrap.appendTo( that.editable, true ); return this; }, // Looks are as follows: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ]. lineChildren: [ extend( newElementFromHtml( '', doc ), { base: CSS_COMMON + 'height:17px;width:17px;' + ( that.rtl ? 'left' : 'right' ) + ':17px;' + 'background:url(' + iconPath + ') center no-repeat ' + that.boxColor + ';cursor:pointer;' + ( env.hc ? 'font-size: 15px;line-height:14px;border:1px solid #fff;text-align:center;' : '' ) + ( env.hidpi ? 'background-size: 9px 10px;' : '' ), looks: [ 'top:-8px; border-radius: 2px;', 'top:-17px; border-radius: 2px 2px 0px 0px;', 'top:-1px; border-radius: 0px 0px 2px 2px;' ] } ), extend( newElementFromHtml( TRIANGLE_HTML, doc ), { base: CSS_TRIANGLE + 'left:0px;border-left-color:' + that.boxColor + ';', looks: [ 'border-width:8px 0 8px 8px;top:-8px', 'border-width:8px 0 0 8px;top:-8px', 'border-width:0 0 8px 8px;top:0px' ] } ), extend( newElementFromHtml( TRIANGLE_HTML, doc ), { base: CSS_TRIANGLE + 'right:0px;border-right-color:' + that.boxColor + ';', looks: [ 'border-width:8px 8px 8px 0;top:-8px', 'border-width:8px 8px 0 0;top:-8px', 'border-width:0 8px 8px 0;top:0px' ] } ) ], detach: function() { // Detach only if already attached. if ( this.wrap.getParent() ) this.wrap.remove(); return this; }, // Checks whether mouseY is around an element by comparing boundaries and considering // an offset distance. mouseNear: function() { updateSize( that, this ); var offset = that.holdDistance, size = this.size; // Determine neighborhood by element dimensions and offsets. if ( size && inBetween( that.mouse.y, size.top - offset, size.bottom + offset ) && inBetween( that.mouse.x, size.left - offset, size.right + offset ) ) { return true; } return false; }, // Adjusts position of the box according to the trigger properties. // If also affects look of the box depending on the type of the trigger. place: function() { var view = that.view, editable = that.editable, trigger = that.trigger, upper = trigger.upper, lower = trigger.lower, any = upper || lower, parent = any.getParent(), styleSet = {}; // Save recent trigger for further insertion. // It is necessary due to the fact, that that.trigger may // contain different boxTrigger at the moment of insertion // or may be even null. this.trigger = trigger; upper && updateSize( that, upper, true ); lower && updateSize( that, lower, true ); updateSize( that, parent, true ); // Yeah, that's gonna be useful in inline-mode case. if ( that.inInlineMode ) updateEditableSize( that, true ); // Set X coordinate (left, right, width). if ( parent.equals( editable ) ) { styleSet.left = view.scroll.x; styleSet.right = -view.scroll.x; styleSet.width = ''; } else { styleSet.left = any.size.left - any.size.margin.left + view.scroll.x - ( that.inInlineMode ? view.editable.left + view.editable.border.left : 0 ); styleSet.width = any.size.outerWidth + any.size.margin.left + any.size.margin.right + view.scroll.x; styleSet.right = ''; } // Set Y coordinate (top) for trigger consisting of two elements. if ( upper && lower ) { // No margins at all or they're equal. Place box right between. if ( upper.size.margin.bottom === lower.size.margin.top ) styleSet.top = 0 | ( upper.size.bottom + upper.size.margin.bottom / 2 ); else { // Upper margin < lower margin. Place at lower margin. if ( upper.size.margin.bottom < lower.size.margin.top ) styleSet.top = upper.size.bottom + upper.size.margin.bottom; // Upper margin > lower margin. Place at upper margin - lower margin. else styleSet.top = upper.size.bottom + upper.size.margin.bottom - lower.size.margin.top; } } // Set Y coordinate (top) for single-edge trigger. else if ( !upper ) styleSet.top = lower.size.top - lower.size.margin.top; else if ( !lower ) { styleSet.top = upper.size.bottom + upper.size.margin.bottom; } // Set box button modes if close to the viewport horizontal edge // or look forced by the trigger. if ( trigger.is( LOOK_TOP ) || inBetween( styleSet.top, view.scroll.y - 15, view.scroll.y + 5 ) ) { styleSet.top = that.inInlineMode ? 0 : view.scroll.y; this.look( LOOK_TOP ); } else if ( trigger.is( LOOK_BOTTOM ) || inBetween( styleSet.top, view.pane.bottom - 5, view.pane.bottom + 15 ) ) { styleSet.top = that.inInlineMode ? ( view.editable.height + view.editable.padding.top + view.editable.padding.bottom ) : ( view.pane.bottom - 1 ); this.look( LOOK_BOTTOM ); } else { if ( that.inInlineMode ) styleSet.top -= view.editable.top + view.editable.border.top; this.look( LOOK_NORMAL ); } if ( that.inInlineMode ) { // 1px bug here... styleSet.top--; // Consider the editable to be an element with overflow:scroll // and non-zero scrollTop/scrollLeft value. // For example: divarea editable. (https://dev.ckeditor.com/ticket/9383) styleSet.top += view.editable.scroll.top; styleSet.left += view.editable.scroll.left; } // Append `px` prefixes. for ( var style in styleSet ) styleSet[ style ] = CKEDITOR.tools.cssLength( styleSet[ style ] ); this.setStyles( styleSet ); }, // Changes look of the box according to current needs. // Three different styles are available: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ]. look: function( look ) { if ( this.oldLook == look ) return; for ( var i = this.lineChildren.length, child; i--; ) ( child = this.lineChildren[ i ] ).setAttribute( 'style', child.base + child.looks[ 0 | look / 2 ] ); this.oldLook = look; }, wrap: new newElement( 'span', that.doc ) } ); // Insert children into the box. for ( var i = line.lineChildren.length; i--; ) line.lineChildren[ i ].appendTo( line ); // Set default look of the box. line.look( LOOK_NORMAL ); // Using that wrapper prevents IE (8,9) from resizing editable area at the moment // of box insertion. This works thanks to the fact, that positioned box is wrapped by // an inline element. So much tricky. line.appendTo( line.wrap ); // Make the box unselectable. line.unselectable(); // Handle accessSpace node insertion. line.lineChildren[ 0 ].on( 'mouseup', function( event ) { line.detach(); accessFocusSpace( that, function( accessNode ) { // Use old trigger that was saved by 'place' method. Look: line.place var trigger = that.line.trigger; accessNode[ trigger.is( EDGE_TOP ) ? 'insertBefore' : 'insertAfter' ]( trigger.is( EDGE_TOP ) ? trigger.lower : trigger.upper ); }, true ); that.editor.focus(); if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR ) that.hotNode.scrollIntoView(); event.data.preventDefault( true ); } ); // Prevents IE9 from displaying the resize box and disables drag'n'drop functionality. line.on( 'mousedown', function( event ) { event.data.preventDefault( true ); } ); that.line = line; } // This function allows accessing any focus space according to the insert function: // * For enterMode ENTER_P it creates P element filled with dummy white-space. // * For enterMode ENTER_DIV it creates DIV element filled with dummy white-space. // * For enterMode ENTER_BR it creates BR element or   in IE. // // The node is being inserted according to insertFunction. Finally the method // selects the non-breaking space making the node ready for typing. function accessFocusSpace( that, insertFunction, doSave ) { var range = new CKEDITOR.dom.range( that.doc ), editor = that.editor, accessNode; // IE requires text node of   in ENTER_BR mode. if ( env.ie && that.enterMode == CKEDITOR.ENTER_BR ) accessNode = that.doc.createText( WHITE_SPACE ); // In other cases a regular element is used. else { // Use the enterMode of editable's limit or editor's // enter mode if not in nested editable. var limit = getClosestEditableLimit( that.element, true ), // This is an enter mode for the context. We cannot use // editor.activeEnterMode because the focused nested editable will // have a different enterMode as editor but magicline will be inserted // directly into editor's editable. enterMode = limit && limit.data( 'cke-enter-mode' ) || that.enterMode; accessNode = new newElement( enterElements[ enterMode ], that.doc ); if ( !accessNode.is( 'br' ) ) { var dummy = that.doc.createText( WHITE_SPACE ); dummy.appendTo( accessNode ); } } doSave && editor.fire( 'saveSnapshot' ); insertFunction( accessNode ); //dummy.appendTo( accessNode ); range.moveToPosition( accessNode, CKEDITOR.POSITION_AFTER_START ); editor.getSelection().selectRanges( [ range ] ); that.hotNode = accessNode; doSave && editor.fire( 'saveSnapshot' ); } // Access focus space on demand by taking an element under the caret as a reference. // The space is accessed provided the element under the caret is trigger AND: // // 1. First/last-child of its parent: // +----------------------- Parent element -+ // | +------------------------------ DIV -+ | <-- Access before // | | Foo^ | | // | | | | // | +------------------------------------+ | <-- Access after // +----------------------------------------+ // // OR // // 2. It has a direct sibling element, which is also a trigger: // +-------------------------------- DIV#1 -+ // | Foo^ | // | | // +----------------------------------------+ // <-- Access here // +-------------------------------- DIV#2 -+ // | Bar | // | | // +----------------------------------------+ // // OR // // 3. It has a direct sibling, which is a trigger and has a valid neighbour trigger, // but belongs to dtd.$.empty/nonEditable: // +------------------------------------ P -+ // | Foo^ | // | | // +----------------------------------------+ // +----------------------------------- HR -+ // <-- Access here // +-------------------------------- DIV#2 -+ // | Bar | // | | // +----------------------------------------+ // function accessFocusSpaceCmd( that, insertAfter ) { return { canUndo: true, modes: { wysiwyg: 1 }, exec: ( function() { // Inserts line (accessNode) at the position by taking target node as a reference. function doAccess( target ) { // Remove old hotNode under certain circumstances. var hotNodeChar = ( env.ie && env.version < 9 ? ' ' : WHITE_SPACE ), removeOld = that.hotNode && // Old hotNode must exist. that.hotNode.getText() == hotNodeChar && // Old hotNode hasn't been changed. that.element.equals( that.hotNode ) && // Caret is inside old hotNode. // Command is executed in the same direction. that.lastCmdDirection === !!insertAfter; // jshint ignore:line accessFocusSpace( that, function( accessNode ) { if ( removeOld && that.hotNode ) that.hotNode.remove(); accessNode[ insertAfter ? 'insertAfter' : 'insertBefore' ]( target ); // Make this element distinguishable. Also remember the direction // it's been inserted into document. accessNode.setAttributes( { 'data-cke-magicline-hot': 1, 'data-cke-magicline-dir': !!insertAfter } ); // Save last direction of the command (is insertAfter?). that.lastCmdDirection = !!insertAfter; } ); if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR ) that.hotNode.scrollIntoView(); // Detach the line if was visible (previously triggered by mouse). that.line.detach(); } return function( editor ) { var selected = editor.getSelection().getStartElement(), limit; // (https://dev.ckeditor.com/ticket/9833) Go down to the closest non-inline element in DOM structure // since inline elements don't participate in in magicline. selected = selected.getAscendant( DTD_BLOCK, 1 ); // Stop if selected is a child of a tabu-list element. if ( isInTabu( that, selected ) ) return; // Sometimes it may happen that there's no parent block below selected element // or, for example, getAscendant reaches editable or editable parent. // We must avoid such pathological cases. if ( !selected || selected.equals( that.editable ) || selected.contains( that.editable ) ) return; // Executing the command directly in nested editable should // access space before/after it. if ( ( limit = getClosestEditableLimit( selected ) ) && limit.getAttribute( 'contenteditable' ) == 'false' ) selected = limit; // That holds element from mouse. Replace it with the // element under the caret. that.element = selected; // (3.) Handle the following cases where selected neighbour // is a trigger inaccessible for the caret AND: // - Is first/last-child // OR // - Has a sibling, which is also a trigger. var neighbor = getNonEmptyNeighbour( that, selected, !insertAfter ), neighborSibling; // Check for a neighbour that belongs to triggers. // Consider only non-accessible elements (they cannot have any children) // since they cannot be given a caret inside, to run the command // the regular way (1. & 2.). if ( isHtml( neighbor ) && neighbor.is( that.triggers ) && neighbor.is( DTD_NONACCESSIBLE ) && ( // Check whether neighbor is first/last-child. !getNonEmptyNeighbour( that, neighbor, !insertAfter ) || // Check for a sibling of a neighbour that also is a trigger. ( ( neighborSibling = getNonEmptyNeighbour( that, neighbor, !insertAfter ) ) && isHtml( neighborSibling ) && neighborSibling.is( that.triggers ) ) ) ) { doAccess( neighbor ); return; } // Look for possible target element DOWN "selected" DOM branch (towards editable) // that belong to that.triggers var target = getAscendantTrigger( that, selected ); // No HTML target -> no access. if ( !isHtml( target ) ) return; // (1.) Target is first/last child -> access. if ( !getNonEmptyNeighbour( that, target, !insertAfter ) ) { doAccess( target ); return; } var sibling = getNonEmptyNeighbour( that, target, !insertAfter ); // (2.) Target has a sibling that belongs to that.triggers -> access. if ( sibling && isHtml( sibling ) && sibling.is( that.triggers ) ) { doAccess( target ); return; } }; } )() }; } function isLine( that, node ) { if ( !( node && node.type == CKEDITOR.NODE_ELEMENT && node.$ ) ) return false; var line = that.line; return line.wrap.equals( node ) || line.wrap.contains( node ); } // Is text node containing white-spaces only? var isEmptyTextNode = CKEDITOR.dom.walker.whitespaces(); // Is fully visible HTML node? function isHtml( node ) { return node && node.type == CKEDITOR.NODE_ELEMENT && node.$; // IE requires that } function isFloated( element ) { if ( !isHtml( element ) ) return false; var options = { left: 1, right: 1, center: 1 }; return !!( options[ element.getComputedStyle( 'float' ) ] || options[ element.getAttribute( 'align' ) ] ); } function isFlowBreaker( element ) { if ( !isHtml( element ) ) return false; return isPositioned( element ) || isFloated( element ); } // Isn't node of NODE_COMMENT type? var isComment = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_COMMENT ); function isPositioned( element ) { return !!{ absolute: 1, fixed: 1 }[ element.getComputedStyle( 'position' ) ]; } // Is text node? function isTextNode( node ) { return node && node.type == CKEDITOR.NODE_TEXT; } function isTrigger( that, element ) { return isHtml( element ) ? element.is( that.triggers ) : null; } function isInTabu( that, element ) { if ( !element ) return false; var parents = element.getParents( 1 ); for ( var i = parents.length ; i-- ; ) { for ( var j = that.tabuList.length ; j-- ; ) { if ( parents[ i ].hasAttribute( that.tabuList[ j ] ) ) return true; } } return false; } // This function checks vertically is there's a relevant child between element's edge // and the pointer. // \-> Table contents are omitted. function isChildBetweenPointerAndEdge( that, parent, edgeBottom ) { var edgeChild = parent[ edgeBottom ? 'getLast' : 'getFirst' ]( function( node ) { return that.isRelevant( node ) && !node.is( DTD_TABLECONTENT ); } ); if ( !edgeChild ) return false; updateSize( that, edgeChild ); return edgeBottom ? edgeChild.size.top > that.mouse.y : edgeChild.size.bottom < that.mouse.y; } // This method handles edge cases: // \-> Mouse is around upper or lower edge of view pane. // \-> Also scroll position is either minimal or maximal. // \-> It's OK to show LOOK_TOP(BOTTOM) type line. // // This trigger doesn't need additional post-filtering. // // +----------------------------- Editable -+ /-- // | +---------------------- First child -+ | | <-- Top edge (first child) // | | | | | // | | | | | * Mouse activation area * // | | | | | // | | ... | | \-- Top edge + trigger offset // | . . | // | | // | . . | // | | ... | | /-- Bottom edge - trigger offset // | | | | | // | | | | | * Mouse activation area * // | | | | | // | +----------------------- Last child -+ | | <-- Bottom edge (last child) // +----------------------------------------+ \-- // function triggerEditable( that ) { var editable = that.editable, mouse = that.mouse, view = that.view, triggerOffset = that.triggerOffset, triggerLook; // Update editable dimensions. updateEditableSize( that ); // This flag determines whether checking bottom trigger. var bottomTrigger = mouse.y > ( that.inInlineMode ? ( view.editable.top + view.editable.height / 2 ) : ( // This is to handle case when editable.height / 2 <<< pane.height. Math.min( view.editable.height, view.pane.height ) / 2 ) ), // Edge node according to bottomTrigger. edgeNode = editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( function( node ) { return !( isEmptyTextNode( node ) || isComment( node ) ); } ); // There's no edge node. Abort. if ( !edgeNode ) { return null; } // If the edgeNode in editable is ML, get the next one. if ( isLine( that, edgeNode ) ) { edgeNode = that.line.wrap[ bottomTrigger ? 'getPrevious' : 'getNext' ]( function( node ) { return !( isEmptyTextNode( node ) || isComment( node ) ); } ); } // Exclude bad nodes (no ML needed then): // \-> Edge node is text. // \-> Edge node is floated, etc. // // Edge node *must be* a valid trigger at this stage as well. if ( !isHtml( edgeNode ) || isFlowBreaker( edgeNode ) || !isTrigger( that, edgeNode ) ) { return null; } // Update size of edge node. Dimensions will be necessary. updateSize( that, edgeNode ); // Return appropriate trigger according to bottomTrigger. // \-> Top edge trigger case first. if ( !bottomTrigger && // Top trigger case. edgeNode.size.top >= 0 && // Check if the first element is fully visible. inBetween( mouse.y, 0, edgeNode.size.top + triggerOffset ) ) { // Check if mouse in [0, edgeNode.top + triggerOffset]. // Determine trigger look. triggerLook = that.inInlineMode || view.scroll.y === 0 ? LOOK_TOP : LOOK_NORMAL; return new boxTrigger( [ null, edgeNode, EDGE_TOP, TYPE_EDGE, triggerLook ] ); } // \-> Bottom case. else if ( bottomTrigger && edgeNode.size.bottom <= view.pane.height && // Check if the last element is fully visible inBetween( mouse.y, // Check if mouse in... edgeNode.size.bottom - triggerOffset, view.pane.height ) ) { // [ edgeNode.bottom - triggerOffset, paneHeight ] // Determine trigger look. triggerLook = that.inInlineMode || inBetween( edgeNode.size.bottom, view.pane.height - triggerOffset, view.pane.height ) ? LOOK_BOTTOM : LOOK_NORMAL; return new boxTrigger( [ edgeNode, null, EDGE_BOTTOM, TYPE_EDGE, triggerLook ] ); } return null; } // This method covers cases *inside* of an element: // \-> The pointer is in the top (bottom) area of an element and there's // HTML node before (after) this element. // \-> An element being the first or last child of its parent. // // +----------------------- Parent element -+ // | +----------------------- Element #1 -+ | /-- // | | | | | * Mouse activation area (as first child) * // | | | | \-- // | | | | /-- // | | | | | * Mouse activation area (Element #2) * // | +------------------------------------+ | \-- // | | // | +----------------------- Element #2 -+ | /-- // | | | | | * Mouse activation area (Element #1) * // | | | | \-- // | | | | // | +------------------------------------+ | // | | // | Text node is here. | // | | // | +----------------------- Element #3 -+ | // | | | | // | | | | // | | | | /-- // | | | | | * Mouse activation area (as last child) * // | +------------------------------------+ | \-- // +----------------------------------------+ // function triggerEdge( that ) { var mouse = that.mouse, view = that.view, triggerOffset = that.triggerOffset; // Get the ascendant trigger basing on elementFromMouse. var element = getAscendantTrigger( that ); // Abort if there's no appropriate element. if ( !element ) { return null; } // Dimensions will be necessary. updateSize( that, element ); // If triggerOffset is larger than a half of element's height, // use an offset of 1/2 of element's height. If the offset wasn't reduced, // top area would cover most (all) cases. var fixedOffset = Math.min( triggerOffset, 0 | ( element.size.outerHeight / 2 ) ), // This variable will hold the trigger to be returned. triggerSetup = [], triggerLook, // This flag determines whether dealing with a bottom trigger. bottomTrigger; // \-> Top trigger. if ( inBetween( mouse.y, element.size.top - 1, element.size.top + fixedOffset ) ) bottomTrigger = false; // \-> Bottom trigger. else if ( inBetween( mouse.y, element.size.bottom - fixedOffset, element.size.bottom + 1 ) ) bottomTrigger = true; // \-> Abort. Not in a valid trigger space. else { return null; } // Reject wrong elements. // \-> Reject an element which is a flow breaker. // \-> Reject an element which has a child above/below the mouse pointer. // \-> Reject an element which belongs to list items. if ( isFlowBreaker( element ) || isChildBetweenPointerAndEdge( that, element, bottomTrigger ) || element.getParent().is( DTD_LISTITEM ) ) { return null; } // Get sibling according to bottomTrigger. var elementSibling = getNonEmptyNeighbour( that, element, !bottomTrigger ); // No sibling element. // This is a first or last child case. if ( !elementSibling ) { // No need to reject the element as it has already been done before. // Prepare a trigger. // Determine trigger look. if ( element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ) { updateEditableSize( that ); if ( bottomTrigger && inBetween( mouse.y, element.size.bottom - fixedOffset, view.pane.height ) && inBetween( element.size.bottom, view.pane.height - fixedOffset, view.pane.height ) ) { triggerLook = LOOK_BOTTOM; } else if ( inBetween( mouse.y, 0, element.size.top + fixedOffset ) ) { triggerLook = LOOK_TOP; } } else { triggerLook = LOOK_NORMAL; } triggerSetup = [ null, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [ bottomTrigger ? EDGE_BOTTOM : EDGE_TOP, TYPE_EDGE, triggerLook, element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ? ( bottomTrigger ? LOOK_BOTTOM : LOOK_TOP ) : LOOK_NORMAL ] ); } // Abort. Sibling is a text element. else if ( isTextNode( elementSibling ) ) { return null; } // Check if the sibling is a HTML element. // If so, create an TYPE_EDGE, EDGE_MIDDLE trigger. else if ( isHtml( elementSibling ) ) { // Reject wrong elementSiblings. // \-> Reject an elementSibling which is a flow breaker. // \-> Reject an elementSibling which isn't a trigger. // \-> Reject an elementSibling which belongs to list items. if ( isFlowBreaker( elementSibling ) || !isTrigger( that, elementSibling ) || elementSibling.getParent().is( DTD_LISTITEM ) ) { return null; } // Prepare a trigger. triggerSetup = [ elementSibling, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [ EDGE_MIDDLE, TYPE_EDGE ] ); } if ( 0 in triggerSetup ) { return new boxTrigger( triggerSetup ); } return null; } // Checks iteratively up and down in search for elements using elementFromMouse method. // Useful if between two triggers. // // +----------------------- Parent element -+ // | +----------------------- Element #1 -+ | // | | | | // | | | | // | | | | // | +------------------------------------+ | // | | /-- // | . | | // | . +-- Floated -+ | | // | | | | | | * Mouse activation area * // | | | IGNORE | | | // | X | | | | Method searches vertically for sibling elements. // | | +------------+ | | Start point is X (mouse-y coordinate). // | | | | Floated elements, comments and empty text nodes are omitted. // | . | | // | . | | // | | \-- // | +----------------------- Element #2 -+ | // | | | | // | | | | // | | | | // | | | | // | +------------------------------------+ | // +----------------------------------------+ // var triggerExpand = ( function() { // The heart of the procedure. This method creates triggers that are // filtered by expandFilter method. function expandEngine( that ) { var startElement = that.element, upper, lower, trigger; if ( !isHtml( startElement ) || startElement.contains( that.editable ) ) { return null; } // Stop searching if element is in non-editable branch of DOM. if ( startElement.isReadOnly() ) return null; trigger = verticalSearch( that, function( current, startElement ) { return !startElement.equals( current ); // stop when start element and the current one differ }, function( that, mouse ) { return elementFromMouse( that, true, mouse ); }, startElement ), upper = trigger.upper, lower = trigger.lower; // Success: two siblings have been found if ( areSiblings( that, upper, lower ) ) { return trigger.set( EDGE_MIDDLE, TYPE_EXPAND ); } // Danger. Dragons ahead. // No siblings have been found during previous phase, post-processing may be necessary. // We can traverse DOM until a valid pair of elements around the pointer is found. // Prepare for post-processing: // 1. Determine if upper and lower are children of startElement. // 1.1. If so, find their ascendants that are closest to startElement (one level deeper than startElement). // 1.2. Otherwise use first/last-child of the startElement as upper/lower. Why?: // a) upper/lower belongs to another branch of the DOM tree. // b) verticalSearch encountered an edge of the viewport and failed. // 1.3. Make sure upper and lower still exist. Why?: // a) Upper and lower may be not belong to the branch of the startElement (may not exist at all) and // startElement has no children. // 2. Perform the post-processing. // 2.1. Gather dimensions of an upper element. // 2.2. Abort if lower edge of upper is already under the mouse pointer. Why?: // a) We expect upper to be above and lower below the mouse pointer. // 3. Perform iterative search while upper != lower. // 3.1. Find the upper-next element. If there's no such element, break current search. Why?: // a) There's no point in further search if there are only text nodes ahead. // 3.2. Calculate the distance between the middle point of ( upper, upperNext ) and mouse-y. // 3.3. If the distance is shorter than the previous best, save it (save upper, upperNext as well). // 3.4. If the optimal pair is found, assign it back to the trigger. // 1.1., 1.2. if ( upper && startElement.contains( upper ) ) { while ( !upper.getParent().equals( startElement ) ) upper = upper.getParent(); } else { upper = startElement.getFirst( function( node ) { return expandSelector( that, node ); } ); } if ( lower && startElement.contains( lower ) ) { while ( !lower.getParent().equals( startElement ) ) lower = lower.getParent(); } else { lower = startElement.getLast( function( node ) { return expandSelector( that, node ); } ); } // 1.3. if ( !upper || !lower ) { return null; } // 2.1. updateSize( that, upper ); updateSize( that, lower ); if ( !checkMouseBetweenElements( that, upper, lower ) ) { return null; } var minDistance = Number.MAX_VALUE, currentDistance, upperNext, minElement, minElementNext; while ( lower && !lower.equals( upper ) ) { // 3.1. if ( !( upperNext = upper.getNext( that.isRelevant ) ) ) break; // 3.2. currentDistance = Math.abs( getMidpoint( that, upper, upperNext ) - that.mouse.y ); // 3.3. if ( currentDistance < minDistance ) { minDistance = currentDistance; minElement = upper; minElementNext = upperNext; } upper = upperNext; updateSize( that, upper ); } // 3.4. if ( !minElement || !minElementNext ) { return null; } if ( !checkMouseBetweenElements( that, minElement, minElementNext ) ) { return null; } // An element of minimal distance has been found. Assign it to the trigger. trigger.upper = minElement; trigger.lower = minElementNext; // Success: post-processing revealed a pair of elements. return trigger.set( EDGE_MIDDLE, TYPE_EXPAND ); } // This is default element selector used by the engine. function expandSelector( that, node ) { return !( isTextNode( node ) || isComment( node ) || isFlowBreaker( node ) || isLine( that, node ) || ( node.type == CKEDITOR.NODE_ELEMENT && node.$ && node.is( 'br' ) ) ); } // This method checks whether mouse-y is between the top edge of upper // and bottom edge of lower. // // NOTE: This method assumes that updateSize has already been called // for the elements and is up-to-date. // // +---------------------------- Upper -+ /-- // | | | // +------------------------------------+ | // | // ... | // | // X | * Return true for mouse-y in this range * // | // ... | // | // +---------------------------- Lower -+ | // | | | // +------------------------------------+ \-- // function checkMouseBetweenElements( that, upper, lower ) { return inBetween( that.mouse.y, upper.size.top, lower.size.bottom ); } // A method for trigger filtering. Accepts or rejects trigger pairs // by their location in DOM etc. function expandFilter( that, trigger ) { var upper = trigger.upper, lower = trigger.lower; if ( !upper || !lower || // NOT: EDGE_MIDDLE trigger ALWAYS has two elements. isFlowBreaker( lower ) || isFlowBreaker( upper ) || // NOT: one of the elements is floated or positioned lower.equals( upper ) || upper.equals( lower ) || // NOT: two trigger elements, one equals another. lower.contains( upper ) || upper.contains( lower ) ) { // NOT: two trigger elements, one contains another. return false; } // YES: two trigger elements, pure siblings. else if ( isTrigger( that, upper ) && isTrigger( that, lower ) && areSiblings( that, upper, lower ) ) { return true; } return false; } // Simple wrapper for expandEngine and expandFilter. return function( that ) { var trigger = expandEngine( that ); return trigger && expandFilter( that, trigger ) ? trigger : null; }; } )(); // Collects dimensions of an element. var sizePrefixes = [ 'top', 'left', 'right', 'bottom' ]; function getSize( that, element, ignoreScroll, force ) { var docPosition = element.getDocumentPosition(), border = {}, margin = {}, padding = {}, box = {}; for ( var i = sizePrefixes.length; i--; ) { border[ sizePrefixes[ i ] ] = parseInt( getStyle( 'border-' + sizePrefixes[ i ] + '-width' ), 10 ) || 0; padding[ sizePrefixes[ i ] ] = parseInt( getStyle( 'padding-' + sizePrefixes[ i ] ), 10 ) || 0; margin[ sizePrefixes[ i ] ] = parseInt( getStyle( 'margin-' + sizePrefixes[ i ] ), 10 ) || 0; } // updateWindowSize if forced to do so OR NOT ignoring scroll. if ( !ignoreScroll || force ) updateWindowSize( that, force ); box.top = docPosition.y - ( ignoreScroll ? 0 : that.view.scroll.y ), box.left = docPosition.x - ( ignoreScroll ? 0 : that.view.scroll.x ), // w/ borders and paddings. box.outerWidth = element.$.offsetWidth, box.outerHeight = element.$.offsetHeight, // w/o borders and paddings. box.height = box.outerHeight - ( padding.top + padding.bottom + border.top + border.bottom ), box.width = box.outerWidth - ( padding.left + padding.right + border.left + border.right ), box.bottom = box.top + box.outerHeight, box.right = box.left + box.outerWidth; if ( that.inInlineMode ) { box.scroll = { top: element.$.scrollTop, left: element.$.scrollLeft }; } return extend( { border: border, padding: padding, margin: margin, ignoreScroll: ignoreScroll }, box, true ); function getStyle( propertyName ) { return element.getComputedStyle.call( element, propertyName ); } } function updateSize( that, element, ignoreScroll ) { if ( !isHtml( element ) ) // i.e. an element is hidden return ( element.size = null ); // -> reset size to make it useless for other methods if ( !element.size ) element.size = {}; // Abort if there was a similar query performed recently. // This kind of caching provides great performance improvement. else if ( element.size.ignoreScroll == ignoreScroll && element.size.date > new Date() - CACHE_TIME ) { return null; } return extend( element.size, getSize( that, element, ignoreScroll ), { date: +new Date() }, true ); } // Updates that.view.editable object. // This one must be called separately outside of updateWindowSize // to prevent cyclic dependency getSize<->updateWindowSize. // It calls getSize with force flag to avoid getWindowSize cache (look: getSize). function updateEditableSize( that, ignoreScroll ) { that.view.editable = getSize( that, that.editable, ignoreScroll, true ); } function updateWindowSize( that, force ) { if ( !that.view ) that.view = {}; var view = that.view; if ( !force && view && view.date > new Date() - CACHE_TIME ) { return; } var win = that.win, scroll = win.getScrollPosition(), paneSize = win.getViewPaneSize(); extend( that.view, { scroll: { x: scroll.x, y: scroll.y, width: that.doc.$.documentElement.scrollWidth - paneSize.width, height: that.doc.$.documentElement.scrollHeight - paneSize.height }, pane: { width: paneSize.width, height: paneSize.height, bottom: paneSize.height + scroll.y }, date: +new Date() }, true ); } // This method searches document vertically using given // select criterion until stop criterion is fulfilled. function verticalSearch( that, stopCondition, selectCriterion, startElement ) { var upper = startElement, lower = startElement, mouseStep = 0, upperFound = false, lowerFound = false, viewPaneHeight = that.view.pane.height, mouse = that.mouse; while ( mouse.y + mouseStep < viewPaneHeight && mouse.y - mouseStep > 0 ) { if ( !upperFound ) upperFound = stopCondition( upper, startElement ); if ( !lowerFound ) lowerFound = stopCondition( lower, startElement ); // Still not found... if ( !upperFound && mouse.y - mouseStep > 0 ) upper = selectCriterion( that, { x: mouse.x, y: mouse.y - mouseStep } ); if ( !lowerFound && mouse.y + mouseStep < viewPaneHeight ) lower = selectCriterion( that, { x: mouse.x, y: mouse.y + mouseStep } ); if ( upperFound && lowerFound ) break; // Instead of ++ to reduce the number of invocations by half. // It's trades off accuracy in some edge cases for improved performance. mouseStep += 2; } return new boxTrigger( [ upper, lower, null, null ] ); } } )(); /** * Sets the default vertical distance between the edge of the element and the mouse pointer that * causes the magic line to appear. This option accepts a value in pixels, without the unit (for example: * `15` for 15 pixels). * * Read more in the {@glink features/magicline documentation} * and see the {@glink examples/magicline example}. * * // Changes the offset to 15px. * CKEDITOR.config.magicline_triggerOffset = 15; * * @cfg {Number} [magicline_triggerOffset=30] * @member CKEDITOR.config * @see CKEDITOR.config#magicline_holdDistance */ /** * Defines the distance between the mouse pointer and the box within * which the magic line stays revealed and no other focus space is offered to be accessed. * This value is relative to {@link #magicline_triggerOffset}. * * Read more in the {@glink features/magicline documentation} * and see the {@glink examples/magicline example}. * * // Increases the distance to 80% of CKEDITOR.config.magicline_triggerOffset. * CKEDITOR.config.magicline_holdDistance = .8; * * @cfg {Number} [magicline_holdDistance=0.5] * @member CKEDITOR.config * @see CKEDITOR.config#magicline_triggerOffset */ /** * Defines the default keystroke that accesses the closest unreachable focus space **before** * the caret (start of the selection). If there is no focus space available, the selection remains unchanged. * * Read more in the {@glink features/magicline documentation} * and see the {@glink examples/magicline example}. * * // Changes the default keystroke to "Ctrl + ,". * CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + 188; * * @cfg {Number} [magicline_keystrokePrevious=CKEDITOR.CTRL + CKEDITOR.SHIFT + 51 (CTRL + SHIFT + 3)] * @member CKEDITOR.config */ CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + CKEDITOR.SHIFT + 51; // CTRL + SHIFT + 3 /** * Defines the default keystroke that accesses the closest unreachable focus space **after** * the caret (start of the selection). If there is no focus space available, the selection remains unchanged. * * Read more in the {@glink features/magicline documentation} * and see the {@glink examples/magicline example}. * * // Changes keystroke to "Ctrl + .". * CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + 190; * * @cfg {Number} [magicline_keystrokeNext=CKEDITOR.CTRL + CKEDITOR.SHIFT + 52 (CTRL + SHIFT + 4)] * @member CKEDITOR.config */ CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + CKEDITOR.SHIFT + 52; // CTRL + SHIFT + 4 /** * Defines a list of attributes that, if assigned to some elements, prevent the magic line from being * used within these elements. * * Read more in the {@glink features/magicline documentation} * and see the {@glink examples/magicline example}. * * // Adds the "data-tabu" attribute to the magic line tabu list. * CKEDITOR.config.magicline_tabuList = [ 'data-tabu' ]; * * @cfg {Number} [magicline_tabuList=[ 'data-widget-wrapper' ]] * @member CKEDITOR.config */ /** * Defines the color of the magic line. The color may be adjusted to enhance readability. * * Read more in the {@glink features/magicline documentation} * and see the {@glink examples/magicline example}. * * // Changes magic line color to blue. * CKEDITOR.config.magicline_color = '#0000FF'; * * @cfg {String} [magicline_color='#FF0000'] * @member CKEDITOR.config */ /** * Activates the special all-encompassing mode that considers all focus spaces between * {@link CKEDITOR.dtd#$block} elements as accessible by the magic line. * * Read more in the {@glink features/magicline documentation} * and see the {@glink examples/magicline example}. * * // Enables the greedy "put everywhere" mode. * CKEDITOR.config.magicline_everywhere = true; * * @cfg {Boolean} [magicline_everywhere=false] * @member CKEDITOR.config */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ ( function() { function protectFormStyles( formElement ) { if ( !formElement || formElement.type != CKEDITOR.NODE_ELEMENT || formElement.getName() != 'form' ) return []; var hijackRecord = [], hijackNames = [ 'style', 'className' ]; for ( var i = 0; i < hijackNames.length; i++ ) { var name = hijackNames[ i ]; var $node = formElement.$.elements.namedItem( name ); if ( $node ) { var hijackNode = new CKEDITOR.dom.element( $node ); hijackRecord.push( [ hijackNode, hijackNode.nextSibling ] ); hijackNode.remove(); } } return hijackRecord; } function restoreFormStyles( formElement, hijackRecord ) { if ( !formElement || formElement.type != CKEDITOR.NODE_ELEMENT || formElement.getName() != 'form' ) return; if ( hijackRecord.length > 0 ) { for ( var i = hijackRecord.length - 1; i >= 0; i-- ) { var node = hijackRecord[ i ][ 0 ]; var sibling = hijackRecord[ i ][ 1 ]; if ( sibling ) node.insertBefore( sibling ); else node.appendTo( formElement ); } } } function saveStyles( element, isInsideEditor ) { var data = protectFormStyles( element ); var retval = {}; var $element = element.$; if ( !isInsideEditor ) { retval[ 'class' ] = $element.className || ''; $element.className = ''; } retval.inline = $element.style.cssText || ''; if ( !isInsideEditor ) // Reset any external styles that might interfere. (https://dev.ckeditor.com/ticket/2474) $element.style.cssText = 'position: static; overflow: visible'; restoreFormStyles( data ); return retval; } function restoreStyles( element, savedStyles ) { var data = protectFormStyles( element ); var $element = element.$; if ( 'class' in savedStyles ) $element.className = savedStyles[ 'class' ]; if ( 'inline' in savedStyles ) $element.style.cssText = savedStyles.inline; restoreFormStyles( data ); } function refreshCursor( editor ) { if ( editor.editable().isInline() ) return; // Refresh all editor instances on the page (https://dev.ckeditor.com/ticket/5724). var all = CKEDITOR.instances; for ( var i in all ) { var one = all[ i ]; if ( one.mode == 'wysiwyg' && !one.readOnly ) { var body = one.document.getBody(); // Refresh 'contentEditable' otherwise // DOM lifting breaks design mode. (https://dev.ckeditor.com/ticket/5560) body.setAttribute( 'contentEditable', false ); body.setAttribute( 'contentEditable', true ); } } if ( editor.editable().hasFocus ) { editor.toolbox.focus(); editor.focus(); } } CKEDITOR.plugins.add( 'maximize', { // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { // Maximize plugin isn't available in inline mode yet. if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) return; var lang = editor.lang; var mainDocument = CKEDITOR.document, mainWindow = mainDocument.getWindow(); // Saved selection and scroll position for the editing area. var savedSelection, savedScroll; // Saved scroll position for the outer window. var outerScroll; // Saved resize handler function. function resizeHandler() { var viewPaneSize = mainWindow.getViewPaneSize(); editor.resize( viewPaneSize.width, viewPaneSize.height, null, true ); } // Retain state after mode switches. var savedState = CKEDITOR.TRISTATE_OFF; editor.addCommand( 'maximize', { // Disabled on iOS (https://dev.ckeditor.com/ticket/8307). modes: { wysiwyg: !CKEDITOR.env.iOS, source: !CKEDITOR.env.iOS }, readOnly: 1, editorFocus: false, exec: function() { var container = editor.container.getFirst( function( node ) { return node.type == CKEDITOR.NODE_ELEMENT && node.hasClass( 'cke_inner' ); } ); var contents = editor.ui.space( 'contents' ); // Save current selection and scroll position in editing area. if ( editor.mode == 'wysiwyg' ) { var selection = editor.getSelection(); savedSelection = selection && selection.getRanges(); savedScroll = mainWindow.getScrollPosition(); } else { var $textarea = editor.editable().$; savedSelection = !CKEDITOR.env.ie && [ $textarea.selectionStart, $textarea.selectionEnd ]; savedScroll = [ $textarea.scrollLeft, $textarea.scrollTop ]; } // Go fullscreen if the state is off. if ( this.state == CKEDITOR.TRISTATE_OFF ) { // Add event handler for resizing. mainWindow.on( 'resize', resizeHandler ); // Save the scroll bar position. outerScroll = mainWindow.getScrollPosition(); // Save and reset the styles for the entire node tree. var currentNode = editor.container; while ( ( currentNode = currentNode.getParent() ) ) { currentNode.setCustomData( 'maximize_saved_styles', saveStyles( currentNode ) ); // Show under floatpanels (-1) and context menu (-2). currentNode.setStyle( 'z-index', editor.config.baseFloatZIndex - 5 ); } contents.setCustomData( 'maximize_saved_styles', saveStyles( contents, true ) ); container.setCustomData( 'maximize_saved_styles', saveStyles( container, true ) ); // Hide scroll bars. var styles = { overflow: CKEDITOR.env.webkit ? '' : 'hidden', // https://dev.ckeditor.com/ticket/6896 width: 0, height: 0 }; mainDocument.getDocumentElement().setStyles( styles ); !CKEDITOR.env.gecko && mainDocument.getDocumentElement().setStyle( 'position', 'fixed' ); !( CKEDITOR.env.gecko && CKEDITOR.env.quirks ) && mainDocument.getBody().setStyles( styles ); // Scroll to the top left (IE needs some time for it - https://dev.ckeditor.com/ticket/4923). CKEDITOR.env.ie ? setTimeout( function() { mainWindow.$.scrollTo( 0, 0 ); }, 0 ) : mainWindow.$.scrollTo( 0, 0 ); // Resize and move to top left. // Special treatment for FF Quirks (https://dev.ckeditor.com/ticket/7284) container.setStyle( 'position', CKEDITOR.env.gecko && CKEDITOR.env.quirks ? 'fixed' : 'absolute' ); container.$.offsetLeft; // SAFARI BUG: See https://dev.ckeditor.com/ticket/2066. container.setStyles( { // Show under floatpanels (-1) and context menu (-2). 'z-index': editor.config.baseFloatZIndex - 5, left: '0px', top: '0px' } ); // Add cke_maximized class before resize handle since that will change things sizes (https://dev.ckeditor.com/ticket/5580) container.addClass( 'cke_maximized' ); resizeHandler(); // Still not top left? Fix it. (Bug https://dev.ckeditor.com/ticket/174) var offset = container.getDocumentPosition(); container.setStyles( { left: ( -1 * offset.x ) + 'px', top: ( -1 * offset.y ) + 'px' } ); // Fixing positioning editor chrome in Firefox break design mode. (https://dev.ckeditor.com/ticket/5149) CKEDITOR.env.gecko && refreshCursor( editor ); } // Restore from fullscreen if the state is on. else if ( this.state == CKEDITOR.TRISTATE_ON ) { // Remove event handler for resizing. mainWindow.removeListener( 'resize', resizeHandler ); // Restore CSS styles for the entire node tree. var editorElements = [ contents, container ]; for ( var i = 0; i < editorElements.length; i++ ) { restoreStyles( editorElements[ i ], editorElements[ i ].getCustomData( 'maximize_saved_styles' ) ); editorElements[ i ].removeCustomData( 'maximize_saved_styles' ); } currentNode = editor.container; while ( ( currentNode = currentNode.getParent() ) ) { restoreStyles( currentNode, currentNode.getCustomData( 'maximize_saved_styles' ) ); currentNode.removeCustomData( 'maximize_saved_styles' ); } // Restore the window scroll position. CKEDITOR.env.ie ? setTimeout( function() { mainWindow.$.scrollTo( outerScroll.x, outerScroll.y ); }, 0 ) : mainWindow.$.scrollTo( outerScroll.x, outerScroll.y ); // Remove cke_maximized class. container.removeClass( 'cke_maximized' ); // Webkit requires a re-layout on editor chrome. (https://dev.ckeditor.com/ticket/6695) if ( CKEDITOR.env.webkit ) { container.setStyle( 'display', 'inline' ); setTimeout( function() { container.setStyle( 'display', 'block' ); }, 0 ); } // Emit a resize event, because this time the size is modified in // restoreStyles. editor.fire( 'resize', { outerHeight: editor.container.$.offsetHeight, contentsHeight: contents.$.offsetHeight, outerWidth: editor.container.$.offsetWidth } ); } this.toggleState(); // Toggle button label. var button = this.uiItems[ 0 ]; // Only try to change the button if it exists (https://dev.ckeditor.com/ticket/6166) if ( button ) { var label = ( this.state == CKEDITOR.TRISTATE_OFF ) ? lang.maximize.maximize : lang.maximize.minimize; var buttonNode = CKEDITOR.document.getById( button._.id ); buttonNode.getChild( 1 ).setHtml( label ); buttonNode.setAttribute( 'title', label ); buttonNode.setAttribute( 'href', 'javascript:void("' + label + '");' ); // jshint ignore:line } // Restore selection and scroll position in editing area. if ( editor.mode == 'wysiwyg' ) { if ( savedSelection ) { // Fixing positioning editor chrome in Firefox break design mode. (https://dev.ckeditor.com/ticket/5149) CKEDITOR.env.gecko && refreshCursor( editor ); editor.getSelection().selectRanges( savedSelection ); var element = editor.getSelection().getStartElement(); element && element.scrollIntoView( true ); } else { mainWindow.$.scrollTo( savedScroll.x, savedScroll.y ); } } else { if ( savedSelection ) { $textarea.selectionStart = savedSelection[ 0 ]; $textarea.selectionEnd = savedSelection[ 1 ]; } $textarea.scrollLeft = savedScroll[ 0 ]; $textarea.scrollTop = savedScroll[ 1 ]; } savedSelection = savedScroll = null; savedState = this.state; editor.fire( 'maximize', this.state ); }, canUndo: false } ); editor.ui.addButton && editor.ui.addButton( 'Maximize', { label: lang.maximize.maximize, command: 'maximize', toolbar: 'tools,10' } ); // Restore the command state after mode change, unless it has been changed to disabled (https://dev.ckeditor.com/ticket/6467) editor.on( 'mode', function() { var command = editor.getCommand( 'maximize' ); command.setState( command.state == CKEDITOR.TRISTATE_DISABLED ? CKEDITOR.TRISTATE_DISABLED : savedState ); }, null, null, 100 ); } } ); } )(); /** * Event fired when the maximize command is called. * It also indicates whether an editor is maximized or not. * * @event maximize * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param {Number} data Current state of the command. See {@link CKEDITOR#TRISTATE_ON} and {@link CKEDITOR#TRISTATE_OFF}. */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ ( function() { /* global confirm */ CKEDITOR.plugins.add( 'pastefromword', { requires: 'clipboard', // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { // Flag indicate this command is actually been asked instead of a generic pasting. var forceFromWord = 0, path = this.path, configInlineImages = editor.config.pasteFromWord_inlineImages === undefined ? true : editor.config.pasteFromWord_inlineImages; editor.addCommand( 'pastefromword', { // Snapshots are done manually by editable.insertXXX methods. canUndo: false, async: true, /** * The Paste from Word command. It will determine its pasted content from Word automatically if possible. * * At the time of writing it was working correctly only in Internet Explorer browsers, due to their * `paste` support in `document.execCommand`. * * @private * @param {CKEDITOR.editor} editor An instance of the editor where the command is being executed. * @param {Object} [data] The options object. * @param {Boolean/String} [data.notification=true] Content for a notification shown after an unsuccessful * paste attempt. If `false`, the notification will not be displayed. This parameter was added in 4.7.0. * @member CKEDITOR.editor.commands.pastefromword */ exec: function( editor, data ) { forceFromWord = 1; editor.execCommand( 'paste', { type: 'html', notification: data && typeof data.notification !== 'undefined' ? data.notification : true } ); } } ); // Register the toolbar button. CKEDITOR.plugins.clipboard.addPasteButton( editor, 'PasteFromWord', { label: editor.lang.pastefromword.toolbar, command: 'pastefromword', toolbar: 'clipboard,50' } ); // Features brought by this command beside the normal process: // 1. No more bothering of user about the clean-up. // 2. Perform the clean-up even if content is not from Microsoft Word. // (e.g. from a Microsoft Word similar application.) // 3. Listen with high priority (3), so clean up is done before content // type sniffing (priority = 6). editor.on( 'paste', function( evt ) { var data = evt.data, dataTransferHtml = CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ? data.dataTransfer.getData( 'text/html', true ) : null, // Required in Paste from Word Image plugin (#662). dataTransferRtf = CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ? data.dataTransfer.getData( 'text/rtf' ) : null, // Some commands fire paste event without setting dataTransfer property. In such case // dataValue should be used. mswordHtml = dataTransferHtml || data.dataValue, pfwEvtData = { dataValue: mswordHtml, dataTransfer: { 'text/rtf': dataTransferRtf } }, officeMetaRegexp = /|<\/font>)/, isOfficeContent = officeMetaRegexp.test( mswordHtml ) || wordRegexp.test( mswordHtml ); if ( !mswordHtml || !( forceFromWord || isOfficeContent ) ) { return; } // PFW might still get prevented, if it's not forced. if ( editor.fire( 'pasteFromWord', pfwEvtData ) === false && !forceFromWord ) { return; } // Do not apply paste filter to data filtered by the Word filter (https://dev.ckeditor.com/ticket/13093). data.dontFilter = true; // If filter rules aren't loaded then cancel 'paste' event, // load them and when they'll get loaded fire new paste event // for which data will be filtered in second execution of // this listener. var isLazyLoad = loadFilterRules( editor, path, function() { // Event continuation with the original data. if ( isLazyLoad ) { editor.fire( 'paste', data ); } else if ( !editor.config.pasteFromWordPromptCleanup || ( forceFromWord || confirm( editor.lang.pastefromword.confirmCleanup ) ) ) { pfwEvtData.dataValue = CKEDITOR.cleanWord( pfwEvtData.dataValue, editor ); editor.fire( 'afterPasteFromWord', pfwEvtData ); data.dataValue = pfwEvtData.dataValue; if ( editor.config.forcePasteAsPlainText === true ) { // If `config.forcePasteAsPlainText` set to true, force plain text even on Word content (#1013). data.type = 'text'; } else if ( !CKEDITOR.plugins.clipboard.isCustomCopyCutSupported && editor.config.forcePasteAsPlainText === 'allow-word' ) { // In browsers using pastebin when pasting from Word, evt.data.type is 'auto' (not 'html') so it gets converted // by 'pastetext' plugin to 'text'. We need to restore 'html' type (#1013) and (#1638). data.type = 'html'; } } // Reset forceFromWord. forceFromWord = 0; } ); // The cleanup rules are to be loaded, we should just cancel // this event. isLazyLoad && evt.cancel(); }, null, null, 3 ); // Paste From Word Image: // RTF clipboard is required for embedding images. // If img tags are not allowed there is no point to process images. if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported && configInlineImages ) { editor.on( 'afterPasteFromWord', imagePastingListener ); } function imagePastingListener( evt ) { var pfw = CKEDITOR.plugins.pastefromword && CKEDITOR.plugins.pastefromword.images, imgTags, hexImages, newSrcValues = [], i; // If pfw images namespace is unavailable or img tags are not allowed we simply skip adding images. if ( !pfw || !evt.editor.filter.check( 'img[src]' ) ) { return; } function createSrcWithBase64( img ) { return img.type ? 'data:' + img.type + ';base64,' + CKEDITOR.tools.convertBytesToBase64( CKEDITOR.tools.convertHexStringToBytes( img.hex ) ) : null; } imgTags = pfw.extractTagsFromHtml( evt.data.dataValue ); if ( imgTags.length === 0 ) { return; } hexImages = pfw.extractFromRtf( evt.data.dataTransfer[ 'text/rtf' ] ); if ( hexImages.length === 0 ) { return; } CKEDITOR.tools.array.forEach( hexImages, function( img ) { newSrcValues.push( createSrcWithBase64( img ) ); }, this ); // Assuming there is equal amount of Images in RTF and HTML source, so we can match them accordingly to the existing order. if ( imgTags.length === newSrcValues.length ) { for ( i = 0; i < imgTags.length; i++ ) { // Replace only `file` urls of images ( shapes get newSrcValue with null ). if ( ( imgTags[ i ].indexOf( 'file://' ) === 0 ) && newSrcValues[ i ] ) { evt.data.dataValue = evt.data.dataValue.replace( imgTags[ i ], newSrcValues[ i ] ); } } } } } } ); function loadFilterRules( editor, path, callback ) { var isLoaded = CKEDITOR.cleanWord; if ( isLoaded ) callback(); else { var filterFilePath = CKEDITOR.getUrl( editor.config.pasteFromWordCleanupFile || ( path + 'filter/default.js' ) ); // Load with busy indicator. CKEDITOR.scriptLoader.load( filterFilePath, callback, null, true ); } return !isLoaded; } } )(); /** * Whether to prompt the user about the clean up of content being pasted from Microsoft Word. * * config.pasteFromWordPromptCleanup = true; * * @since 3.1.0 * @cfg {Boolean} [pasteFromWordPromptCleanup=false] * @member CKEDITOR.config */ /** * The file that provides the Microsoft Word cleanup function for pasting operations. * * **Note:** This is a global configuration shared by all editor instances present * on the page. * * // Load from the 'pastefromword' plugin 'filter' sub folder (custom.js file) using a path relative to the CKEditor installation folder. * CKEDITOR.config.pasteFromWordCleanupFile = 'plugins/pastefromword/filter/custom.js'; * * // Load from the 'pastefromword' plugin 'filter' sub folder (custom.js file) using a full path (including the CKEditor installation folder). * CKEDITOR.config.pasteFromWordCleanupFile = '/ckeditor/plugins/pastefromword/filter/custom.js'; * * // Load custom.js file from the 'customFilters' folder (located in server's root) using the full URL. * CKEDITOR.config.pasteFromWordCleanupFile = 'http://my.example.com/customFilters/custom.js'; * * @since 3.1.0 * @cfg {String} [pasteFromWordCleanupFile= + 'filter/default.js'] * @member CKEDITOR.config */ /** * Flag decides whether embedding images pasted with Word content is enabled or not. * * **Note:** Please be aware that embedding images requires Clipboard API support, available only in modern browsers, that is indicated by * {@link CKEDITOR.plugins.clipboard#isCustomDataTypesSupported} flag. * * // Disable embedding images pasted from Word. * config.pasteFromWord_inlineImages = false; * * @since 4.8.0 * @cfg {Boolean} [pasteFromWord_inlineImages=true] * @member CKEDITOR.config */ /** * Whether pasted element `margin` style that equals to 0 should be removed. * * // Disable removing `margin:0`, `margin-left:0`, etc. * config.pasteFromWord_keepZeroMargins = true; * * @since 4.12.0 * @cfg {Boolean} [pasteFromWord_keepZeroMargins=false] * @member CKEDITOR.config */ /** * Fired when the pasted content was recognized as Microsoft Word content. * * This event is cancellable. If canceled, it will prevent Paste from Word processing. * * @since 4.6.0 * @event pasteFromWord * @param data * @param {String} data.dataValue Pasted content. Changes to this property will affect the pasted content. * @member CKEDITOR.editor */ /** * Fired after the Paste form Word filters have been applied. * * @since 4.6.0 * @event afterPasteFromWord * @param data * @param {String} data.dataValue Pasted content after processing. Changes to this property will affect the pasted content. * @member CKEDITOR.editor */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview The Paste as plain text plugin. */ ( function() { // The pastetext command definition. var pasteTextCmd = { // Snapshots are done manually by editable.insertXXX methods. canUndo: false, async: true, /** * The Paste as plain text command. It will determine its pasted text automatically if possible. * * At the time of writing it was working correctly only in Internet Explorer browsers, due to their * `paste` support in `document.execCommand`. * * @private * @param {CKEDITOR.editor} editor An instance of the editor where the command is being executed. * @param {Object} [data] The options object. * @param {Boolean/String} [data.notification=true] Content for a notification shown after an unsuccessful * paste attempt. If `false`, the notification will not be displayed. This parameter was added in 4.7.0. * @member CKEDITOR.editor.commands.pastetext */ exec: function( editor, data ) { var lang = editor.lang, // In IE we must display keystroke for `paste` command as blocked `pastetext` // can fallback only to native paste. keyInfo = CKEDITOR.tools.keystrokeToString( lang.common.keyboard, editor.getCommandKeystroke( CKEDITOR.env.ie ? editor.commands.paste : this ) ), notification = ( data && typeof data.notification !== 'undefined' ) ? data.notification : !data || !data.from || ( data.from === 'keystrokeHandler' && CKEDITOR.env.ie ), msg = ( notification && typeof notification === 'string' ) ? notification : lang.pastetext.pasteNotification .replace( /%1/, '' + keyInfo.display + '' ); editor.execCommand( 'paste', { type: 'text', notification: notification ? msg : false } ); } }; // Register the plugin. CKEDITOR.plugins.add( 'pastetext', { requires: 'clipboard', // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { var commandName = 'pastetext', pasteKeystroke = !CKEDITOR.env.safari ? CKEDITOR.CTRL + CKEDITOR.SHIFT + 86 : // Ctrl + Shift + V CKEDITOR.CTRL + CKEDITOR.ALT + CKEDITOR.SHIFT + 86; // Ctrl + Shift + Alt + V editor.addCommand( commandName, pasteTextCmd ); editor.setKeystroke( pasteKeystroke, commandName ); CKEDITOR.plugins.clipboard.addPasteButton( editor, 'PasteText', { label: editor.lang.pastetext.button, command: commandName, toolbar: 'clipboard,40' } ); if ( editor.config.forcePasteAsPlainText ) { editor.on( 'beforePaste', function( evt ) { // Do NOT overwrite if HTML format is explicitly requested. // This allows pastefromword dominates over pastetext. if ( evt.data.type != 'html' ) { evt.data.type = 'text'; } } ); } editor.on( 'pasteState', function( evt ) { editor.getCommand( commandName ).setState( evt.data ); } ); } } ); } )(); /** * Whether to force all pasting operations to insert plain text into the * editor, losing any formatting information possibly available in the source text. * * This option accepts the following settings: * * * `true` – Pastes all content as plain text. * * `false` – Preserves content formatting. * * `allow-word` – Content pasted from Microsoft Word will keep its formatting * while any other content will be pasted as plain text. * * Example: * * // All content will be pasted as plain text. * config.forcePasteAsPlainText = true; * * // Only Microsoft Word content formatting will be preserved. * config.forcePasteAsPlainText = 'allow-word'; * * @cfg {Boolean/String} [forcePasteAsPlainText=false] * @member CKEDITOR.config */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview Print Plugin */ CKEDITOR.plugins.add( 'print', { // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { // Print plugin isn't available in inline mode yet. if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) return; var pluginName = 'print'; // Register the command. editor.addCommand( pluginName, CKEDITOR.plugins.print ); // Register the toolbar button. editor.ui.addButton && editor.ui.addButton( 'Print', { label: editor.lang.print.toolbar, command: pluginName, toolbar: 'document,50' } ); } } ); CKEDITOR.plugins.print = { exec: function( editor ) { if ( CKEDITOR.env.gecko ) { editor.window.$.print(); } else { editor.document.$.execCommand( 'Print' ); } }, canUndo: false, readOnly: 1, modes: { wysiwyg: 1 } }; /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'removeformat', { // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { editor.addCommand( 'removeFormat', CKEDITOR.plugins.removeformat.commands.removeformat ); editor.ui.addButton && editor.ui.addButton( 'RemoveFormat', { label: editor.lang.removeformat.toolbar, command: 'removeFormat', toolbar: 'cleanup,10' } ); } } ); CKEDITOR.plugins.removeformat = { commands: { removeformat: { exec: function( editor ) { var tagsRegex = editor._.removeFormatRegex || ( editor._.removeFormatRegex = new RegExp( '^(?:' + editor.config.removeFormatTags.replace( /,/g, '|' ) + ')$', 'i' ) ); var removeAttributes = editor._.removeAttributes || ( editor._.removeAttributes = editor.config.removeFormatAttributes.split( ',' ) ), filter = CKEDITOR.plugins.removeformat.filter, ranges = editor.getSelection().getRanges(), iterator = ranges.createIterator(), isElement = function( element ) { return element.type == CKEDITOR.NODE_ELEMENT; }, newRanges = [], range; while ( ( range = iterator.getNextRange() ) ) { var bookmarkForRangeRecreation = range.createBookmark(); range = editor.createRange(); range.setStartBefore( bookmarkForRangeRecreation.startNode ); bookmarkForRangeRecreation.endNode && range.setEndAfter( bookmarkForRangeRecreation.endNode ); if ( !range.collapsed ) range.enlarge( CKEDITOR.ENLARGE_ELEMENT ); // Bookmark the range so we can re-select it after processing. var bookmark = range.createBookmark(), // The style will be applied within the bookmark boundaries. startNode = bookmark.startNode, endNode = bookmark.endNode, currentNode; // We need to check the selection boundaries (bookmark spans) to break // the code in a way that we can properly remove partially selected nodes. // For example, removing a style from // This is [some text to show the] problem // ... where [ and ] represent the selection, must result: // This is [some text to show the] problem // The strategy is simple, we just break the partial nodes before the // removal logic, having something that could be represented this way: // This is [some text to show the] problem var breakParent = function( node ) { // Let's start checking the start boundary. var path = editor.elementPath( node ), pathElements = path.elements; for ( var i = 1, pathElement; pathElement = pathElements[ i ]; i++ ) { if ( pathElement.equals( path.block ) || pathElement.equals( path.blockLimit ) ) break; // If this element can be removed (even partially). if ( tagsRegex.test( pathElement.getName() ) && filter( editor, pathElement ) ) node.breakParent( pathElement ); } }; breakParent( startNode ); if ( endNode ) { breakParent( endNode ); // Navigate through all nodes between the bookmarks. currentNode = startNode.getNextSourceNode( true, CKEDITOR.NODE_ELEMENT ); while ( currentNode ) { // If we have reached the end of the selection, stop looping. if ( currentNode.equals( endNode ) ) break; if ( currentNode.isReadOnly() ) { // In case of non-editable we're skipping to the next sibling *elmenet*. // We need to be aware that endNode can be nested within current non-editable. // This condition tests if currentNode (non-editable) contains endNode. If it does // then we should break the filtering if ( currentNode.getPosition( endNode ) & CKEDITOR.POSITION_CONTAINS ) { break; } currentNode = currentNode.getNext( isElement ); continue; } // Cache the next node to be processed. Do it now, because // currentNode may be removed. var nextNode = currentNode.getNextSourceNode( false, CKEDITOR.NODE_ELEMENT ), isFakeElement = ( currentNode.getName() == 'img' && currentNode.data( 'cke-realelement' ) ) || currentNode.hasAttribute( 'data-cke-bookmark' ); // This node must not be a fake element, and must not be read-only. if ( !isFakeElement && filter( editor, currentNode ) ) { // Remove elements nodes that match with this style rules. if ( tagsRegex.test( currentNode.getName() ) ) currentNode.remove( 1 ); else { currentNode.removeAttributes( removeAttributes ); editor.fire( 'removeFormatCleanup', currentNode ); } } currentNode = nextNode; } } bookmark.startNode.remove(); bookmark.endNode && bookmark.endNode.remove(); range.moveToBookmark( bookmarkForRangeRecreation ); newRanges.push( range ); } // The selection path may not changed, but we should force a selection // change event to refresh command states, due to the above attribution change. (https://dev.ckeditor.com/ticket/9238) editor.forceNextSelectionCheck(); editor.getSelection().selectRanges( newRanges ); } } }, // Perform the remove format filters on the passed element. // @param {CKEDITOR.editor} editor // @param {CKEDITOR.dom.element} element filter: function( editor, element ) { // If editor#addRemoveFotmatFilter hasn't been executed yet value is not initialized. var filters = editor._.removeFormatFilters || []; for ( var i = 0; i < filters.length; i++ ) { if ( filters[ i ]( element ) === false ) return false; } return true; } }; /** * Add to a collection of functions to decide whether a specific * element should be considered as formatting element and thus * could be removed during `removeFormat` command. * * **Note:** Only available with the existence of `removeformat` plugin. * * // Don't remove empty span. * editor.addRemoveFormatFilter( function( element ) { * return !( element.is( 'span' ) && CKEDITOR.tools.isEmpty( element.getAttributes() ) ); * } ); * * @since 3.3.0 * @member CKEDITOR.editor * @param {Function} func The function to be called, which will be passed an {@link CKEDITOR.dom.element element} to test. */ CKEDITOR.editor.prototype.addRemoveFormatFilter = function( func ) { if ( !this._.removeFormatFilters ) this._.removeFormatFilters = []; this._.removeFormatFilters.push( func ); }; /** * A comma separated list of elements to be removed when executing the `remove * format` command. Note that only inline elements are allowed. * * @cfg * @member CKEDITOR.config */ CKEDITOR.config.removeFormatTags = 'b,big,cite,code,del,dfn,em,font,i,ins,kbd,q,s,samp,small,span,strike,strong,sub,sup,tt,u,var'; /** * A comma separated list of elements attributes to be removed when executing * the `remove format` command. * * @cfg * @member CKEDITOR.config */ CKEDITOR.config.removeFormatAttributes = 'class,style,lang,width,height,align,hspace,valign'; /** * Fired after an element was cleaned by the removeFormat plugin. * * @event removeFormatCleanup * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param data * @param {CKEDITOR.dom.element} data.element The element that was cleaned up. */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add( 'resize', { init: function( editor ) { function dragHandler( evt ) { var dx = evt.data.$.screenX - origin.x, dy = evt.data.$.screenY - origin.y, width = startSize.width, height = startSize.height, internalWidth = width + dx * ( resizeDir == 'rtl' ? -1 : 1 ), internalHeight = height + dy; if ( resizeHorizontal ) width = Math.max( config.resize_minWidth, Math.min( internalWidth, config.resize_maxWidth ) ); if ( resizeVertical ) height = Math.max( config.resize_minHeight, Math.min( internalHeight, config.resize_maxHeight ) ); // DO NOT impose fixed size with single direction resize. (https://dev.ckeditor.com/ticket/6308) editor.resize( resizeHorizontal ? width : null, height ); } function dragEndHandler() { CKEDITOR.document.removeListener( 'mousemove', dragHandler ); CKEDITOR.document.removeListener( 'mouseup', dragEndHandler ); if ( editor.document ) { editor.document.removeListener( 'mousemove', dragHandler ); editor.document.removeListener( 'mouseup', dragEndHandler ); } } var config = editor.config; var spaceId = editor.ui.spaceId( 'resizer' ); // Resize in the same direction of chrome, // which is identical to dir of editor element. (https://dev.ckeditor.com/ticket/6614) var resizeDir = editor.element ? editor.element.getDirection( 1 ) : 'ltr'; !config.resize_dir && ( config.resize_dir = 'vertical' ); ( config.resize_maxWidth === undefined ) && ( config.resize_maxWidth = 3000 ); ( config.resize_maxHeight === undefined ) && ( config.resize_maxHeight = 3000 ); ( config.resize_minWidth === undefined ) && ( config.resize_minWidth = 750 ); ( config.resize_minHeight === undefined ) && ( config.resize_minHeight = 250 ); if ( config.resize_enabled !== false ) { var container = null, origin, startSize, resizeHorizontal = ( config.resize_dir == 'both' || config.resize_dir == 'horizontal' ) && ( config.resize_minWidth != config.resize_maxWidth ), resizeVertical = ( config.resize_dir == 'both' || config.resize_dir == 'vertical' ) && ( config.resize_minHeight != config.resize_maxHeight ); var mouseDownFn = CKEDITOR.tools.addFunction( function( $event ) { if ( !container ) container = editor.getResizable(); startSize = { width: container.$.offsetWidth || 0, height: container.$.offsetHeight || 0 }; origin = { x: $event.screenX, y: $event.screenY }; config.resize_minWidth > startSize.width && ( config.resize_minWidth = startSize.width ); config.resize_minHeight > startSize.height && ( config.resize_minHeight = startSize.height ); CKEDITOR.document.on( 'mousemove', dragHandler ); CKEDITOR.document.on( 'mouseup', dragEndHandler ); if ( editor.document ) { editor.document.on( 'mousemove', dragHandler ); editor.document.on( 'mouseup', dragEndHandler ); } $event.preventDefault && $event.preventDefault(); } ); editor.on( 'destroy', function() { CKEDITOR.tools.removeFunction( mouseDownFn ); } ); editor.on( 'uiSpace', function( event ) { if ( event.data.space == 'bottom' ) { var direction = ''; if ( resizeHorizontal && !resizeVertical ) direction = ' cke_resizer_horizontal'; if ( !resizeHorizontal && resizeVertical ) direction = ' cke_resizer_vertical'; var resizerHtml = '' + // BLACK LOWER RIGHT TRIANGLE (ltr) // BLACK LOWER LEFT TRIANGLE (rtl) ( resizeDir == 'ltr' ? '\u25E2' : '\u25E3' ) + ''; // Always sticks the corner of botttom space. resizeDir == 'ltr' && direction == 'ltr' ? event.data.html += resizerHtml : event.data.html = resizerHtml + event.data.html; } }, editor, null, 100 ); // Toggle the visibility of the resizer when an editor is being maximized or minimized. editor.on( 'maximize', function( event ) { editor.ui.space( 'resizer' )[ event.data == CKEDITOR.TRISTATE_ON ? 'hide' : 'show' ](); } ); } } } ); /** * The minimum editor width, in pixels, when resizing the editor interface by using the resize handle. * Note: It falls back to editor's actual width if it is smaller than the default value. * * Read more in the {@glink features/resize documentation} * and see the {@glink examples/resize example}. * * config.resize_minWidth = 500; * * @cfg {Number} [resize_minWidth=750] * @member CKEDITOR.config */ /** * The minimum editor height, in pixels, when resizing the editor interface by using the resize handle. * Note: It falls back to editor's actual height if it is smaller than the default value. * * Read more in the {@glink features/resize documentation} * and see the {@glink examples/resize example}. * * config.resize_minHeight = 600; * * @cfg {Number} [resize_minHeight=250] * @member CKEDITOR.config */ /** * The maximum editor width, in pixels, when resizing the editor interface by using the resize handle. * * Read more in the {@glink features/resize documentation} * and see the {@glink examples/resize example}. * * config.resize_maxWidth = 750; * * @cfg {Number} [resize_maxWidth=3000] * @member CKEDITOR.config */ /** * The maximum editor height, in pixels, when resizing the editor interface by using the resize handle. * * Read more in the {@glink features/resize documentation} * and see the {@glink examples/resize example}. * * config.resize_maxHeight = 600; * * @cfg {Number} [resize_maxHeight=3000] * @member CKEDITOR.config */ /** * Whether to enable the resizing feature. If this feature is disabled, the resize handle will not be visible. * * Read more in the {@glink features/resize documentation} * and see the {@glink examples/resize example}. * * config.resize_enabled = false; * * @cfg {Boolean} [resize_enabled=true] * @member CKEDITOR.config */ /** * The dimensions for which the editor resizing is enabled. Possible values * are `both`, `vertical`, and `horizontal`. * * Read more in the {@glink features/resize documentation} * and see the {@glink examples/resize example}. * * config.resize_dir = 'both'; * * @since 3.3.0 * @cfg {String} [resize_dir='vertical'] * @member CKEDITOR.config */ /** * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview The Source Editing Area plugin. It registers the "source" editing * mode, which displays raw HTML data being edited in the editor. */ ( function() { CKEDITOR.plugins.add( 'sourcearea', { // jscs:disable maximumLineLength // jscs:enable maximumLineLength init: function( editor ) { // Source mode in inline editors is only available through the "sourcedialog" plugin. if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) return; var sourcearea = CKEDITOR.plugins.sourcearea; editor.addMode( 'source', function( callback ) { var contentsSpace = editor.ui.space( 'contents' ), textarea = contentsSpace.getDocument().createElement( 'textarea' ); textarea.setStyles( CKEDITOR.tools.extend( { // IE7 has overflow the