in the preview.
if ( elementName == 'bdo' )
elementName = 'span';
html = [ '<', elementName ];
// Assign all defined attributes.
var attribs = styleDefinition.attributes;
if ( attribs ) {
for ( var att in attribs )
html.push( ' ', att, '="', attribs[ att ], '"' );
}
// Assign the style attribute.
var cssStyle = CKEDITOR.style.getStyleText( styleDefinition );
if ( cssStyle )
html.push( ' style="', cssStyle, '"' );
html.push( '>', ( label || styleDefinition.name ), '', elementName, '>' );
return html.join( '' );
},
/**
* Returns the style definition.
*
* @since 4.1
* @returns {Object}
*/
getDefinition: function() {
return this._.definition;
}
/**
* If defined (for example by {@link CKEDITOR.style#addCustomHandler custom style handler}), it returns
* the {@link CKEDITOR.filter.allowedContentRules allowed content rules} which should be added to the
* {@link CKEDITOR.filter} when enabling this style.
*
* **Note:** This method is not defined in the {@link CKEDITOR.style} class.
*
* @since 4.4
* @method toAllowedContentRules
* @param {CKEDITOR.editor} [editor] The editor instance.
* @returns {CKEDITOR.filter.allowedContentRules} The rules that should represent this style in the {@link CKEDITOR.filter}.
*/
};
/**
* Builds the inline style text based on the style definition.
*
* @static
* @param styleDefinition
* @returns {String} Inline style text.
*/
CKEDITOR.style.getStyleText = function( styleDefinition ) {
// If we have already computed it, just return it.
var stylesDef = styleDefinition._ST;
if ( stylesDef )
return stylesDef;
stylesDef = styleDefinition.styles;
// Builds the StyleText.
var stylesText = ( styleDefinition.attributes && styleDefinition.attributes.style ) || '',
specialStylesText = '';
if ( stylesText.length )
stylesText = stylesText.replace( semicolonFixRegex, ';' );
for ( var style in stylesDef ) {
var styleVal = stylesDef[ style ],
text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' );
// Some browsers don't support 'inherit' property value, leave them intact. (#5242)
if ( styleVal == 'inherit' )
specialStylesText += text;
else
stylesText += text;
}
// Browsers make some changes to the style when applying them. So, here
// we normalize it to the browser format.
if ( stylesText.length )
stylesText = CKEDITOR.tools.normalizeCssText( stylesText, true );
stylesText += specialStylesText;
// Return it, saving it to the next request.
return ( styleDefinition._ST = stylesText );
};
/**
* Namespace containing custom style handlers added with {@link CKEDITOR.style#addCustomHandler}.
*
* @since 4.4
* @class
* @singleton
*/
CKEDITOR.style.customHandlers = {};
/**
* Creates a {@link CKEDITOR.style} subclass and registers it in the style system.
* Registered class will be used as a handler for a style of this type. This allows
* to extend the styles system, which by default uses only the {@link CKEDITOR.style}, with
* new functionality. Registered classes are accessible in the {@link CKEDITOR.style.customHandlers}.
*
* ### The Style Class Definition
*
* The definition object is used to override properties in a prototype inherited
* from the {@link CKEDITOR.style} class. It must contain a `type` property which is
* a name of the new type and therefore it must be unique. The default style types
* ({@link CKEDITOR#STYLE_BLOCK STYLE_BLOCK}, {@link CKEDITOR#STYLE_INLINE STYLE_INLINE},
* and {@link CKEDITOR#STYLE_OBJECT STYLE_OBJECT}) are integers, but for easier identification
* it is recommended to use strings as custom type names.
*
* Besides `type`, the definition may contain two more special properties:
*
* * `setup {Function}` – An optional callback executed when a style instance is created.
* Like the style constructor, it is executed in style context and with the style definition as an argument.
* * `assignedTo {Number}` – Can be set to one of the default style types. Some editor
* features like the Styles drop-down assign styles to one of the default groups based on
* the style type. By using this property it is possible to notify them to which group this
* custom style should be assigned. It defaults to the {@link CKEDITOR#STYLE_OBJECT}.
*
* Other properties of the definition object will just be used to extend the prototype inherited
* from the {@link CKEDITOR.style} class. So if the definition contains an `apply` method, it will
* override the {@link CKEDITOR.style#apply} method.
*
* ### Usage
*
* Registering a basic handler:
*
* var styleClass = CKEDITOR.style.addCustomHandler( {
* type: 'custom'
* } );
*
* var style = new styleClass( { ... } );
* style instanceof styleClass; // -> true
* style instanceof CKEDITOR.style; // -> true
* style.type; // -> 'custom'
*
* The {@link CKEDITOR.style} constructor used as a factory:
*
* var styleClass = CKEDITOR.style.addCustomHandler( {
* type: 'custom'
* } );
*
* // Style constructor accepts style definition (do not confuse with style class definition).
* var style = new CKEDITOR.style( { type: 'custom', attributes: ... } );
* style instanceof styleClass; // -> true
*
* Thanks to that, integration code using styles does not need to know
* which style handler it should use. It is determined by the {@link CKEDITOR.style} constructor.
*
* Overriding existing {@link CKEDITOR.style} methods:
*
* var styleClass = CKEDITOR.style.addCustomHandler( {
* type: 'custom',
* apply: function( editor ) {
* console.log( 'apply' );
* },
* remove: function( editor ) {
* console.log( 'remove' );
* }
* } );
*
* var style = new CKEDITOR.style( { type: 'custom', attributes: ... } );
* editor.applyStyle( style ); // logged 'apply'
*
* style = new CKEDITOR.style( { element: 'img', attributes: { 'class': 'foo' } } );
* editor.applyStyle( style ); // style is really applied if image was selected
*
* ### Practical Recommendations
*
* The style handling job, which includes such tasks as applying, removing, checking state, and
* checking if a style can be applied, is very complex. Therefore without deep knowledge
* about DOM and especially {@link CKEDITOR.dom.range ranges} and {@link CKEDITOR.dom.walker DOM walker} it is impossible
* to implement a completely custom style handler able to handle block, inline, and object type styles.
* However, it is possible to customize the default implementation by overriding default methods and
* reusing them.
*
* The only style handler which can be implemented from scratch without huge effort is a style
* applicable to objects ([read more about types](http://docs.ckeditor.com/#!/guide/dev_styles-section-style-types)).
* Such style can only be applied when a specific object is selected. An example implementation can
* be found in the [widget plugin](https://github.com/ckeditor/ckeditor-dev/blob/master/plugins/widget/plugin.js).
*
* When implementing a style handler from scratch at least the following methods must be defined:
*
* * {@link CKEDITOR.style#apply apply} and {@link CKEDITOR.style#remove remove},
* * {@link CKEDITOR.style#checkElementRemovable checkElementRemovable} and
* {@link CKEDITOR.style#checkElementMatch checkElementMatch} – Note that both methods reuse the same logic,
* * {@link CKEDITOR.style#checkActive checkActive} – Reuses
* {@link CKEDITOR.style#checkElementMatch checkElementMatch},
* * {@link CKEDITOR.style#toAllowedContentRules toAllowedContentRules} – Not required, but very useful in
* case of a custom style that has to notify the {@link CKEDITOR.filter} which rules it allows when registered.
*
* @since 4.4
* @static
* @member CKEDITOR.style
* @param definition The style class definition.
* @returns {CKEDITOR.style} The new style class created for the provided definition.
*/
CKEDITOR.style.addCustomHandler = function( definition ) {
var styleClass = function( styleDefinition ) {
this._ = {
definition: styleDefinition
};
if ( this.setup )
this.setup( styleDefinition );
};
styleClass.prototype = CKEDITOR.tools.extend(
// Prototype of CKEDITOR.style.
CKEDITOR.tools.prototypedCopy( CKEDITOR.style.prototype ),
// Defaults.
{
assignedTo: CKEDITOR.STYLE_OBJECT
},
// Passed definition - overrides.
definition,
true
);
this.customHandlers[ definition.type ] = styleClass;
return styleClass;
};
// Gets the parent element which blocks the styling for an element. This
// can be done through read-only elements (contenteditable=false) or
// elements with the "data-nostyle" attribute.
function getUnstylableParent( element, root ) {
var unstylable, editable;
while ( ( element = element.getParent() ) ) {
if ( element.equals( root ) )
break;
if ( element.getAttribute( 'data-nostyle' ) )
unstylable = element;
else if ( !editable ) {
var contentEditable = element.getAttribute( 'contentEditable' );
if ( contentEditable == 'false' )
unstylable = element;
else if ( contentEditable == 'true' )
editable = 1;
}
}
return unstylable;
}
var posPrecedingIdenticalContained =
CKEDITOR.POSITION_PRECEDING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED,
posFollowingIdenticalContained =
CKEDITOR.POSITION_FOLLOWING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED;
// Checks if the current node can be a child of the style element.
function checkIfNodeCanBeChildOfStyle( def, currentNode, lastNode, nodeName, dtd, nodeIsNoStyle, nodeIsReadonly, includeReadonly ) {
// Style can be applied to text node.
if ( !nodeName )
return 1;
// Style definitely cannot be applied if DTD or data-nostyle do not allow.
if ( !dtd[ nodeName ] || nodeIsNoStyle )
return 0;
// Non-editable element cannot be styled is we shouldn't include readonly elements.
if ( nodeIsReadonly && !includeReadonly )
return 0;
// Check that we haven't passed lastNode yet and that style's childRule allows this style on current element.
return checkPositionAndRule( currentNode, lastNode, def, posPrecedingIdenticalContained );
}
// Check if the style element can be a child of the current
// node parent or if the element is not defined in the DTD.
function checkIfStyleCanBeChildOf( def, currentParent, elementName, isUnknownElement ) {
return currentParent &&
( ( currentParent.getDtd() || CKEDITOR.dtd.span )[ elementName ] || isUnknownElement ) &&
( !def.parentRule || def.parentRule( currentParent ) );
}
function checkIfStartsRange( nodeName, currentNode, lastNode ) {
return (
!nodeName || !CKEDITOR.dtd.$removeEmpty[ nodeName ] ||
( currentNode.getPosition( lastNode ) | posPrecedingIdenticalContained ) == posPrecedingIdenticalContained
);
}
function checkIfTextOrReadonlyOrEmptyElement( currentNode, nodeIsReadonly ) {
var nodeType = currentNode.type;
return nodeType == CKEDITOR.NODE_TEXT || nodeIsReadonly || ( nodeType == CKEDITOR.NODE_ELEMENT && !currentNode.getChildCount() );
}
// Checks if position is a subset of posBitFlags and that nodeA fulfills style def rule.
function checkPositionAndRule( nodeA, nodeB, def, posBitFlags ) {
return ( nodeA.getPosition( nodeB ) | posBitFlags ) == posBitFlags &&
( !def.childRule || def.childRule( nodeA ) );
}
function applyInlineStyle( range ) {
var document = range.document;
if ( range.collapsed ) {
// Create the element to be inserted in the DOM.
var collapsedElement = getElement( this, document );
// Insert the empty element into the DOM at the range position.
range.insertNode( collapsedElement );
// Place the selection right inside the empty element.
range.moveToPosition( collapsedElement, CKEDITOR.POSITION_BEFORE_END );
return;
}
var elementName = this.element,
def = this._.definition,
isUnknownElement;
// Indicates that fully selected read-only elements are to be included in the styling range.
var ignoreReadonly = def.ignoreReadonly,
includeReadonly = ignoreReadonly || def.includeReadonly;
// If the read-only inclusion is not available in the definition, try
// to get it from the root data (most often it's the editable).
if ( includeReadonly == null )
includeReadonly = range.root.getCustomData( 'cke_includeReadonly' );
// Get the DTD definition for the element. Defaults to "span".
var dtd = CKEDITOR.dtd[ elementName ];
if ( !dtd ) {
isUnknownElement = true;
dtd = CKEDITOR.dtd.span;
}
// Expand the range.
range.enlarge( CKEDITOR.ENLARGE_INLINE, 1 );
range.trim();
// Get the first node to be processed and the last, which concludes the processing.
var boundaryNodes = range.createBookmark(),
firstNode = boundaryNodes.startNode,
lastNode = boundaryNodes.endNode,
currentNode = firstNode,
styleRange;
if ( !ignoreReadonly ) {
// Check if the boundaries are inside non stylable elements.
var root = range.getCommonAncestor(),
firstUnstylable = getUnstylableParent( firstNode, root ),
lastUnstylable = getUnstylableParent( lastNode, root );
// If the first element can't be styled, we'll start processing right
// after its unstylable root.
if ( firstUnstylable )
currentNode = firstUnstylable.getNextSourceNode( true );
// If the last element can't be styled, we'll stop processing on its
// unstylable root.
if ( lastUnstylable )
lastNode = lastUnstylable;
}
// Do nothing if the current node now follows the last node to be processed.
if ( currentNode.getPosition( lastNode ) == CKEDITOR.POSITION_FOLLOWING )
currentNode = 0;
while ( currentNode ) {
var applyStyle = false;
if ( currentNode.equals( lastNode ) ) {
currentNode = null;
applyStyle = true;
} else {
var nodeName = currentNode.type == CKEDITOR.NODE_ELEMENT ? currentNode.getName() : null,
nodeIsReadonly = nodeName && ( currentNode.getAttribute( 'contentEditable' ) == 'false' ),
nodeIsNoStyle = nodeName && currentNode.getAttribute( 'data-nostyle' );
// Skip bookmarks.
if ( nodeName && currentNode.data( 'cke-bookmark' ) ) {
currentNode = currentNode.getNextSourceNode( true );
continue;
}
// Find all nested editables of a non-editable block and apply this style inside them.
if ( nodeIsReadonly && includeReadonly && CKEDITOR.dtd.$block[ nodeName ] )
applyStyleOnNestedEditables.call( this, currentNode );
// Check if the current node can be a child of the style element.
if ( checkIfNodeCanBeChildOfStyle( def, currentNode, lastNode, nodeName, dtd, nodeIsNoStyle, nodeIsReadonly, includeReadonly ) ) {
var currentParent = currentNode.getParent();
// Check if the style element can be a child of the current
// node parent or if the element is not defined in the DTD.
if ( checkIfStyleCanBeChildOf( def, currentParent, elementName, isUnknownElement ) ) {
// This node will be part of our range, so if it has not
// been started, place its start right before the node.
// In the case of an element node, it will be included
// only if it is entirely inside the range.
if ( !styleRange && checkIfStartsRange( nodeName, currentNode, lastNode ) ) {
styleRange = range.clone();
styleRange.setStartBefore( currentNode );
}
// Non element nodes, readonly elements, or empty
// elements can be added completely to the range.
if ( checkIfTextOrReadonlyOrEmptyElement( currentNode, nodeIsReadonly ) ) {
var includedNode = currentNode;
var parentNode;
// This node is about to be included completelly, but,
// if this is the last node in its parent, we must also
// check if the parent itself can be added completelly
// to the range, otherwise apply the style immediately.
while (
( applyStyle = !includedNode.getNext( notBookmark ) ) &&
( parentNode = includedNode.getParent(), dtd[ parentNode.getName() ] ) &&
checkPositionAndRule( parentNode, firstNode, def, posFollowingIdenticalContained )
) {
includedNode = parentNode;
}
styleRange.setEndAfter( includedNode );
}
} else {
applyStyle = true;
}
}
// Style isn't applicable to current element, so apply style to
// range ending at previously chosen position, or nowhere if we haven't
// yet started styleRange.
else {
applyStyle = true;
}
// Get the next node to be processed.
// If we're currently on a non-editable element or non-styleable element,
// then we'll be moved to current node's sibling (or even further), so we'll
// avoid messing up its content.
currentNode = currentNode.getNextSourceNode( nodeIsNoStyle || nodeIsReadonly );
}
// Apply the style if we have something to which apply it.
if ( applyStyle && styleRange && !styleRange.collapsed ) {
// Build the style element, based on the style object definition.
var styleNode = getElement( this, document ),
styleHasAttrs = styleNode.hasAttributes();
// Get the element that holds the entire range.
var parent = styleRange.getCommonAncestor();
var removeList = {
styles: {},
attrs: {},
// Styles cannot be removed.
blockedStyles: {},
// Attrs cannot be removed.
blockedAttrs: {}
};
var attName, styleName, value;
// Loop through the parents, removing the redundant attributes
// from the element to be applied.
while ( styleNode && parent ) {
if ( parent.getName() == elementName ) {
for ( attName in def.attributes ) {
if ( removeList.blockedAttrs[ attName ] || !( value = parent.getAttribute( styleName ) ) )
continue;
if ( styleNode.getAttribute( attName ) == value )
removeList.attrs[ attName ] = 1;
else
removeList.blockedAttrs[ attName ] = 1;
}
for ( styleName in def.styles ) {
if ( removeList.blockedStyles[ styleName ] || !( value = parent.getStyle( styleName ) ) )
continue;
if ( styleNode.getStyle( styleName ) == value )
removeList.styles[ styleName ] = 1;
else
removeList.blockedStyles[ styleName ] = 1;
}
}
parent = parent.getParent();
}
for ( attName in removeList.attrs )
styleNode.removeAttribute( attName );
for ( styleName in removeList.styles )
styleNode.removeStyle( styleName );
if ( styleHasAttrs && !styleNode.hasAttributes() )
styleNode = null;
if ( styleNode ) {
// Move the contents of the range to the style element.
styleRange.extractContents().appendTo( styleNode );
// Insert it into the range position (it is collapsed after
// extractContents.
styleRange.insertNode( styleNode );
// Here we do some cleanup, removing all duplicated
// elements from the style element.
removeFromInsideElement.call( this, styleNode );
// Let's merge our new style with its neighbors, if possible.
styleNode.mergeSiblings();
// As the style system breaks text nodes constantly, let's normalize
// things for performance.
// With IE, some paragraphs get broken when calling normalize()
// repeatedly. Also, for IE, we must normalize body, not documentElement.
// IE is also known for having a "crash effect" with normalize().
// We should try to normalize with IE too in some way, somewhere.
if ( !CKEDITOR.env.ie )
styleNode.$.normalize();
}
// Style already inherit from parents, left just to clear up any internal overrides. (#5931)
else {
styleNode = new CKEDITOR.dom.element( 'span' );
styleRange.extractContents().appendTo( styleNode );
styleRange.insertNode( styleNode );
removeFromInsideElement.call( this, styleNode );
styleNode.remove( true );
}
// Style applied, let's release the range, so it gets
// re-initialization in the next loop.
styleRange = null;
}
}
// Remove the bookmark nodes.
range.moveToBookmark( boundaryNodes );
// Minimize the result range to exclude empty text nodes. (#5374)
range.shrink( CKEDITOR.SHRINK_TEXT );
// Get inside the remaining element if range.shrink( TEXT ) has failed because of non-editable elements inside.
// E.g. range.shrink( TEXT ) will not get inside:
// [x ]
// but range.shrink( ELEMENT ) will.
range.shrink( CKEDITOR.NODE_ELEMENT, true );
}
function removeInlineStyle( range ) {
// Make sure our range has included all "collpased" parent inline nodes so
// that our operation logic can be simpler.
range.enlarge( CKEDITOR.ENLARGE_INLINE, 1 );
var bookmark = range.createBookmark(),
startNode = bookmark.startNode;
if ( range.collapsed ) {
var startPath = new CKEDITOR.dom.elementPath( startNode.getParent(), range.root ),
// The topmost element in elementspatch which we should jump out of.
boundaryElement;
for ( var i = 0, element; i < startPath.elements.length && ( element = startPath.elements[ i ] ); i++ ) {
// 1. If it's collaped inside text nodes, try to remove the style from the whole element.
//
// 2. Otherwise if it's collapsed on element boundaries, moving the selection
// outside the styles instead of removing the whole tag,
// also make sure other inner styles were well preserverd.(#3309)
if ( element == startPath.block || element == startPath.blockLimit )
break;
if ( this.checkElementRemovable( element ) ) {
var isStart;
if ( range.collapsed && ( range.checkBoundaryOfElement( element, CKEDITOR.END ) || ( isStart = range.checkBoundaryOfElement( element, CKEDITOR.START ) ) ) ) {
boundaryElement = element;
boundaryElement.match = isStart ? 'start' : 'end';
} else {
// Before removing the style node, there may be a sibling to the style node
// that's exactly the same to the one to be removed. To the user, it makes
// no difference that they're separate entities in the DOM tree. So, merge
// them before removal.
element.mergeSiblings();
if ( element.is( this.element ) )
removeFromElement.call( this, element );
else
removeOverrides( element, getOverrides( this )[ element.getName() ] );
}
}
}
// Re-create the style tree after/before the boundary element,
// the replication start from bookmark start node to define the
// new range.
if ( boundaryElement ) {
var clonedElement = startNode;
for ( i = 0; ; i++ ) {
var newElement = startPath.elements[ i ];
if ( newElement.equals( boundaryElement ) )
break;
// Avoid copying any matched element.
else if ( newElement.match )
continue;
else
newElement = newElement.clone();
newElement.append( clonedElement );
clonedElement = newElement;
}
clonedElement[ boundaryElement.match == 'start' ? 'insertBefore' : 'insertAfter' ]( boundaryElement );
}
} else {
// Now our range isn't collapsed. Lets walk from the start node to the end
// node via DFS and remove the styles one-by-one.
var endNode = bookmark.endNode,
me = this;
breakNodes();
// Now, do the DFS walk.
var currentNode = startNode;
while ( !currentNode.equals( endNode ) ) {
// Need to get the next node first because removeFromElement() can remove
// the current node from DOM tree.
var nextNode = currentNode.getNextSourceNode();
if ( currentNode.type == CKEDITOR.NODE_ELEMENT && this.checkElementRemovable( currentNode ) ) {
// Remove style from element or overriding element.
if ( currentNode.getName() == this.element )
removeFromElement.call( this, currentNode );
else
removeOverrides( currentNode, getOverrides( this )[ currentNode.getName() ] );
// removeFromElement() may have merged the next node with something before
// the startNode via mergeSiblings(). In that case, the nextNode would
// contain startNode and we'll have to call breakNodes() again and also
// reassign the nextNode to something after startNode.
if ( nextNode.type == CKEDITOR.NODE_ELEMENT && nextNode.contains( startNode ) ) {
breakNodes();
nextNode = startNode.getNext();
}
}
currentNode = nextNode;
}
}
range.moveToBookmark( bookmark );
// See the comment for range.shrink in applyInlineStyle.
range.shrink( CKEDITOR.NODE_ELEMENT, true );
// Find out the style ancestor that needs to be broken down at startNode
// and endNode.
function breakNodes() {
var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ),
endPath = new CKEDITOR.dom.elementPath( endNode.getParent() ),
breakStart = null,
breakEnd = null;
for ( var i = 0; i < startPath.elements.length; i++ ) {
var element = startPath.elements[ i ];
if ( element == startPath.block || element == startPath.blockLimit )
break;
if ( me.checkElementRemovable( element, true ) )
breakStart = element;
}
for ( i = 0; i < endPath.elements.length; i++ ) {
element = endPath.elements[ i ];
if ( element == endPath.block || element == endPath.blockLimit )
break;
if ( me.checkElementRemovable( element, true ) )
breakEnd = element;
}
if ( breakEnd )
endNode.breakParent( breakEnd );
if ( breakStart )
startNode.breakParent( breakStart );
}
}
// Apply style to nested editables inside editablesContainer.
// @param {CKEDITOR.dom.element} editablesContainer
// @context CKEDITOR.style
function applyStyleOnNestedEditables( editablesContainer ) {
var editables = findNestedEditables( editablesContainer ),
editable,
l = editables.length,
i = 0,
range = l && new CKEDITOR.dom.range( editablesContainer.getDocument() );
for ( ; i < l; ++i ) {
editable = editables[ i ];
// Check if style is allowed by this editable's ACF.
if ( checkIfAllowedInEditable( editable, this ) ) {
range.selectNodeContents( editable );
applyInlineStyle.call( this, range );
}
}
}
// Finds nested editables within container. Does not return
// editables nested in another editable (twice).
function findNestedEditables( container ) {
var editables = [];
container.forEach( function( element ) {
if ( element.getAttribute( 'contenteditable' ) == 'true' ) {
editables.push( element );
return false; // Skip children.
}
}, CKEDITOR.NODE_ELEMENT, true );
return editables;
}
// Checks if style is allowed in this editable.
function checkIfAllowedInEditable( editable, style ) {
var filter = CKEDITOR.filter.instances[ editable.data( 'cke-filter' ) ];
return filter ? filter.check( style ) : 1;
}
// Checks if style is allowed by iterator's active filter.
function checkIfAllowedByIterator( iterator, style ) {
return iterator.activeFilter ? iterator.activeFilter.check( style ) : 1;
}
function applyObjectStyle( range ) {
// Selected or parent element. (#9651)
var start = range.getEnclosedNode() || range.getCommonAncestor( false, true ),
element = new CKEDITOR.dom.elementPath( start, range.root ).contains( this.element, 1 );
element && !element.isReadOnly() && setupElement( element, this );
}
function removeObjectStyle( range ) {
var parent = range.getCommonAncestor( true, true ),
element = new CKEDITOR.dom.elementPath( parent, range.root ).contains( this.element, 1 );
if ( !element )
return;
var style = this,
def = style._.definition,
attributes = def.attributes;
// Remove all defined attributes.
if ( attributes ) {
for ( var att in attributes )
element.removeAttribute( att, attributes[ att ] );
}
// Assign all defined styles.
if ( def.styles ) {
for ( var i in def.styles ) {
if ( def.styles.hasOwnProperty( i ) )
element.removeStyle( i );
}
}
}
function applyBlockStyle( range ) {
// Serializible bookmarks is needed here since
// elements may be merged.
var bookmark = range.createBookmark( true );
var iterator = range.createIterator();
iterator.enforceRealBlocks = true;
// make recognize tag as a separator in ENTER_BR mode (#5121)
if ( this._.enterMode )
iterator.enlargeBr = ( this._.enterMode != CKEDITOR.ENTER_BR );
var block,
doc = range.document,
newBlock;
while ( ( block = iterator.getNextParagraph() ) ) {
if ( !block.isReadOnly() && checkIfAllowedByIterator( iterator, this ) ) {
newBlock = getElement( this, doc, block );
replaceBlock( block, newBlock );
}
}
range.moveToBookmark( bookmark );
}
function removeBlockStyle( range ) {
// Serializible bookmarks is needed here since
// elements may be merged.
var bookmark = range.createBookmark( 1 );
var iterator = range.createIterator();
iterator.enforceRealBlocks = true;
iterator.enlargeBr = this._.enterMode != CKEDITOR.ENTER_BR;
var block,
newBlock;
while ( ( block = iterator.getNextParagraph() ) ) {
if ( this.checkElementRemovable( block ) ) {
// get special treatment.
if ( block.is( 'pre' ) ) {
newBlock = this._.enterMode == CKEDITOR.ENTER_BR ? null :
range.document.createElement( this._.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
newBlock && block.copyAttributes( newBlock );
replaceBlock( block, newBlock );
} else {
removeFromElement.call( this, block );
}
}
}
range.moveToBookmark( bookmark );
}
// Replace the original block with new one, with special treatment
// for blocks to make sure content format is well preserved, and merging/splitting adjacent
// when necessary. (#3188)
function replaceBlock( block, newBlock ) {
// Block is to be removed, create a temp element to
// save contents.
var removeBlock = !newBlock;
if ( removeBlock ) {
newBlock = block.getDocument().createElement( 'div' );
block.copyAttributes( newBlock );
}
var newBlockIsPre = newBlock && newBlock.is( 'pre' ),
blockIsPre = block.is( 'pre' ),
isToPre = newBlockIsPre && !blockIsPre,
isFromPre = !newBlockIsPre && blockIsPre;
if ( isToPre )
newBlock = toPre( block, newBlock );
else if ( isFromPre )
// Split big into pieces before start to convert.
newBlock = fromPres( removeBlock ? [ block.getHtml() ] : splitIntoPres( block ), newBlock );
else
block.moveChildren( newBlock );
newBlock.replace( block );
if ( newBlockIsPre ) {
// Merge previous blocks.
mergePre( newBlock );
} else if ( removeBlock ) {
removeNoAttribsElement( newBlock );
}
}
// Merge a block with a previous sibling if available.
function mergePre( preBlock ) {
var previousBlock;
if ( !( ( previousBlock = preBlock.getPrevious( nonWhitespaces ) ) && previousBlock.type == CKEDITOR.NODE_ELEMENT && previousBlock.is( 'pre' ) ) )
return;
// Merge the previous block contents into the current
// block.
//
// Another thing to be careful here is that currentBlock might contain
// a '\n' at the beginning, and previousBlock might contain a '\n'
// towards the end. These new lines are not normally displayed but they
// become visible after merging.
var mergedHtml = replace( previousBlock.getHtml(), /\n$/, '' ) + '\n\n' +
replace( preBlock.getHtml(), /^\n/, '' );
// Krugle: IE normalizes innerHTML from , breaking whitespaces.
if ( CKEDITOR.env.ie )
preBlock.$.outerHTML = '' + mergedHtml + ' ';
else
preBlock.setHtml( mergedHtml );
previousBlock.remove();
}
// Split into multiple blocks separated by double line-break.
function splitIntoPres( preBlock ) {
// Exclude the ones at header OR at tail,
// and ignore bookmark content between them.
var duoBrRegex = /(\S\s*)\n(?:\s|(]+data-cke-bookmark.*?\/span>))*\n(?!$)/gi,
pres = [],
splitedHtml = replace( preBlock.getOuterHtml(), duoBrRegex, function( match, charBefore, bookmark ) {
return charBefore + ' ' + bookmark + '';
} );
splitedHtml.replace( /([\s\S]*?)<\/pre>/gi, function( match, preContent ) {
pres.push( preContent );
} );
return pres;
}
// Wrapper function of String::replace without considering of head/tail bookmarks nodes.
function replace( str, regexp, replacement ) {
var headBookmark = '',
tailBookmark = '';
str = str.replace( /(^]+data-cke-bookmark.*?\/span>)|(]+data-cke-bookmark.*?\/span>$)/gi, function( str, m1, m2 ) {
m1 && ( headBookmark = m1 );
m2 && ( tailBookmark = m2 );
return '';
} );
return headBookmark + str.replace( regexp, replacement ) + tailBookmark;
}
// Converting a list of into blocks with format well preserved.
function fromPres( preHtmls, newBlock ) {
var docFrag;
if ( preHtmls.length > 1 )
docFrag = new CKEDITOR.dom.documentFragment( newBlock.getDocument() );
for ( var i = 0; i < preHtmls.length; i++ ) {
var blockHtml = preHtmls[ i ];
// 1. Trim the first and last line-breaks immediately after and before ,
// they're not visible.
blockHtml = blockHtml.replace( /(\r\n|\r)/g, '\n' );
blockHtml = replace( blockHtml, /^[ \t]*\n/, '' );
blockHtml = replace( blockHtml, /\n$/, '' );
// 2. Convert spaces or tabs at the beginning or at the end to
blockHtml = replace( blockHtml, /^[ \t]+|[ \t]+$/g, function( match, offset ) {
if ( match.length == 1 ) // one space, preserve it
return ' ';
else if ( !offset ) // beginning of block
return CKEDITOR.tools.repeat( ' ', match.length - 1 ) + ' ';
else // end of block
return ' ' + CKEDITOR.tools.repeat( ' ', match.length - 1 );
} );
// 3. Convert \n to .
// 4. Convert contiguous (i.e. non-singular) spaces or tabs to
blockHtml = blockHtml.replace( /\n/g, ' ' );
blockHtml = blockHtml.replace( /[ \t]{2,}/g, function( match ) {
return CKEDITOR.tools.repeat( ' ', match.length - 1 ) + ' ';
} );
if ( docFrag ) {
var newBlockClone = newBlock.clone();
newBlockClone.setHtml( blockHtml );
docFrag.append( newBlockClone );
} else {
newBlock.setHtml( blockHtml );
}
}
return docFrag || newBlock;
}
// Converting from a non-PRE block to a PRE block in formatting operations.
function toPre( block, newBlock ) {
var bogus = block.getBogus();
bogus && bogus.remove();
// First trim the block content.
var preHtml = block.getHtml();
// 1. Trim head/tail spaces, they're not visible.
preHtml = replace( preHtml, /(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, '' );
// 2. Delete ANSI whitespaces immediately before and after because
// they are not visible.
preHtml = preHtml.replace( /[ \t\r\n]*( ]*>)[ \t\r\n]*/gi, '$1' );
// 3. Compress other ANSI whitespaces since they're only visible as one
// single space previously.
// 4. Convert to spaces since is no longer needed in .
preHtml = preHtml.replace( /([ \t\n\r]+| )/g, ' ' );
// 5. Convert any to \n. This must not be done earlier because
// the \n would then get compressed.
preHtml = preHtml.replace( / ]*>/gi, '\n' );
// Krugle: IE normalizes innerHTML to , breaking whitespaces.
if ( CKEDITOR.env.ie ) {
var temp = block.getDocument().createElement( 'div' );
temp.append( newBlock );
newBlock.$.outerHTML = '' + preHtml + ' ';
newBlock.copyAttributes( temp.getFirst() );
newBlock = temp.getFirst().remove();
} else {
newBlock.setHtml( preHtml );
}
return newBlock;
}
// Removes a style from an element itself, don't care about its subtree.
function removeFromElement( element, keepDataAttrs ) {
var def = this._.definition,
attributes = def.attributes,
styles = def.styles,
overrides = getOverrides( this )[ element.getName() ],
// If the style is only about the element itself, we have to remove the element.
removeEmpty = CKEDITOR.tools.isEmpty( attributes ) && CKEDITOR.tools.isEmpty( styles );
// Remove definition attributes/style from the elemnt.
for ( var attName in attributes ) {
// The 'class' element value must match (#1318).
if ( ( attName == 'class' || this._.definition.fullMatch ) && element.getAttribute( attName ) != normalizeProperty( attName, attributes[ attName ] ) )
continue;
// Do not touch data-* attributes (#11011) (#11258).
if ( keepDataAttrs && attName.slice( 0, 5 ) == 'data-' )
continue;
removeEmpty = element.hasAttribute( attName );
element.removeAttribute( attName );
}
for ( var styleName in styles ) {
// Full match style insist on having fully equivalence. (#5018)
if ( this._.definition.fullMatch && element.getStyle( styleName ) != normalizeProperty( styleName, styles[ styleName ], true ) )
continue;
removeEmpty = removeEmpty || !!element.getStyle( styleName );
element.removeStyle( styleName );
}
// Remove overrides, but don't remove the element if it's a block element
removeOverrides( element, overrides, blockElements[ element.getName() ] );
if ( removeEmpty ) {
if ( this._.definition.alwaysRemoveElement )
removeNoAttribsElement( element, 1 );
else {
if ( !CKEDITOR.dtd.$block[ element.getName() ] || this._.enterMode == CKEDITOR.ENTER_BR && !element.hasAttributes() )
removeNoAttribsElement( element );
else
element.renameNode( this._.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
}
}
}
// Removes a style from inside an element. Called on applyStyle to make cleanup
// before apply. During clean up this function keep data-* attribute in contrast
// to removeFromElement.
function removeFromInsideElement( element ) {
var overrides = getOverrides( this ),
innerElements = element.getElementsByTag( this.element ),
innerElement;
for ( var i = innerElements.count(); --i >= 0; ) {
innerElement = innerElements.getItem( i );
// Do not remove elements which are read only (e.g. duplicates inside widgets).
if ( !innerElement.isReadOnly() )
removeFromElement.call( this, innerElement, true );
}
// Now remove any other element with different name that is
// defined to be overriden.
for ( var overrideElement in overrides ) {
if ( overrideElement != this.element ) {
innerElements = element.getElementsByTag( overrideElement );
for ( i = innerElements.count() - 1; i >= 0; i-- ) {
innerElement = innerElements.getItem( i );
// Do not remove elements which are read only (e.g. duplicates inside widgets).
if ( !innerElement.isReadOnly() )
removeOverrides( innerElement, overrides[ overrideElement ] );
}
}
}
}
// Remove overriding styles/attributes from the specific element.
// Note: Remove the element if no attributes remain.
// @param {Object} element
// @param {Object} overrides
// @param {Boolean} Don't remove the element
function removeOverrides( element, overrides, dontRemove ) {
var attributes = overrides && overrides.attributes;
if ( attributes ) {
for ( var i = 0; i < attributes.length; i++ ) {
var attName = attributes[ i ][ 0 ],
actualAttrValue;
if ( ( actualAttrValue = element.getAttribute( attName ) ) ) {
var attValue = attributes[ i ][ 1 ];
// Remove the attribute if:
// - The override definition value is null ;
// - The override definition valie is a string that
// matches the attribute value exactly.
// - The override definition value is a regex that
// has matches in the attribute value.
if ( attValue === null || ( attValue.test && attValue.test( actualAttrValue ) ) || ( typeof attValue == 'string' && actualAttrValue == attValue ) )
element.removeAttribute( attName );
}
}
}
if ( !dontRemove )
removeNoAttribsElement( element );
}
// If the element has no more attributes, remove it.
function removeNoAttribsElement( element, forceRemove ) {
// If no more attributes remained in the element, remove it,
// leaving its children.
if ( !element.hasAttributes() || forceRemove ) {
if ( CKEDITOR.dtd.$block[ element.getName() ] ) {
var previous = element.getPrevious( nonWhitespaces ),
next = element.getNext( nonWhitespaces );
if ( previous && ( previous.type == CKEDITOR.NODE_TEXT || !previous.isBlockBoundary( { br: 1 } ) ) )
element.append( 'br', 1 );
if ( next && ( next.type == CKEDITOR.NODE_TEXT || !next.isBlockBoundary( { br: 1 } ) ) )
element.append( 'br' );
element.remove( true );
} else {
// Removing elements may open points where merging is possible,
// so let's cache the first and last nodes for later checking.
var firstChild = element.getFirst();
var lastChild = element.getLast();
element.remove( true );
if ( firstChild ) {
// Check the cached nodes for merging.
firstChild.type == CKEDITOR.NODE_ELEMENT && firstChild.mergeSiblings();
if ( lastChild && !firstChild.equals( lastChild ) && lastChild.type == CKEDITOR.NODE_ELEMENT )
lastChild.mergeSiblings();
}
}
}
}
function getElement( style, targetDocument, element ) {
var el,
elementName = style.element;
// The "*" element name will always be a span for this function.
if ( elementName == '*' )
elementName = 'span';
// Create the element.
el = new CKEDITOR.dom.element( elementName, targetDocument );
// #6226: attributes should be copied before the new ones are applied
if ( element )
element.copyAttributes( el );
el = setupElement( el, style );
// Avoid ID duplication.
if ( targetDocument.getCustomData( 'doc_processing_style' ) && el.hasAttribute( 'id' ) )
el.removeAttribute( 'id' );
else
targetDocument.setCustomData( 'doc_processing_style', 1 );
return el;
}
function setupElement( el, style ) {
var def = style._.definition,
attributes = def.attributes,
styles = CKEDITOR.style.getStyleText( def );
// Assign all defined attributes.
if ( attributes ) {
for ( var att in attributes )
el.setAttribute( att, attributes[ att ] );
}
// Assign all defined styles.
if ( styles )
el.setAttribute( 'style', styles );
return el;
}
function replaceVariables( list, variablesValues ) {
for ( var item in list ) {
list[ item ] = list[ item ].replace( varRegex, function( match, varName ) {
return variablesValues[ varName ];
} );
}
}
// Returns an object that can be used for style matching comparison.
// Attributes names and values are all lowercased, and the styles get
// merged with the style attribute.
function getAttributesForComparison( styleDefinition ) {
// If we have already computed it, just return it.
var attribs = styleDefinition._AC;
if ( attribs )
return attribs;
attribs = {};
var length = 0;
// Loop through all defined attributes.
var styleAttribs = styleDefinition.attributes;
if ( styleAttribs ) {
for ( var styleAtt in styleAttribs ) {
length++;
attribs[ styleAtt ] = styleAttribs[ styleAtt ];
}
}
// Includes the style definitions.
var styleText = CKEDITOR.style.getStyleText( styleDefinition );
if ( styleText ) {
if ( !attribs.style )
length++;
attribs.style = styleText;
}
// Appends the "length" information to the object.
attribs._length = length;
// Return it, saving it to the next request.
return ( styleDefinition._AC = attribs );
}
// Get the the collection used to compare the elements and attributes,
// defined in this style overrides, with other element. All information in
// it is lowercased.
// @param {CKEDITOR.style} style
function getOverrides( style ) {
if ( style._.overrides )
return style._.overrides;
var overrides = ( style._.overrides = {} ),
definition = style._.definition.overrides;
if ( definition ) {
// The override description can be a string, object or array.
// Internally, well handle arrays only, so transform it if needed.
if ( !CKEDITOR.tools.isArray( definition ) )
definition = [ definition ];
// Loop through all override definitions.
for ( var i = 0; i < definition.length; i++ ) {
var override = definition[ i ],
elementName,
overrideEl,
attrs;
// If can be a string with the element name.
if ( typeof override == 'string' )
elementName = override.toLowerCase();
// Or an object.
else {
elementName = override.element ? override.element.toLowerCase() : style.element;
attrs = override.attributes;
}
// We can have more than one override definition for the same
// element name, so we attempt to simply append information to
// it if it already exists.
overrideEl = overrides[ elementName ] || ( overrides[ elementName ] = {} );
if ( attrs ) {
// The returning attributes list is an array, because we
// could have different override definitions for the same
// attribute name.
var overrideAttrs = ( overrideEl.attributes = overrideEl.attributes || [] );
for ( var attName in attrs ) {
// Each item in the attributes array is also an array,
// where [0] is the attribute name and [1] is the
// override value.
overrideAttrs.push( [ attName.toLowerCase(), attrs[ attName ] ] );
}
}
}
}
return overrides;
}
// Make the comparison of attribute value easier by standardizing it.
function normalizeProperty( name, value, isStyle ) {
var temp = new CKEDITOR.dom.element( 'span' );
temp[ isStyle ? 'setStyle' : 'setAttribute' ]( name, value );
return temp[ isStyle ? 'getStyle' : 'getAttribute' ]( name );
}
// Compare two bunch of styles, with the speciality that value 'inherit'
// is treated as a wildcard which will match any value.
// @param {Object/String} source
// @param {Object/String} target
function compareCssText( source, target ) {
if ( typeof source == 'string' )
source = CKEDITOR.tools.parseCssText( source );
if ( typeof target == 'string' )
target = CKEDITOR.tools.parseCssText( target, true );
for ( var name in source ) {
if ( !( name in target && ( target[ name ] == source[ name ] || source[ name ] == 'inherit' || target[ name ] == 'inherit' ) ) )
return false;
}
return true;
}
function applyStyleOnSelection( selection, remove, editor ) {
var doc = selection.document,
ranges = selection.getRanges(),
func = remove ? this.removeFromRange : this.applyToRange,
range;
var iterator = ranges.createIterator();
while ( ( range = iterator.getNextRange() ) )
func.call( this, range, editor );
selection.selectRanges( ranges );
doc.removeCustomData( 'doc_processing_style' );
}
} )();
/**
* Generic style command. It applies a specific style when executed.
*
* var boldStyle = new CKEDITOR.style( { element: 'strong' } );
* // Register the "bold" command, which applies the bold style.
* editor.addCommand( 'bold', new CKEDITOR.dialogCommand( boldStyle ) );
*
* @class
* @constructor Creates a styleCommand class instance.
* @extends CKEDITOR.commandDefinition
* @param {CKEDITOR.style} style The style to be applied when command is executed.
* @param {Object} [ext] Additional command definition's properties.
*/
CKEDITOR.styleCommand = function( style, ext ) {
this.style = style;
this.allowedContent = style;
this.requiredContent = style;
CKEDITOR.tools.extend( this, ext, true );
};
/**
* @param {CKEDITOR.editor} editor
* @todo
*/
CKEDITOR.styleCommand.prototype.exec = function( editor ) {
editor.focus();
if ( this.state == CKEDITOR.TRISTATE_OFF )
editor.applyStyle( this.style );
else if ( this.state == CKEDITOR.TRISTATE_ON )
editor.removeStyle( this.style );
};
/**
* Manages styles registration and loading. See also {@link CKEDITOR.config#stylesSet}.
*
* // The set of styles for the Styles drop-down list.
* CKEDITOR.stylesSet.add( 'default', [
* // Block Styles
* { name: 'Blue Title', element: 'h3', styles: { 'color': 'Blue' } },
* { name: 'Red Title', element: 'h3', styles: { 'color': 'Red' } },
*
* // Inline Styles
* { name: 'Marker: Yellow', element: 'span', styles: { 'background-color': 'Yellow' } },
* { name: 'Marker: Green', element: 'span', styles: { 'background-color': 'Lime' } },
*
* // Object Styles
* {
* name: 'Image on Left',
* element: 'img',
* attributes: {
* style: 'padding: 5px; margin-right: 5px',
* border: '2',
* align: 'left'
* }
* }
* ] );
*
* @since 3.2
* @class
* @singleton
* @extends CKEDITOR.resourceManager
*/
CKEDITOR.stylesSet = new CKEDITOR.resourceManager( '', 'stylesSet' );
// Backward compatibility (#5025).
CKEDITOR.addStylesSet = CKEDITOR.tools.bind( CKEDITOR.stylesSet.add, CKEDITOR.stylesSet );
CKEDITOR.loadStylesSet = function( name, url, callback ) {
CKEDITOR.stylesSet.addExternal( name, url, '' );
CKEDITOR.stylesSet.load( name, callback );
};
CKEDITOR.tools.extend( CKEDITOR.editor.prototype, {
/**
* Registers a function to be called whenever the selection position changes in the
* editing area. The current state is passed to the function. The possible
* states are {@link CKEDITOR#TRISTATE_ON} and {@link CKEDITOR#TRISTATE_OFF}.
*
* // Create a style object for the element.
* var style = new CKEDITOR.style( { element: 'b' } );
* var editor = CKEDITOR.instances.editor1;
* editor.attachStyleStateChange( style, function( state ) {
* if ( state == CKEDITOR.TRISTATE_ON )
* alert( 'The current state for the B element is ON' );
* else
* alert( 'The current state for the B element is OFF' );
* } );
*
* @member CKEDITOR.editor
* @param {CKEDITOR.style} style The style to be watched.
* @param {Function} callback The function to be called.
*/
attachStyleStateChange: function( style, callback ) {
// Try to get the list of attached callbacks.
var styleStateChangeCallbacks = this._.styleStateChangeCallbacks;
// If it doesn't exist, it means this is the first call. So, let's create
// all the structure to manage the style checks and the callback calls.
if ( !styleStateChangeCallbacks ) {
// Create the callbacks array.
styleStateChangeCallbacks = this._.styleStateChangeCallbacks = [];
// Attach to the selectionChange event, so we can check the styles at
// that point.
this.on( 'selectionChange', function( ev ) {
// Loop throw all registered callbacks.
for ( var i = 0; i < styleStateChangeCallbacks.length; i++ ) {
var callback = styleStateChangeCallbacks[ i ];
// Check the current state for the style defined for that callback.
var currentState = callback.style.checkActive( ev.data.path, this ) ?
CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF;
// Call the callback function, passing the current state to it.
callback.fn.call( this, currentState );
}
} );
}
// Save the callback info, so it can be checked on the next occurrence of
// selectionChange.
styleStateChangeCallbacks.push( { style: style, fn: callback } );
},
/**
* Applies the style upon the editor's current selection. Shorthand for
* {@link CKEDITOR.style#apply}.
*
* @member CKEDITOR.editor
* @param {CKEDITOR.style} style
*/
applyStyle: function( style ) {
style.apply( this );
},
/**
* Removes the style from the editor's current selection. Shorthand for
* {@link CKEDITOR.style#remove}.
*
* @member CKEDITOR.editor
* @param {CKEDITOR.style} style
*/
removeStyle: function( style ) {
style.remove( this );
},
/**
* Gets the current `stylesSet` for this instance.
*
* editor.getStylesSet( function( stylesDefinitions ) {} );
*
* See also {@link CKEDITOR.editor#stylesSet} event.
*
* @member CKEDITOR.editor
* @param {Function} callback The function to be called with the styles data.
*/
getStylesSet: function( callback ) {
if ( !this._.stylesDefinitions ) {
var editor = this,
// Respect the backwards compatible definition entry
configStyleSet = editor.config.stylesCombo_stylesSet || editor.config.stylesSet;
// The false value means that none styles should be loaded.
if ( configStyleSet === false ) {
callback( null );
return;
}
// #5352 Allow to define the styles directly in the config object
if ( configStyleSet instanceof Array ) {
editor._.stylesDefinitions = configStyleSet;
callback( configStyleSet );
return;
}
// Default value is 'default'.
if ( !configStyleSet )
configStyleSet = 'default';
var partsStylesSet = configStyleSet.split( ':' ),
styleSetName = partsStylesSet[ 0 ],
externalPath = partsStylesSet[ 1 ];
CKEDITOR.stylesSet.addExternal( styleSetName, externalPath ? partsStylesSet.slice( 1 ).join( ':' ) : CKEDITOR.getUrl( 'styles.js' ), '' );
CKEDITOR.stylesSet.load( styleSetName, function( stylesSet ) {
editor._.stylesDefinitions = stylesSet[ styleSetName ];
callback( editor._.stylesDefinitions );
} );
} else {
callback( this._.stylesDefinitions );
}
}
} );
/**
* Indicates that fully selected read-only elements will be included when
* applying the style (for inline styles only).
*
* @since 3.5
* @property {Boolean} [includeReadonly=false]
* @member CKEDITOR.style
*/
/**
* Indicates that any matches element of this style will be eventually removed
* when calling {@link CKEDITOR.editor#removeStyle}.
*
* @since 4.0
* @property {Boolean} [alwaysRemoveElement=false]
* @member CKEDITOR.style
*/
/**
* Disables inline styling on read-only elements.
*
* @since 3.5
* @cfg {Boolean} [disableReadonlyStyling=false]
* @member CKEDITOR.config
*/
/**
* The "styles definition set" to use in the editor. They will be used in the
* styles combo and the style selector of the div container.
*
* The styles may be defined in the page containing the editor, or can be
* loaded on demand from an external file. In the second case, if this setting
* contains only a name, the `styles.js` file will be loaded from the
* CKEditor root folder (what ensures backward compatibility with CKEditor 4.0).
*
* Otherwise, this setting has the `name:url` syntax, making it
* possible to set the URL from which loading the styles file.
* Note that the `name` has to be equal to the name used in
* {@link CKEDITOR.stylesSet#add} while registering styles set.
*
* **Note**: Since 4.1 it is possible to set `stylesSet` to `false`
* to prevent loading any styles set.
*
* // Do not load any file. Styles set is empty.
* config.stylesSet = false;
*
* // Load the 'mystyles' styles set from styles.js file.
* config.stylesSet = 'mystyles';
*
* // Load the 'mystyles' styles set from a relative URL.
* config.stylesSet = 'mystyles:/editorstyles/styles.js';
*
* // Load from a full URL.
* config.stylesSet = 'mystyles:http://www.example.com/editorstyles/styles.js';
*
* // Load from a list of definitions.
* config.stylesSet = [
* { name: 'Strong Emphasis', element: 'strong' },
* { name: 'Emphasis', element: 'em' },
* ...
* ];
*
* @since 3.3
* @cfg {String/Array/Boolean} [stylesSet='default']
* @member CKEDITOR.config
*/
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Defines the {@link CKEDITOR.dom.comment} class, which represents
* a DOM comment node.
*/
/**
* Represents a DOM comment node.
*
* var nativeNode = document.createComment( 'Example' );
* var comment = new CKEDITOR.dom.comment( nativeNode );
*
* var comment = new CKEDITOR.dom.comment( 'Example' );
*
* @class
* @extends CKEDITOR.dom.node
* @constructor Creates a comment class instance.
* @param {Object/String} comment A native DOM comment node or a string containing
* the text to use to create a new comment node.
* @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
* the node in case of new node creation. Defaults to the current document.
*/
CKEDITOR.dom.comment = function( comment, ownerDocument ) {
if ( typeof comment == 'string' )
comment = ( ownerDocument ? ownerDocument.$ : document ).createComment( comment );
CKEDITOR.dom.domObject.call( this, comment );
};
CKEDITOR.dom.comment.prototype = new CKEDITOR.dom.node();
CKEDITOR.tools.extend( CKEDITOR.dom.comment.prototype, {
/**
* The node type. This is a constant value set to {@link CKEDITOR#NODE_COMMENT}.
*
* @readonly
* @property {Number} [=CKEDITOR.NODE_COMMENT]
*/
type: CKEDITOR.NODE_COMMENT,
/**
* Gets the outer HTML of this comment.
*
* @returns {String} The HTML ``.
*/
getOuterHtml: function() {
return '';
}
} );
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
'use strict';
( function() {
var pathBlockLimitElements = {},
pathBlockElements = {},
tag;
// Elements that are considered the "Block limit" in an element path.
for ( tag in CKEDITOR.dtd.$blockLimit ) {
// Exclude from list roots.
if ( !( tag in CKEDITOR.dtd.$list ) )
pathBlockLimitElements[ tag ] = 1;
}
// Elements that are considered the "End level Block" in an element path.
for ( tag in CKEDITOR.dtd.$block ) {
// Exclude block limits, and empty block element, e.g. hr.
if ( !( tag in CKEDITOR.dtd.$blockLimit || tag in CKEDITOR.dtd.$empty ) )
pathBlockElements[ tag ] = 1;
}
// Check if an element contains any block element.
function checkHasBlock( element ) {
var childNodes = element.getChildren();
for ( var i = 0, count = childNodes.count(); i < count; i++ ) {
var child = childNodes.getItem( i );
if ( child.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$block[ child.getName() ] )
return true;
}
return false;
}
/**
* Retrieve the list of nodes walked from the start node up to the editable element of the editor.
*
* @class
* @constructor Creates an element path class instance.
* @param {CKEDITOR.dom.element} startNode From which the path should start.
* @param {CKEDITOR.dom.element} root To which element the path should stop, defaults to the `body` element.
*/
CKEDITOR.dom.elementPath = function( startNode, root ) {
var block = null,
blockLimit = null,
elements = [],
e = startNode,
elementName;
// Backward compact.
root = root || startNode.getDocument().getBody();
do {
if ( e.type == CKEDITOR.NODE_ELEMENT ) {
elements.push( e );
if ( !this.lastElement ) {
this.lastElement = e;
// If an object or non-editable element is fully selected at the end of the element path,
// it must not become the block limit.
if ( e.is( CKEDITOR.dtd.$object ) || e.getAttribute( 'contenteditable' ) == 'false' )
continue;
}
if ( e.equals( root ) )
break;
if ( !blockLimit ) {
elementName = e.getName();
// First editable element becomes a block limit, because it cannot be split.
if ( e.getAttribute( 'contenteditable' ) == 'true' )
blockLimit = e;
// "Else" because element cannot be both - block and block levelimit.
else if ( !block && pathBlockElements[ elementName ] )
block = e;
if ( pathBlockLimitElements[ elementName ] ) {
// End level DIV is considered as the block, if no block is available. (#525)
// But it must NOT be the root element (checked above).
if ( !block && elementName == 'div' && !checkHasBlock( e ) )
block = e;
else
blockLimit = e;
}
}
}
}
while ( ( e = e.getParent() ) );
// Block limit defaults to root.
if ( !blockLimit )
blockLimit = root;
/**
* First non-empty block element which:
*
* * is not a {@link CKEDITOR.dtd#$blockLimit},
* * or is a `div` which does not contain block elements and is not a `root`.
*
* This means a first, splittable block in elements path.
*
* @readonly
* @property {CKEDITOR.dom.element}
*/
this.block = block;
/**
* See the {@link CKEDITOR.dtd#$blockLimit} description.
*
* @readonly
* @property {CKEDITOR.dom.element}
*/
this.blockLimit = blockLimit;
/**
* The root of the elements path - `root` argument passed to class constructor or a `body` element.
*
* @readonly
* @property {CKEDITOR.dom.element}
*/
this.root = root;
/**
* An array of elements (from `startNode` to `root`) in the path.
*
* @readonly
* @property {CKEDITOR.dom.element[]}
*/
this.elements = elements;
/**
* The last element of the elements path - `startNode` or its parent.
*
* @readonly
* @property {CKEDITOR.dom.element} lastElement
*/
};
} )();
CKEDITOR.dom.elementPath.prototype = {
/**
* Compares this element path with another one.
*
* @param {CKEDITOR.dom.elementPath} otherPath The elementPath object to be
* compared with this one.
* @returns {Boolean} `true` if the paths are equal, containing the same
* number of elements and the same elements in the same order.
*/
compare: function( otherPath ) {
var thisElements = this.elements;
var otherElements = otherPath && otherPath.elements;
if ( !otherElements || thisElements.length != otherElements.length )
return false;
for ( var i = 0; i < thisElements.length; i++ ) {
if ( !thisElements[ i ].equals( otherElements[ i ] ) )
return false;
}
return true;
},
/**
* Search the path elements that meets the specified criteria.
*
* @param {String/Array/Function/Object/CKEDITOR.dom.element} query The criteria that can be
* either a tag name, list (array and object) of tag names, element or an node evaluator function.
* @param {Boolean} [excludeRoot] Not taking path root element into consideration.
* @param {Boolean} [fromTop] Search start from the topmost element instead of bottom.
* @returns {CKEDITOR.dom.element} The first matched dom element or `null`.
*/
contains: function( query, excludeRoot, fromTop ) {
var evaluator;
if ( typeof query == 'string' )
evaluator = function( node ) {
return node.getName() == query;
};
if ( query instanceof CKEDITOR.dom.element )
evaluator = function( node ) {
return node.equals( query );
};
else if ( CKEDITOR.tools.isArray( query ) )
evaluator = function( node ) {
return CKEDITOR.tools.indexOf( query, node.getName() ) > -1;
};
else if ( typeof query == 'function' )
evaluator = query;
else if ( typeof query == 'object' )
evaluator = function( node ) {
return node.getName() in query;
};
var elements = this.elements,
length = elements.length;
excludeRoot && length--;
if ( fromTop ) {
elements = Array.prototype.slice.call( elements, 0 );
elements.reverse();
}
for ( var i = 0; i < length; i++ ) {
if ( evaluator( elements[ i ] ) )
return elements[ i ];
}
return null;
},
/**
* Check whether the elements path is the proper context for the specified
* tag name in the DTD.
*
* @param {String} tag The tag name.
* @returns {Boolean}
*/
isContextFor: function( tag ) {
var holder;
// Check for block context.
if ( tag in CKEDITOR.dtd.$block ) {
// Indeterminate elements which are not subjected to be splitted or surrounded must be checked first.
var inter = this.contains( CKEDITOR.dtd.$intermediate );
holder = inter || ( this.root.equals( this.block ) && this.block ) || this.blockLimit;
return !!holder.getDtd()[ tag ];
}
return true;
},
/**
* Retrieve the text direction for this elements path.
*
* @returns {'ltr'/'rtl'}
*/
direction: function() {
var directionNode = this.block || this.blockLimit || this.root;
return directionNode.getDirection( 1 );
}
};
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Defines the {@link CKEDITOR.dom.text} class, which represents
* a DOM text node.
*/
/**
* Represents a DOM text node.
*
* var nativeNode = document.createTextNode( 'Example' );
* var text = new CKEDITOR.dom.text( nativeNode );
*
* var text = new CKEDITOR.dom.text( 'Example' );
*
* @class
* @extends CKEDITOR.dom.node
* @constructor Creates a text class instance.
* @param {Object/String} text A native DOM text node or a string containing
* the text to use to create a new text node.
* @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
* the node in case of new node creation. Defaults to the current document.
*/
CKEDITOR.dom.text = function( text, ownerDocument ) {
if ( typeof text == 'string' )
text = ( ownerDocument ? ownerDocument.$ : document ).createTextNode( text );
// Theoretically, we should call the base constructor here
// (not CKEDITOR.dom.node though). But, IE doesn't support expando
// properties on text node, so the features provided by domObject will not
// work for text nodes (which is not a big issue for us).
//
// CKEDITOR.dom.domObject.call( this, element );
this.$ = text;
};
CKEDITOR.dom.text.prototype = new CKEDITOR.dom.node();
CKEDITOR.tools.extend( CKEDITOR.dom.text.prototype, {
/**
* The node type. This is a constant value set to {@link CKEDITOR#NODE_TEXT}.
*
* @readonly
* @property {Number} [=CKEDITOR.NODE_TEXT]
*/
type: CKEDITOR.NODE_TEXT,
/**
* Gets length of node's value.
*
* @returns {Number}
*/
getLength: function() {
return this.$.nodeValue.length;
},
/**
* Gets node's value.
*
* @returns {String}
*/
getText: function() {
return this.$.nodeValue;
},
/**
* Sets node's value.
*
* @param {String} text
*/
setText: function( text ) {
this.$.nodeValue = text;
},
/**
* Breaks this text node into two nodes at the specified offset,
* keeping both in the tree as siblings. This node then only contains
* all the content up to the offset point. A new text node, which is
* inserted as the next sibling of this node, contains all the content
* at and after the offset point. When the offset is equal to the
* length of this node, the new node has no data.
*
* @param {Number} The position at which to split, starting from zero.
* @returns {CKEDITOR.dom.text} The new text node.
*/
split: function( offset ) {
// Saved the children count and text length beforehand.
var parent = this.$.parentNode,
count = parent.childNodes.length,
length = this.getLength();
var doc = this.getDocument();
var retval = new CKEDITOR.dom.text( this.$.splitText( offset ), doc );
if ( parent.childNodes.length == count ) {
// If the offset is after the last char, IE creates the text node
// on split, but don't include it into the DOM. So, we have to do
// that manually here.
if ( offset >= length ) {
retval = doc.createText( '' );
retval.insertAfter( this );
} else {
// IE BUG: IE8+ does not update the childNodes array in DOM after splitText(),
// we need to make some DOM changes to make it update. (#3436)
var workaround = doc.createText( '' );
workaround.insertAfter( retval );
workaround.remove();
}
}
return retval;
},
/**
* Extracts characters from indexA up to but not including `indexB`.
*
* @param {Number} indexA An integer between `0` and one less than the
* length of the text.
* @param {Number} [indexB] An integer between `0` and the length of the
* string. If omitted, extracts characters to the end of the text.
*/
substring: function( indexA, indexB ) {
// We need the following check due to a Firefox bug
// https://bugzilla.mozilla.org/show_bug.cgi?id=458886
if ( typeof indexB != 'number' )
return this.$.nodeValue.substr( indexA );
else
return this.$.nodeValue.substring( indexA, indexB );
}
} );
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
/**
* Represents a list os CKEDITOR.dom.range objects, which can be easily
* iterated sequentially.
*
* @class
* @extends Array
* @constructor Creates a rangeList class instance.
* @param {CKEDITOR.dom.range/CKEDITOR.dom.range[]} [ranges] The ranges contained on this list.
* Note that, if an array of ranges is specified, the range sequence
* should match its DOM order. This class will not help to sort them.
*/
CKEDITOR.dom.rangeList = function( ranges ) {
if ( ranges instanceof CKEDITOR.dom.rangeList )
return ranges;
if ( !ranges )
ranges = [];
else if ( ranges instanceof CKEDITOR.dom.range )
ranges = [ ranges ];
return CKEDITOR.tools.extend( ranges, mixins );
};
var mixins = {
/**
* Creates an instance of the rangeList iterator, it should be used
* only when the ranges processing could be DOM intrusive, which
* means it may pollute and break other ranges in this list.
* Otherwise, it's enough to just iterate over this array in a for loop.
*
* @returns {CKEDITOR.dom.rangeListIterator}
*/
createIterator: function() {
var rangeList = this,
bookmark = CKEDITOR.dom.walker.bookmark(),
bookmarks = [],
current;
return {
/**
* Retrieves the next range in the list.
*
* @member CKEDITOR.dom.rangeListIterator
* @param {Boolean} [mergeConsequent=false] Whether join two adjacent
* ranges into single, e.g. consequent table cells.
*/
getNextRange: function( mergeConsequent ) {
current = current === undefined ? 0 : current + 1;
var range = rangeList[ current ];
// Multiple ranges might be mangled by each other.
if ( range && rangeList.length > 1 ) {
// Bookmarking all other ranges on the first iteration,
// the range correctness after it doesn't matter since we'll
// restore them before the next iteration.
if ( !current ) {
// Make sure bookmark correctness by reverse processing.
for ( var i = rangeList.length - 1; i >= 0; i-- )
bookmarks.unshift( rangeList[ i ].createBookmark( true ) );
}
if ( mergeConsequent ) {
// Figure out how many ranges should be merged.
var mergeCount = 0;
while ( rangeList[ current + mergeCount + 1 ] ) {
var doc = range.document,
found = 0,
left = doc.getById( bookmarks[ mergeCount ].endNode ),
right = doc.getById( bookmarks[ mergeCount + 1 ].startNode ),
next;
// Check subsequent range.
while ( 1 ) {
next = left.getNextSourceNode( false );
if ( !right.equals( next ) ) {
// This could be yet another bookmark or
// walking across block boundaries.
if ( bookmark( next ) || ( next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() ) ) {
left = next;
continue;
}
} else {
found = 1;
}
break;
}
if ( !found )
break;
mergeCount++;
}
}
range.moveToBookmark( bookmarks.shift() );
// Merge ranges finally after moving to bookmarks.
while ( mergeCount-- ) {
next = rangeList[ ++current ];
next.moveToBookmark( bookmarks.shift() );
range.setEnd( next.endContainer, next.endOffset );
}
}
return range;
}
};
},
/**
* Create bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark}.
*
* @param {Boolean} [serializable=false] See {@link CKEDITOR.dom.range#createBookmark}.
* @returns {Array} Array of bookmarks.
*/
createBookmarks: function( serializable ) {
var retval = [],
bookmark;
for ( var i = 0; i < this.length; i++ ) {
retval.push( bookmark = this[ i ].createBookmark( serializable, true ) );
// Updating the container & offset values for ranges
// that have been touched.
for ( var j = i + 1; j < this.length; j++ ) {
this[ j ] = updateDirtyRange( bookmark, this[ j ] );
this[ j ] = updateDirtyRange( bookmark, this[ j ], true );
}
}
return retval;
},
/**
* Create "unobtrusive" bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark2}.
*
* @param {Boolean} [normalized=false] See {@link CKEDITOR.dom.range#createBookmark2}.
* @returns {Array} Array of bookmarks.
*/
createBookmarks2: function( normalized ) {
var bookmarks = [];
for ( var i = 0; i < this.length; i++ )
bookmarks.push( this[ i ].createBookmark2( normalized ) );
return bookmarks;
},
/**
* Move each range in the list to the position specified by a list of bookmarks.
*
* @param {Array} bookmarks The list of bookmarks, each one matching a range in the list.
*/
moveToBookmarks: function( bookmarks ) {
for ( var i = 0; i < this.length; i++ )
this[ i ].moveToBookmark( bookmarks[ i ] );
}
};
// Update the specified range which has been mangled by previous insertion of
// range bookmark nodes.(#3256)
function updateDirtyRange( bookmark, dirtyRange, checkEnd ) {
var serializable = bookmark.serializable,
container = dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ],
offset = checkEnd ? 'endOffset' : 'startOffset';
var bookmarkStart = serializable ? dirtyRange.document.getById( bookmark.startNode ) : bookmark.startNode;
var bookmarkEnd = serializable ? dirtyRange.document.getById( bookmark.endNode ) : bookmark.endNode;
if ( container.equals( bookmarkStart.getPrevious() ) ) {
dirtyRange.startOffset = dirtyRange.startOffset - container.getLength() - bookmarkEnd.getPrevious().getLength();
container = bookmarkEnd.getNext();
} else if ( container.equals( bookmarkEnd.getPrevious() ) ) {
dirtyRange.startOffset = dirtyRange.startOffset - container.getLength();
container = bookmarkEnd.getNext();
}
container.equals( bookmarkStart.getParent() ) && dirtyRange[ offset ]++;
container.equals( bookmarkEnd.getParent() ) && dirtyRange[ offset ]++;
// Update and return this range.
dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ] = container;
return dirtyRange;
}
} )();
/**
* (Virtual Class) Do not call this constructor. This class is not really part
* of the API. It just describes the return type of {@link CKEDITOR.dom.rangeList#createIterator}.
*
* @class CKEDITOR.dom.rangeListIterator
*/
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Defines the {@link CKEDITOR.skin} class that is used to manage skin parts.
*/
( function() {
var cssLoaded = {};
function getName() {
return CKEDITOR.skinName.split( ',' )[ 0 ];
}
function getConfigPath() {
return CKEDITOR.getUrl( CKEDITOR.skinName.split( ',' )[ 1 ] || ( 'skins/' + getName() + '/' ) );
}
/**
* Manages the loading of skin parts among all editor instances.
*
* @class
* @singleton
*/
CKEDITOR.skin = {
/**
* Returns the root path to the skin directory.
*
* @method
* @todo
*/
path: getConfigPath,
/**
* Loads a skin part into the page. Does nothing if the part has already been loaded.
*
* **Note:** The "editor" part is always auto loaded upon instance creation,
* thus this function is mainly used to **lazy load** other parts of the skin
* that do not have to be displayed until requested.
*
* // Load the dialog part.
* editor.skin.loadPart( 'dialog' );
*
* @param {String} part The name of the skin part CSS file that resides in the skin directory.
* @param {Function} fn The provided callback function which is invoked after the part is loaded.
*/
loadPart: function( part, fn ) {
if ( CKEDITOR.skin.name != getName() ) {
CKEDITOR.scriptLoader.load( CKEDITOR.getUrl( getConfigPath() + 'skin.js' ), function() {
loadCss( part, fn );
} );
} else {
loadCss( part, fn );
}
},
/**
* Retrieves the real URL of a (CSS) skin part.
*
* @param {String} part
*/
getPath: function( part ) {
return CKEDITOR.getUrl( getCssPath( part ) );
},
/**
* The list of registered icons. To add new icons to this list, use {@link #addIcon}.
*/
icons: {},
/**
* Registers an icon.
*
* @param {String} name The icon name.
* @param {String} path The path to the icon image file.
* @param {Number} [offset] The vertical offset position of the icon, if
* available inside a strip image.
* @param {String} [bgsize] The value of the CSS "background-size" property to
* use for this icon
*/
addIcon: function( name, path, offset, bgsize ) {
name = name.toLowerCase();
if ( !this.icons[ name ] ) {
this.icons[ name ] = {
path: path,
offset: offset || 0,
bgsize: bgsize || '16px'
};
}
},
/**
* Gets the CSS background styles to be used to render a specific icon.
*
* @param {String} name The icon name, as registered with {@link #addIcon}.
* @param {Boolean} [rtl] Indicates that the RTL version of the icon is
* to be used, if available.
* @param {String} [overridePath] The path to the icon image file. It
* overrides the path defined by the named icon, if available, and is
* used if the named icon was not registered.
* @param {Number} [overrideOffset] The vertical offset position of the
* icon. It overrides the offset defined by the named icon, if
* available, and is used if the named icon was not registered.
* @param {String} [overrideBgsize] The value of the CSS "background-size" property
* to use for the icon. It overrides the value defined by the named icon,
* if available, and is used if the named icon was not registered.
*/
getIconStyle: function( name, rtl, overridePath, overrideOffset, overrideBgsize ) {
var icon, path, offset, bgsize;
if ( name ) {
name = name.toLowerCase();
// If we're in RTL, try to get the RTL version of the icon.
if ( rtl )
icon = this.icons[ name + '-rtl' ];
// If not in LTR or no RTL version available, get the generic one.
if ( !icon )
icon = this.icons[ name ];
}
path = overridePath || ( icon && icon.path ) || '';
offset = overrideOffset || ( icon && icon.offset );
bgsize = overrideBgsize || ( icon && icon.bgsize ) || '16px';
return path &&
( 'background-image:url(' + CKEDITOR.getUrl( path ) + ');background-position:0 ' + offset + 'px;background-size:' + bgsize + ';' );
}
};
function getCssPath( part ) {
// Check for ua-specific version of skin part.
var uas = CKEDITOR.skin[ 'ua_' + part ], env = CKEDITOR.env;
if ( uas ) {
// Having versioned UA checked first.
uas = uas.split( ',' ).sort( function( a, b ) {
return a > b ? -1 : 1;
} );
// Loop through all ua entries, checking is any of them match the current ua.
for ( var i = 0, ua; i < uas.length; i++ ) {
ua = uas[ i ];
if ( env.ie ) {
if ( ( ua.replace( /^ie/, '' ) == env.version ) || ( env.quirks && ua == 'iequirks' ) )
ua = 'ie';
}
if ( env[ ua ] ) {
part += '_' + uas[ i ];
break;
}
}
}
return CKEDITOR.getUrl( getConfigPath() + part + '.css' );
}
function loadCss( part, callback ) {
// Avoid reload.
if ( !cssLoaded[ part ] ) {
CKEDITOR.document.appendStyleSheet( getCssPath( part ) );
cssLoaded[ part ] = 1;
}
// CSS loading should not be blocking.
callback && callback();
}
CKEDITOR.tools.extend( CKEDITOR.editor.prototype, {
/** Gets the color of the editor user interface.
*
* CKEDITOR.instances.editor1.getUiColor();
*
* @method
* @member CKEDITOR.editor
* @returns {String} uiColor The editor UI color or `undefined` if the UI color is not set.
*/
getUiColor: function() {
return this.uiColor;
},
/** Sets the color of the editor user interface. This method accepts a color value in
* hexadecimal notation, with a `#` character (e.g. #ffffff).
*
* CKEDITOR.instances.editor1.setUiColor( '#ff00ff' );
*
* @method
* @member CKEDITOR.editor
* @param {String} color The desired editor UI color in hexadecimal notation.
*/
setUiColor: function( color ) {
var uiStyle = getStylesheet( CKEDITOR.document );
return ( this.setUiColor = function( color ) {
this.uiColor = color;
var chameleon = CKEDITOR.skin.chameleon,
editorStyleContent = '',
panelStyleContent = '';
if ( typeof chameleon == 'function' ) {
editorStyleContent = chameleon( this, 'editor' );
panelStyleContent = chameleon( this, 'panel' );
}
var replace = [ [ uiColorRegexp, color ] ];
// Update general style.
updateStylesheets( [ uiStyle ], editorStyleContent, replace );
// Update panel styles.
updateStylesheets( uiColorMenus, panelStyleContent, replace );
} ).call( this, color );
}
} );
var uiColorStylesheetId = 'cke_ui_color',
uiColorMenus = [],
uiColorRegexp = /\$color/g;
function getStylesheet( document ) {
var node = document.getById( uiColorStylesheetId );
if ( !node ) {
node = document.getHead().append( 'style' );
node.setAttribute( 'id', uiColorStylesheetId );
node.setAttribute( 'type', 'text/css' );
}
return node;
}
function updateStylesheets( styleNodes, styleContent, replace ) {
var r, i, content;
// We have to split CSS declarations for webkit.
if ( CKEDITOR.env.webkit ) {
styleContent = styleContent.split( '}' ).slice( 0, -1 );
for ( i = 0; i < styleContent.length; i++ )
styleContent[ i ] = styleContent[ i ].split( '{' );
}
for ( var id = 0; id < styleNodes.length; id++ ) {
if ( CKEDITOR.env.webkit ) {
for ( i = 0; i < styleContent.length; i++ ) {
content = styleContent[ i ][ 1 ];
for ( r = 0; r < replace.length; r++ )
content = content.replace( replace[ r ][ 0 ], replace[ r ][ 1 ] );
styleNodes[ id ].$.sheet.addRule( styleContent[ i ][ 0 ], content );
}
} else {
content = styleContent;
for ( r = 0; r < replace.length; r++ )
content = content.replace( replace[ r ][ 0 ], replace[ r ][ 1 ] );
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 )
styleNodes[ id ].$.styleSheet.cssText += content;
else
styleNodes[ id ].$.innerHTML += content;
}
}
}
CKEDITOR.on( 'instanceLoaded', function( evt ) {
// The chameleon feature is not for IE quirks.
if ( CKEDITOR.env.ie && CKEDITOR.env.quirks )
return;
var editor = evt.editor,
showCallback = function( event ) {
var panel = event.data[ 0 ] || event.data;
var iframe = panel.element.getElementsByTag( 'iframe' ).getItem( 0 ).getFrameDocument();
// Add stylesheet if missing.
if ( !iframe.getById( 'cke_ui_color' ) ) {
var node = getStylesheet( iframe );
uiColorMenus.push( node );
var color = editor.getUiColor();
// Set uiColor for new panel.
if ( color )
updateStylesheets( [ node ], CKEDITOR.skin.chameleon( editor, 'panel' ), [ [ uiColorRegexp, color ] ] );
}
};
editor.on( 'panelShow', showCallback );
editor.on( 'menuShow', showCallback );
// Apply UI color if specified in config.
if ( editor.config.uiColor )
editor.setUiColor( editor.config.uiColor );
} );
} )();
/**
* The list of file names matching the browser user agent string from
* {@link CKEDITOR.env}. This is used to load the skin part file in addition
* to the "main" skin file for a particular browser.
*
* **Note:** For each of the defined skin parts the corresponding
* CSS file with the same name as the user agent must exist inside
* the skin directory.
*
* @property ua
* @todo type?
*/
/**
* The name of the skin that is currently used.
*
* @property {String} name
* @todo
*/
/**
* The editor skin name. Note that it is not possible to have editors with
* different skin settings in the same page. In such case just one of the
* skins will be used for all editors.
*
* This is a shortcut to {@link CKEDITOR#skinName}.
*
* It is possible to install skins outside the default `skin` folder in the
* editor installation. In that case, the absolute URL path to that folder
* should be provided, separated by a comma (`'skin_name,skin_path'`).
*
* config.skin = 'moono';
*
* config.skin = 'myskin,/customstuff/myskin/';
*
* @cfg {String} skin
* @member CKEDITOR.config
*/
/**
* A function that supports the chameleon (skin color switch) feature, providing
* the skin color style updates to be applied in runtime.
*
* **Note:** The embedded `$color` variable is to be substituted with a specific UI color.
*
* @method chameleon
* @param {String} editor The editor instance that the color changes apply to.
* @param {String} part The name of the skin part where the color changes take place.
*/
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview API initialization code.
*/
( function() {
// Disable HC detection in WebKit. (#5429)
if ( CKEDITOR.env.webkit )
CKEDITOR.env.hc = false;
else {
// Check whether high contrast is active by creating a colored border.
var hcDetect = CKEDITOR.dom.element.createFromHtml( '
', CKEDITOR.document );
hcDetect.appendTo( CKEDITOR.document.getHead() );
// Update CKEDITOR.env.
// Catch exception needed sometimes for FF. (#4230)
try {
var top = hcDetect.getComputedStyle( 'border-top-color' ),
right = hcDetect.getComputedStyle( 'border-right-color' );
// We need to check if getComputedStyle returned any value, because on FF
// it returnes empty string if CKEditor is loaded in hidden iframe. (#11121)
CKEDITOR.env.hc = !!( top && top == right );
} catch ( e ) {
CKEDITOR.env.hc = false;
}
hcDetect.remove();
}
if ( CKEDITOR.env.hc )
CKEDITOR.env.cssClass += ' cke_hc';
// Initially hide UI spaces when relevant skins are loading, later restored by skin css.
CKEDITOR.document.appendStyleText( '.cke{visibility:hidden;}' );
// Mark the editor as fully loaded.
CKEDITOR.status = 'loaded';
CKEDITOR.fireOnce( 'loaded' );
// Process all instances created by the "basic" implementation.
var pending = CKEDITOR._.pending;
if ( pending ) {
delete CKEDITOR._.pending;
for ( var i = 0; i < pending.length; i++ ) {
CKEDITOR.editor.prototype.constructor.apply( pending[ i ][ 0 ], pending[ i ][ 1 ] );
CKEDITOR.add( pending[ i ][ 0 ] );
}
}
} )();
/**
* Indicates that CKEditor is running on a High Contrast environment.
*
* if ( CKEDITOR.env.hc )
* alert( 'You\'re running on High Contrast mode. The editor interface will get adapted to provide you a better experience.' );
*
* @property {Boolean} hc
* @member CKEDITOR.env
*/
/**
* Fired when a CKEDITOR core object is fully loaded and ready for interaction.
*
* @event loaded
* @member CKEDITOR
*/
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/*
skin.js
=========
In this file we interact with the CKEditor JavaScript API to register the skin
and enable additional skin related features.
The level of complexity of this file depends on the features available in the
skin. There is only one mandatory line of code to be included here, which is
setting CKEDITOR.skin.name. All the rest is optional, but recommended to be
implemented as they make higher quality skins.
For this skin, the following tasks are achieved in this file:
1. Register the skin.
2. Register browser specific skin files.
3. Define the "Chameleon" feature.
4. Register the skin icons, to have them used on the development version of
the skin.
*/
// 1. Register the skin
// ----------------------
// The CKEDITOR.skin.name property must be set to the skin name. This is a
// lower-cased name, which must match the skin folder name as well as the value
// used on config.skin to tell the editor to use the skin.
//
// This is the only mandatory property to be defined in this file.
CKEDITOR.skin.name = 'moono';
// 2. Register browser specific skin files
// -----------------------------------------
// (http://docs.cksource.com/CKEditor_4.x/Skin_SDK/Browser_Hacks)
//
// To help implementing browser specific "hacks" to the skin files and have it
// easy to maintain, it is possible to have dedicated files for such browsers,
// for both the main skin CSS files: editor.css and dialog.css.
//
// The browser files must be named after the main file names, appended by an
// underscore and the browser name (e.g. editor_ie.css, dialog_ie8.css).
//
// The accepted browser names must match the CKEDITOR.env properties. The most
// common names are: ie, webkit and gecko. Check the documentation for the complete
// list:
// http://docs.ckeditor.com/#!/api/CKEDITOR.env
//
// Internet explorer is an expection and the browser version is also accepted
// (ie7, ie8, ie9, ie10), as well as a special name for IE in Quirks mode (iequirks).
//
// The available browser specific files must be set separately for editor.css
// and dialog.css.
CKEDITOR.skin.ua_editor = 'ie,iequirks,ie7,ie8,gecko';
CKEDITOR.skin.ua_dialog = 'ie,iequirks,ie7,ie8';
// 3. Define the "Chameleon" feature
// -----------------------------------
// (http://docs.cksource.com/CKEditor_4.x/Skin_SDK/Chameleon)
//
// "Chameleon" is a unique feature available in CKEditor. It makes it possible
// to end users to specify which color to use as the basis for the editor UI.
// It is enough to set config.uiColor to any color value and voila, the UI is
// colored.
//
// The only detail here is that the skin itself must be compatible with the
// Chameleon feature. That's because the skin CSS files are the responsible to
// apply colors in the UI and each skin do that in different way and on
// different places.
//
// Implementing the Chameleon feature requires a bit of JavaScript programming.
// The CKEDITOR.skin.chameleon function must be defined. It must return the CSS
// "template" to be used to change the color of a specific CKEditor instance
// available in the page. When a color change is required, this template is
// appended to the page holding the editor, overriding styles defined in the
// skin files.
//
// The "$color" placeholder can be used in the returned string. It'll be
// replaced with the desired color.
CKEDITOR.skin.chameleon = ( function() {
// This method can be used to adjust colour brightness of various element.
// Colours are accepted in 7-byte hex format, for example: #00ff00.
// Brightness ratio must be a float number within [-1, 1],
// where -1 is black, 1 is white and 0 is the original colour.
var colorBrightness = ( function() {
function channelBrightness( channel, ratio ) {
var brighten = ratio < 0 ? (
0 | channel * ( 1 + ratio )
) : (
0 | channel + ( 255 - channel ) * ratio
);
return ( '0' + brighten.toString( 16 ) ).slice( -2 );
}
return function( hexColor, ratio ) {
var channels = hexColor.match( /[^#]./g );
for ( var i = 0 ; i < 3 ; i++ )
channels[ i ] = channelBrightness( parseInt( channels[ i ], 16 ), ratio );
return '#' + channels.join( '' );
};
} )(),
// Use this function just to avoid having to repeat all these rules on
// several places of our template.
verticalGradient = ( function() {
var template = new CKEDITOR.template( 'background:#{to};' +
'background-image:linear-gradient(to bottom,{from},{to});' +
'filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr=\'{from}\',endColorstr=\'{to}\');' );
return function( from, to ) {
return template.output( { from: from, to: to } );
};
} )(),
// Style templates for various user interface parts:
// * Default editor template.
// * Default panel template.
templates = {
editor: new CKEDITOR.template(
'{id}.cke_chrome [border-color:{defaultBorder};] ' +
'{id} .cke_top [ ' +
'{defaultGradient}' +
'border-bottom-color:{defaultBorder};' +
'] ' +
'{id} .cke_bottom [' +
'{defaultGradient}' +
'border-top-color:{defaultBorder};' +
'] ' +
'{id} .cke_resizer [border-right-color:{ckeResizer}] ' +
// Dialogs.
'{id} .cke_dialog_title [' +
'{defaultGradient}' +
'border-bottom-color:{defaultBorder};' +
'] ' +
'{id} .cke_dialog_footer [' +
'{defaultGradient}' +
'outline-color:{defaultBorder};' +
'border-top-color:{defaultBorder};' + // IE7 doesn't use outline.
'] ' +
'{id} .cke_dialog_tab [' +
'{lightGradient}' +
'border-color:{defaultBorder};' +
'] ' +
'{id} .cke_dialog_tab:hover [' +
'{mediumGradient}' +
'] ' +
'{id} .cke_dialog_contents [' +
'border-top-color:{defaultBorder};' +
'] ' +
'{id} .cke_dialog_tab_selected, {id} .cke_dialog_tab_selected:hover [' +
'background:{dialogTabSelected};' +
'border-bottom-color:{dialogTabSelectedBorder};' +
'] ' +
'{id} .cke_dialog_body [' +
'background:{dialogBody};' +
'border-color:{defaultBorder};' +
'] ' +
// Toolbars, buttons.
'{id} .cke_toolgroup [' +
'{lightGradient}' +
'border-color:{defaultBorder};' +
'] ' +
'{id} a.cke_button_off:hover, {id} a.cke_button_off:focus, {id} a.cke_button_off:active [' +
'{mediumGradient}' +
'] ' +
'{id} .cke_button_on [' +
'{ckeButtonOn}' +
'] ' +
'{id} .cke_toolbar_separator [' +
'background-color: {ckeToolbarSeparator};' +
'] ' +
// Combo buttons.
'{id} .cke_combo_button [' +
'border-color:{defaultBorder};' +
'{lightGradient}' +
'] ' +
'{id} a.cke_combo_button:hover, {id} a.cke_combo_button:focus, {id} .cke_combo_on a.cke_combo_button [' +
'border-color:{defaultBorder};' +
'{mediumGradient}' +
'] ' +
// Elementspath.
'{id} .cke_path_item [' +
'color:{elementsPathColor};' +
'] ' +
'{id} a.cke_path_item:hover, {id} a.cke_path_item:focus, {id} a.cke_path_item:active [' +
'background-color:{elementsPathBg};' +
'] ' +
'{id}.cke_panel [' +
'border-color:{defaultBorder};' +
'] '
),
panel: new CKEDITOR.template(
// Panel drop-downs.
'.cke_panel_grouptitle [' +
'{lightGradient}' +
'border-color:{defaultBorder};' +
'] ' +
// Context menus.
'.cke_menubutton_icon [' +
'background-color:{menubuttonIcon};' +
'] ' +
'.cke_menubutton:hover .cke_menubutton_icon, .cke_menubutton:focus .cke_menubutton_icon, .cke_menubutton:active .cke_menubutton_icon [' +
'background-color:{menubuttonIconHover};' +
'] ' +
'.cke_menuseparator [' +
'background-color:{menubuttonIcon};' +
'] ' +
// Color boxes.
'a:hover.cke_colorbox, a:focus.cke_colorbox, a:active.cke_colorbox [' +
'border-color:{defaultBorder};' +
'] ' +
'a:hover.cke_colorauto, a:hover.cke_colormore, a:focus.cke_colorauto, a:focus.cke_colormore, a:active.cke_colorauto, a:active.cke_colormore [' +
'background-color:{ckeColorauto};' +
'border-color:{defaultBorder};' +
'] '
)
};
return function( editor, part ) {
var uiColor = editor.uiColor,
// The following are CSS styles used in templates.
// Styles are generated according to current editor.uiColor.
templateStyles = {
// CKEditor instances have a unique ID, which is used as class name into
// the outer container of the editor UI (e.g. ".cke_1").
//
// The Chameleon feature is available for each CKEditor instance,
// independently. Because of this, we need to prefix all CSS selectors with
// the unique class name of the instance.
id: '.' + editor.id,
// These styles are used by various UI elements.
defaultBorder: colorBrightness( uiColor, -0.1 ),
defaultGradient: verticalGradient( colorBrightness( uiColor, 0.9 ), uiColor ),
lightGradient: verticalGradient( colorBrightness( uiColor, 1 ), colorBrightness( uiColor, 0.7 ) ),
mediumGradient: verticalGradient( colorBrightness( uiColor, 0.8 ), colorBrightness( uiColor, 0.5 ) ),
// These are for specific UI elements.
ckeButtonOn: verticalGradient( colorBrightness( uiColor, 0.6 ), colorBrightness( uiColor, 0.7 ) ),
ckeResizer: colorBrightness( uiColor, -0.4 ),
ckeToolbarSeparator: colorBrightness( uiColor, 0.5 ),
ckeColorauto: colorBrightness( uiColor, 0.8 ),
dialogBody: colorBrightness( uiColor, 0.7 ),
// Use gradient instead of simple hex to avoid further filter resetting in IE.
dialogTabSelected: verticalGradient( '#FFFFFF', '#FFFFFF' ),
dialogTabSelectedBorder: '#FFF',
elementsPathColor: colorBrightness( uiColor, -0.6 ),
elementsPathBg: uiColor,
menubuttonIcon: colorBrightness( uiColor, 0.5 ),
menubuttonIconHover: colorBrightness( uiColor, 0.3 )
};
return templates[ part ]
.output( templateStyles )
.replace( /\[/g, '{' ) // Replace brackets with braces.
.replace( /\]/g, '}' );
};
} )();
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview The Dialog User Interface plugin.
*/
CKEDITOR.plugins.add( 'dialogui', {
onLoad: function() {
var initPrivateObject = function( elementDefinition ) {
this._ || ( this._ = {} );
this._[ 'default' ] = this._.initValue = elementDefinition[ 'default' ] || '';
this._.required = elementDefinition.required || false;
var args = [ this._ ];
for ( var i = 1; i < arguments.length; i++ )
args.push( arguments[ i ] );
args.push( true );
CKEDITOR.tools.extend.apply( CKEDITOR.tools, args );
return this._;
},
textBuilder = {
build: function( dialog, elementDefinition, output ) {
return new CKEDITOR.ui.dialog.textInput( dialog, elementDefinition, output );
}
},
commonBuilder = {
build: function( dialog, elementDefinition, output ) {
return new CKEDITOR.ui.dialog[ elementDefinition.type ]( dialog, elementDefinition, output );
}
},
containerBuilder = {
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 );
}
},
commonPrototype = {
isChanged: function() {
return this.getValue() != this.getInitValue();
},
reset: function( noChangeEvent ) {
this.setValue( this.getInitValue(), noChangeEvent );
},
setInitValue: function() {
this._.initValue = this.getValue();
},
resetInitValue: function() {
this._.initValue = this._[ 'default' ];
},
getInitValue: function() {
return this._.initValue;
}
},
commonEventProcessors = CKEDITOR.tools.extend( {}, CKEDITOR.ui.dialog.uiElement.prototype.eventProcessors, {
onChange: function( dialog, func ) {
if ( !this._.domOnChangeRegistered ) {
dialog.on( 'load', function() {
this.getInputElement().on( 'change', function() {
// Make sure 'onchange' doesn't get fired after dialog closed. (#5719)
if ( !dialog.parts.dialog.isVisible() )
return;
this.fire( 'change', { value: this.getValue() } );
}, this );
}, this );
this._.domOnChangeRegistered = true;
}
this.on( 'change', func );
}
}, true ),
eventRegex = /^on([A-Z]\w+)/,
cleanInnerDefinition = function( def ) {
// An inner UI element should not have the parent's type, title or events.
for ( var i in def ) {
if ( eventRegex.test( i ) || i == 'title' || i == 'type' )
delete def[ i ];
}
return def;
},
// @context {CKEDITOR.dialog.uiElement} UI element (textarea or textInput)
// @param {CKEDITOR.dom.event} evt
toggleBidiKeyUpHandler = function( evt ) {
var keystroke = evt.data.getKeystroke();
// ALT + SHIFT + Home for LTR direction.
if ( keystroke == CKEDITOR.SHIFT + CKEDITOR.ALT + 36 )
this.setDirectionMarker( 'ltr' );
// ALT + SHIFT + End for RTL direction.
else if ( keystroke == CKEDITOR.SHIFT + CKEDITOR.ALT + 35 )
this.setDirectionMarker( 'rtl' );
};
CKEDITOR.tools.extend( CKEDITOR.ui.dialog, {
/**
* Base class for all dialog window elements with a textual label on the left.
*
* @class CKEDITOR.ui.dialog.labeledElement
* @extends CKEDITOR.ui.dialog.uiElement
* @constructor Creates a labeledElement class instance.
* @param {CKEDITOR.dialog} dialog Parent dialog window object.
* @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
* The element definition. Accepted fields:
*
* * `label` (Required) The label string.
* * `labelLayout` (Optional) Put 'horizontal' here if the
* label element is to be laid out horizontally. Otherwise a vertical
* layout will be used.
* * `widths` (Optional) This applies only to horizontal
* layouts — a two-element array of lengths to specify the widths of the
* label and the content element.
* * `role` (Optional) Value for the `role` attribute.
* * `includeLabel` (Optional) If set to `true`, the `aria-labelledby` attribute
* will be included.
*
* @param {Array} htmlList The list of HTML code to output to.
* @param {Function} contentHtml
* A function returning the HTML code string to be added inside the content
* cell.
*/
labeledElement: function( dialog, elementDefinition, htmlList, contentHtml ) {
if ( arguments.length < 4 )
return;
var _ = initPrivateObject.call( this, elementDefinition );
_.labelId = CKEDITOR.tools.getNextId() + '_label';
this._.children = [];
var innerHTML = function() {
var html = [],
requiredClass = elementDefinition.required ? ' cke_required' : '';
if ( elementDefinition.labelLayout != 'horizontal' ) {
html.push(
'',
elementDefinition.label,
' ',
'',
contentHtml.call( this, dialog, elementDefinition ),
'
' );
} else {
var hboxDefinition = {
type: 'hbox',
widths: elementDefinition.widths,
padding: 0,
children: [ {
type: 'html',
html: '' +
CKEDITOR.tools.htmlEncode( elementDefinition.label ) +
' '
},
{
type: 'html',
html: '' +
contentHtml.call( this, dialog, elementDefinition ) +
' '
} ]
};
CKEDITOR.dialog._.uiElementBuilders.hbox.build( dialog, hboxDefinition, html );
}
return html.join( '' );
};
var attributes = { role: elementDefinition.role || 'presentation' };
if ( elementDefinition.includeLabel )
attributes[ 'aria-labelledby' ] = _.labelId;
CKEDITOR.ui.dialog.uiElement.call( this, dialog, elementDefinition, htmlList, 'div', null, attributes, innerHTML );
},
/**
* A text input with a label. This UI element class represents both the
* single-line text inputs and password inputs in dialog boxes.
*
* @class CKEDITOR.ui.dialog.textInput
* @extends CKEDITOR.ui.dialog.labeledElement
* @constructor Creates a textInput class instance.
* @param {CKEDITOR.dialog} dialog Parent dialog window object.
* @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
* The element definition. Accepted fields:
*
* * `default` (Optional) The default value.
* * `validate` (Optional) The validation function.
* * `maxLength` (Optional) The maximum length of text box contents.
* * `size` (Optional) The size of the text box. This is
* usually overridden by the size defined by the skin, though.
*
* @param {Array} htmlList List of HTML code to output to.
*/
textInput: function( dialog, elementDefinition, htmlList ) {
if ( arguments.length < 3 )
return;
initPrivateObject.call( this, elementDefinition );
var domId = this._.inputId = CKEDITOR.tools.getNextId() + '_textInput',
attributes = { 'class': 'cke_dialog_ui_input_' + elementDefinition.type, id: domId, type: elementDefinition.type };
// Set the validator, if any.
if ( elementDefinition.validate )
this.validate = elementDefinition.validate;
// Set the max length and size.
if ( elementDefinition.maxLength )
attributes.maxlength = elementDefinition.maxLength;
if ( elementDefinition.size )
attributes.size = elementDefinition.size;
if ( elementDefinition.inputStyle )
attributes.style = elementDefinition.inputStyle;
// If user presses Enter in a text box, it implies clicking OK for the dialog.
var me = this,
keyPressedOnMe = false;
dialog.on( 'load', function() {
me.getInputElement().on( 'keydown', function( evt ) {
if ( evt.data.getKeystroke() == 13 )
keyPressedOnMe = true;
} );
// Lower the priority this 'keyup' since 'ok' will close the dialog.(#3749)
me.getInputElement().on( 'keyup', function( evt ) {
if ( evt.data.getKeystroke() == 13 && keyPressedOnMe ) {
dialog.getButton( 'ok' ) && setTimeout( function() {
dialog.getButton( 'ok' ).click();
}, 0 );
keyPressedOnMe = false;
}
if ( me.bidi )
toggleBidiKeyUpHandler.call( me, evt );
}, null, null, 1000 );
} );
var innerHTML = function() {
// IE BUG: Text input fields in IE at 100% would exceed a or inline
// container's width, so need to wrap it inside a .
var html = [ '
' );
return html.join( '' );
};
CKEDITOR.ui.dialog.labeledElement.call( this, dialog, elementDefinition, htmlList, innerHTML );
},
/**
* A text area with a label at the top or on the left.
*
* @class CKEDITOR.ui.dialog.textarea
* @extends CKEDITOR.ui.dialog.labeledElement
* @constructor Creates a textarea class instance.
* @param {CKEDITOR.dialog} dialog Parent dialog window object.
* @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
*
* The element definition. Accepted fields:
*
* * `rows` (Optional) The number of rows displayed.
* Defaults to 5 if not defined.
* * `cols` (Optional) The number of cols displayed.
* Defaults to 20 if not defined. Usually overridden by skins.
* * `default` (Optional) The default value.
* * `validate` (Optional) The validation function.
*
* @param {Array} htmlList List of HTML code to output to.
*/
textarea: function( dialog, elementDefinition, htmlList ) {
if ( arguments.length < 3 )
return;
initPrivateObject.call( this, elementDefinition );
var me = this,
domId = this._.inputId = CKEDITOR.tools.getNextId() + '_textarea',
attributes = {};
if ( elementDefinition.validate )
this.validate = elementDefinition.validate;
// Generates the essential attributes for the textarea tag.
attributes.rows = elementDefinition.rows || 5;
attributes.cols = elementDefinition.cols || 20;
attributes[ 'class' ] = 'cke_dialog_ui_input_textarea ' + ( elementDefinition[ 'class' ] || '' );
if ( typeof elementDefinition.inputStyle != 'undefined' )
attributes.style = elementDefinition.inputStyle;
if ( elementDefinition.dir )
attributes.dir = elementDefinition.dir;
if ( me.bidi ) {
dialog.on( 'load', function() {
me.getInputElement().on( 'keyup', toggleBidiKeyUpHandler );
}, me );
}
var innerHTML = function() {
attributes[ 'aria-labelledby' ] = this._.labelId;
this._.required && ( attributes[ 'aria-required' ] = this._.required );
var html = [ '
' );
return html.join( '' );
};
CKEDITOR.ui.dialog.labeledElement.call( this, dialog, elementDefinition, htmlList, innerHTML );
},
/**
* A single checkbox with a label on the right.
*
* @class CKEDITOR.ui.dialog.checkbox
* @extends CKEDITOR.ui.dialog.uiElement
* @constructor Creates a checkbox class instance.
* @param {CKEDITOR.dialog} dialog Parent dialog window object.
* @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
* The element definition. Accepted fields:
*
* * `checked` (Optional) Whether the checkbox is checked
* on instantiation. Defaults to `false`.
* * `validate` (Optional) The validation function.
* * `label` (Optional) The checkbox label.
*
* @param {Array} htmlList List of HTML code to output to.
*/
checkbox: function( dialog, elementDefinition, htmlList ) {
if ( arguments.length < 3 )
return;
var _ = initPrivateObject.call( this, elementDefinition, { 'default': !!elementDefinition[ 'default' ] } );
if ( elementDefinition.validate )
this.validate = elementDefinition.validate;
var innerHTML = function() {
var myDefinition = CKEDITOR.tools.extend(
{},
elementDefinition,
{
id: elementDefinition.id ? elementDefinition.id + '_checkbox' : CKEDITOR.tools.getNextId() + '_checkbox'
},
true
),
html = [];
var labelId = CKEDITOR.tools.getNextId() + '_label';
var attributes = { 'class': 'cke_dialog_ui_checkbox_input', type: 'checkbox', 'aria-labelledby': labelId };
cleanInnerDefinition( myDefinition );
if ( elementDefinition[ 'default' ] )
attributes.checked = 'checked';
if ( typeof myDefinition.inputStyle != 'undefined' )
myDefinition.style = myDefinition.inputStyle;
_.checkbox = new CKEDITOR.ui.dialog.uiElement( dialog, myDefinition, html, 'input', null, attributes );
html.push(
'
',
CKEDITOR.tools.htmlEncode( elementDefinition.label ),
' '
);
return html.join( '' );
};
CKEDITOR.ui.dialog.uiElement.call( this, dialog, elementDefinition, htmlList, 'span', null, null, innerHTML );
},
/**
* A group of radio buttons.
*
* @class CKEDITOR.ui.dialog.radio
* @extends CKEDITOR.ui.dialog.labeledElement
* @constructor Creates a radio class instance.
* @param {CKEDITOR.dialog} dialog Parent dialog window object.
* @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
* The element definition. Accepted fields:
*
* * `default` (Required) The default value.
* * `validate` (Optional) The validation function.
* * `items` (Required) An array of options. Each option
* is a one- or two-item array of format `[ 'Description', 'Value' ]`. If `'Value'`
* is missing, then the value would be assumed to be the same as the description.
*
* @param {Array} htmlList List of HTML code to output to.
*/
radio: function( dialog, elementDefinition, htmlList ) {
if ( arguments.length < 3 )
return;
initPrivateObject.call( this, elementDefinition );
if ( !this._[ 'default' ] )
this._[ 'default' ] = this._.initValue = elementDefinition.items[ 0 ][ 1 ];
if ( elementDefinition.validate )
this.validate = elementDefinition.validate;
var children = [],
me = this;
var innerHTML = function() {
var inputHtmlList = [],
html = [],
commonName = ( elementDefinition.id ? elementDefinition.id : CKEDITOR.tools.getNextId() ) + '_radio';
for ( var i = 0; i < elementDefinition.items.length; i++ ) {
var item = elementDefinition.items[ i ],
title = item[ 2 ] !== undefined ? item[ 2 ] : item[ 0 ],
value = item[ 1 ] !== undefined ? item[ 1 ] : item[ 0 ],
inputId = CKEDITOR.tools.getNextId() + '_radio_input',
labelId = inputId + '_label',
inputDefinition = CKEDITOR.tools.extend( {}, elementDefinition, {
id: inputId,
title: null,
type: null
}, true ),
labelDefinition = CKEDITOR.tools.extend( {}, inputDefinition, {
title: title
}, true ),
inputAttributes = {
type: 'radio',
'class': 'cke_dialog_ui_radio_input',
name: commonName,
value: value,
'aria-labelledby': labelId
},
inputHtml = [];
if ( me._[ 'default' ] == value )
inputAttributes.checked = 'checked';
cleanInnerDefinition( inputDefinition );
cleanInnerDefinition( labelDefinition );
if ( typeof inputDefinition.inputStyle != 'undefined' )
inputDefinition.style = inputDefinition.inputStyle;
// Make inputs of radio type focusable (#10866).
inputDefinition.keyboardFocusable = true;
children.push( new CKEDITOR.ui.dialog.uiElement( dialog, inputDefinition, inputHtml, 'input', null, inputAttributes ) );
inputHtml.push( ' ' );
new CKEDITOR.ui.dialog.uiElement( dialog, labelDefinition, inputHtml, 'label', null, {
id: labelId,
'for': inputAttributes.id
}, item[ 0 ] );
inputHtmlList.push( inputHtml.join( '' ) );
}
new CKEDITOR.ui.dialog.hbox( dialog, children, inputHtmlList, html );
return html.join( '' );
};
// Adding a role="radiogroup" to definition used for wrapper.
elementDefinition.role = 'radiogroup';
elementDefinition.includeLabel = true;
CKEDITOR.ui.dialog.labeledElement.call( this, dialog, elementDefinition, htmlList, innerHTML );
this._.children = children;
},
/**
* A button with a label inside.
*
* @class CKEDITOR.ui.dialog.button
* @extends CKEDITOR.ui.dialog.uiElement
* @constructor Creates a button class instance.
* @param {CKEDITOR.dialog} dialog Parent dialog window object.
* @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
* The element definition. Accepted fields:
*
* * `label` (Required) The button label.
* * `disabled` (Optional) Set to `true` if you want the
* button to appear in the disabled state.
*
* @param {Array} htmlList List of HTML code to output to.
*/
button: function( dialog, elementDefinition, htmlList ) {
if ( !arguments.length )
return;
if ( typeof elementDefinition == 'function' )
elementDefinition = elementDefinition( dialog.getParentEditor() );
initPrivateObject.call( this, elementDefinition, { disabled: elementDefinition.disabled || false } );
// Add OnClick event to this input.
CKEDITOR.event.implementOn( this );
var me = this;
// Register an event handler for processing button clicks.
dialog.on( 'load', function() {
var element = this.getElement();
( function() {
element.on( 'click', function( evt ) {
me.click();
// #9958
evt.data.preventDefault();
} );
element.on( 'keydown', function( evt ) {
if ( evt.data.getKeystroke() in { 32: 1 } ) {
me.click();
evt.data.preventDefault();
}
} );
} )();
element.unselectable();
}, this );
var outerDefinition = CKEDITOR.tools.extend( {}, elementDefinition );
delete outerDefinition.style;
var labelId = CKEDITOR.tools.getNextId() + '_label';
CKEDITOR.ui.dialog.uiElement.call( this, dialog, outerDefinition, htmlList, 'a', null, {
style: elementDefinition.style,
href: 'javascript:void(0)', // jshint ignore:line
title: elementDefinition.label,
hidefocus: 'true',
'class': elementDefinition[ 'class' ],
role: 'button',
'aria-labelledby': labelId
}, '
' +
CKEDITOR.tools.htmlEncode( elementDefinition.label ) +
' ' );
},
/**
* A select box.
*
* @class CKEDITOR.ui.dialog.select
* @extends CKEDITOR.ui.dialog.uiElement
* @constructor Creates a button class instance.
* @param {CKEDITOR.dialog} dialog Parent dialog window object.
* @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
* The element definition. Accepted fields:
*
* * `default` (Required) The default value.
* * `validate` (Optional) The validation function.
* * `items` (Required) An array of options. Each option
* is a one- or two-item array of format `[ 'Description', 'Value' ]`. If `'Value'`
* is missing, then the value would be assumed to be the same as the
* description.
* * `multiple` (Optional) Set this to `true` if you would like
* to have a multiple-choice select box.
* * `size` (Optional) The number of items to display in
* the select box.
*
* @param {Array} htmlList List of HTML code to output to.
*/
select: function( dialog, elementDefinition, htmlList ) {
if ( arguments.length < 3 )
return;
var _ = initPrivateObject.call( this, elementDefinition );
if ( elementDefinition.validate )
this.validate = elementDefinition.validate;
_.inputId = CKEDITOR.tools.getNextId() + '_select';
var innerHTML = function() {
var myDefinition = CKEDITOR.tools.extend(
{},
elementDefinition,
{
id: ( elementDefinition.id ? elementDefinition.id + '_select' : CKEDITOR.tools.getNextId() + '_select' )
},
true
),
html = [],
innerHTML = [],
attributes = { 'id': _.inputId, 'class': 'cke_dialog_ui_input_select', 'aria-labelledby': this._.labelId };
html.push( '
' );
// Add multiple and size attributes from element definition.
if ( elementDefinition.size !== undefined )
attributes.size = elementDefinition.size;
if ( elementDefinition.multiple !== undefined )
attributes.multiple = elementDefinition.multiple;
cleanInnerDefinition( myDefinition );
for ( var i = 0, item; i < elementDefinition.items.length && ( item = elementDefinition.items[ i ] ); i++ ) {
innerHTML.push( ' ', CKEDITOR.tools.htmlEncode( item[ 0 ] ) );
}
if ( typeof myDefinition.inputStyle != 'undefined' )
myDefinition.style = myDefinition.inputStyle;
_.select = new CKEDITOR.ui.dialog.uiElement( dialog, myDefinition, html, 'select', null, attributes, innerHTML.join( '' ) );
html.push( '
' );
return html.join( '' );
};
CKEDITOR.ui.dialog.labeledElement.call( this, dialog, elementDefinition, htmlList, innerHTML );
},
/**
* A file upload input.
*
* @class CKEDITOR.ui.dialog.file
* @extends CKEDITOR.ui.dialog.labeledElement
* @constructor Creates a file class instance.
* @param {CKEDITOR.dialog} dialog Parent dialog window object.
* @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
* The element definition. Accepted fields:
*
* * `validate` (Optional) The validation function.
*
* @param {Array} htmlList List of HTML code to output to.
*/
file: function( dialog, elementDefinition, htmlList ) {
if ( arguments.length < 3 )
return;
if ( elementDefinition[ 'default' ] === undefined )
elementDefinition[ 'default' ] = '';
var _ = CKEDITOR.tools.extend( initPrivateObject.call( this, elementDefinition ), { definition: elementDefinition, buttons: [] } );
if ( elementDefinition.validate )
this.validate = elementDefinition.validate;
/** @ignore */
var innerHTML = function() {
_.frameId = CKEDITOR.tools.getNextId() + '_fileInput';
var html = [
'
' );
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 ( !onClick || onClick.call( this, evt ) !== false ) {
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
* @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
* @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 UI element.
*/
getInputElement: function() {
return this._.select.getElement();
},
/**
* Adds an option to the select box.
*
* @param {String} label Option label.
* @param {String} value (Optional) Option value, if not defined it will be
* assumed to be the same as the label.
* @param {Number} index (Optional) Position of the option to be inserted
* to. If not defined, the new option will be inserted to the end of list.
* @returns {CKEDITOR.ui.dialog.select} The current select UI element.
*/
add: function( label, value, index ) {
var option = new CKEDITOR.dom.element( 'option', this.getDialog().getParentEditor().document ),
selectElement = this.getInputElement().$;
option.$.text = label;
option.$.value = ( value === undefined || value === null ) ? label : value;
if ( index === undefined || index === null ) {
if ( CKEDITOR.env.ie ) {
selectElement.add( option.$ );
} else {
selectElement.add( option.$, null );
}
} else {
selectElement.add( option.$, index );
}
return this;
},
/**
* Removes an option from the selection list.
*
* @param {Number} index Index of the option to be removed.
* @returns {CKEDITOR.ui.dialog.select} The current select UI element.
*/
remove: function( index ) {
var selectElement = this.getInputElement().$;
selectElement.remove( index );
return this;
},
/**
* Clears all options out of the selection list.
*
* @returns {CKEDITOR.ui.dialog.select} The current select UI element.
*/
clear: function() {
var selectElement = this.getInputElement().$;
while ( selectElement.length > 0 )
selectElement.remove( 0 );
return this;
},
keyboardFocusable: true
}, commonPrototype, true );
/** @class CKEDITOR.ui.dialog.checkbox */
CKEDITOR.ui.dialog.checkbox.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.uiElement(), {
/**
* Gets the checkbox DOM element.
*
* @returns {CKEDITOR.dom.element} The DOM element of the checkbox.
*/
getInputElement: function() {
return this._.checkbox.getElement();
},
/**
* Sets the state of the checkbox.
*
* @param {Boolean} checked `true` to tick the checkbox, `false` to untick it.
* @param {Boolean} noChangeEvent Internal commit, to supress `change` event on this element.
*/
setValue: function( checked, noChangeEvent ) {
this.getInputElement().$.checked = checked;
!noChangeEvent && this.fire( 'change', { value: checked } );
},
/**
* Gets the state of the checkbox.
*
* @returns {Boolean} `true` means that the checkbox is ticked, `false` means it is not ticked.
*/
getValue: function() {
return this.getInputElement().$.checked;
},
/**
* Handler for the access key up event. Toggles the checkbox.
*/
accessKeyUp: function() {
this.setValue( !this.getValue() );
},
/**
* Defines the `onChange` event for UI element definitions.
*
* @property {Object}
*/
eventProcessors: {
onChange: function( dialog, func ) {
if ( !CKEDITOR.env.ie || ( CKEDITOR.env.version > 8 ) )
return commonEventProcessors.onChange.apply( this, arguments );
else {
dialog.on( 'load', function() {
var element = this._.checkbox.getElement();
element.on( 'propertychange', function( evt ) {
evt = evt.data.$;
if ( evt.propertyName == 'checked' )
this.fire( 'change', { value: element.$.checked } );
}, this );
}, this );
this.on( 'change', func );
}
return null;
}
},
keyboardFocusable: true
}, commonPrototype, true );
/** @class CKEDITOR.ui.dialog.radio */
CKEDITOR.ui.dialog.radio.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.uiElement(), {
/**
* Selects one of the radio buttons in this button group.
*
* @param {String} value The value of the button to be chcked.
* @param {Boolean} noChangeEvent Internal commit, to supress the `change` event on this element.
*/
setValue: function( value, noChangeEvent ) {
var children = this._.children,
item;
for ( var i = 0;
( i < children.length ) && ( item = children[ i ] ); i++ )
item.getElement().$.checked = ( item.getValue() == value );
!noChangeEvent && this.fire( 'change', { value: value } );
},
/**
* Gets the value of the currently selected radio button.
*
* @returns {String} The currently selected button's value.
*/
getValue: function() {
var children = this._.children;
for ( var i = 0; i < children.length; i++ ) {
if ( children[ i ].getElement().$.checked )
return children[ i ].getValue();
}
return null;
},
/**
* Handler for the access key up event. Focuses the currently
* selected radio button, or the first radio button if none is selected.
*/
accessKeyUp: function() {
var children = this._.children,
i;
for ( i = 0; i < children.length; i++ ) {
if ( children[ i ].getElement().$.checked ) {
children[ i ].getElement().focus();
return;
}
}
children[ 0 ].getElement().focus();
},
/**
* Defines the `onChange` event for UI element definitions.
*
* @property {Object}
*/
eventProcessors: {
onChange: function( dialog, func ) {
if ( !CKEDITOR.env.ie )
return commonEventProcessors.onChange.apply( this, arguments );
else {
dialog.on( 'load', function() {
var children = this._.children,
me = this;
for ( var i = 0; i < children.length; i++ ) {
var element = children[ i ].getElement();
element.on( 'propertychange', function( evt ) {
evt = evt.data.$;
if ( evt.propertyName == 'checked' && this.$.checked )
me.fire( 'change', { value: this.getAttribute( 'value' ) } );
} );
}
}, this );
this.on( 'change', func );
}
return null;
}
}
}, commonPrototype, true );
/** @class CKEDITOR.ui.dialog.file */
CKEDITOR.ui.dialog.file.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.labeledElement(), commonPrototype, {
/**
* Gets 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 ` , then the td should definitely be
// included.
if ( node.type == CKEDITOR.NODE_ELEMENT && cellNodeRegex.test( node.getName() ) && !node.getCustomData( 'selected_cell' ) ) {
CKEDITOR.dom.element.setMarker( database, node, 'selected_cell', true );
retval.push( node );
}
}
for ( var i = 0; i < ranges.length; i++ ) {
var range = ranges[ i ];
if ( range.collapsed ) {
// Walker does not handle collapsed ranges yet - fall back to old API.
var startNode = range.getCommonAncestor();
var nearestCell = startNode.getAscendant( 'td', true ) || startNode.getAscendant( 'th', true );
if ( nearestCell )
retval.push( nearestCell );
} else {
var walker = new CKEDITOR.dom.walker( range );
var node;
walker.guard = moveOutOfCellGuard;
while ( ( node = walker.next() ) ) {
// If may be possible for us to have a range like this:
// ^1 ^2
// The 2nd td shouldn't be included.
//
// So we have to take care to include a td we've entered only when we've
// walked into its children.
if ( node.type != CKEDITOR.NODE_ELEMENT || !node.is( CKEDITOR.dtd.table ) ) {
var parent = node.getAscendant( 'td', true ) || node.getAscendant( 'th', true );
if ( parent && !parent.getCustomData( 'selected_cell' ) ) {
CKEDITOR.dom.element.setMarker( database, parent, 'selected_cell', true );
retval.push( parent );
}
}
}
}
}
CKEDITOR.dom.element.clearAllMarkers( database );
return retval;
}
function getFocusElementAfterDelCells( cellsToDelete ) {
var i = 0,
last = cellsToDelete.length - 1,
database = {},
cell, focusedCell, tr;
while ( ( cell = cellsToDelete[ i++ ] ) )
CKEDITOR.dom.element.setMarker( database, cell, 'delete_cell', true );
// 1.first we check left or right side focusable cell row by row;
i = 0;
while ( ( cell = cellsToDelete[ i++ ] ) ) {
if ( ( focusedCell = cell.getPrevious() ) && !focusedCell.getCustomData( 'delete_cell' ) || ( focusedCell = cell.getNext() ) && !focusedCell.getCustomData( 'delete_cell' ) ) {
CKEDITOR.dom.element.clearAllMarkers( database );
return focusedCell;
}
}
CKEDITOR.dom.element.clearAllMarkers( database );
// 2. then we check the toppest row (outside the selection area square) focusable cell
tr = cellsToDelete[ 0 ].getParent();
if ( ( tr = tr.getPrevious() ) )
return tr.getLast();
// 3. last we check the lowerest row focusable cell
tr = cellsToDelete[ last ].getParent();
if ( ( tr = tr.getNext() ) )
return tr.getChild( 0 );
return null;
}
function insertRow( selection, insertBefore ) {
var cells = getSelectedCells( selection ),
firstCell = cells[ 0 ],
table = firstCell.getAscendant( 'table' ),
doc = firstCell.getDocument(),
startRow = cells[ 0 ].getParent(),
startRowIndex = startRow.$.rowIndex,
lastCell = cells[ cells.length - 1 ],
endRowIndex = lastCell.getParent().$.rowIndex + lastCell.$.rowSpan - 1,
endRow = new CKEDITOR.dom.element( table.$.rows[ endRowIndex ] ),
rowIndex = insertBefore ? startRowIndex : endRowIndex,
row = insertBefore ? startRow : endRow;
var map = CKEDITOR.tools.buildTableMap( table ),
cloneRow = map[ rowIndex ],
nextRow = insertBefore ? map[ rowIndex - 1 ] : map[ rowIndex + 1 ],
width = map[ 0 ].length;
var newRow = doc.createElement( 'tr' );
for ( var i = 0; cloneRow[ i ] && i < width; i++ ) {
var cell;
// Check whether there's a spanning row here, do not break it.
if ( cloneRow[ i ].rowSpan > 1 && nextRow && cloneRow[ i ] == nextRow[ i ] ) {
cell = cloneRow[ i ];
cell.rowSpan += 1;
} else {
cell = new CKEDITOR.dom.element( cloneRow[ i ] ).clone();
cell.removeAttribute( 'rowSpan' );
cell.appendBogus();
newRow.append( cell );
cell = cell.$;
}
i += cell.colSpan - 1;
}
insertBefore ? newRow.insertBefore( row ) : newRow.insertAfter( row );
}
function deleteRows( selectionOrRow ) {
if ( selectionOrRow instanceof CKEDITOR.dom.selection ) {
var cells = getSelectedCells( selectionOrRow ),
firstCell = cells[ 0 ],
table = firstCell.getAscendant( 'table' ),
map = CKEDITOR.tools.buildTableMap( table ),
startRow = cells[ 0 ].getParent(),
startRowIndex = startRow.$.rowIndex,
lastCell = cells[ cells.length - 1 ],
endRowIndex = lastCell.getParent().$.rowIndex + lastCell.$.rowSpan - 1,
rowsToDelete = [];
// Delete cell or reduce cell spans by checking through the table map.
for ( var i = startRowIndex; i <= endRowIndex; i++ ) {
var mapRow = map[ i ],
row = new CKEDITOR.dom.element( table.$.rows[ i ] );
for ( var j = 0; j < mapRow.length; j++ ) {
var cell = new CKEDITOR.dom.element( mapRow[ j ] ),
cellRowIndex = cell.getParent().$.rowIndex;
if ( cell.$.rowSpan == 1 )
cell.remove();
// Row spanned cell.
else {
// Span row of the cell, reduce spanning.
cell.$.rowSpan -= 1;
// Root row of the cell, root cell to next row.
if ( cellRowIndex == i ) {
var nextMapRow = map[ i + 1 ];
nextMapRow[ j - 1 ] ? cell.insertAfter( new CKEDITOR.dom.element( nextMapRow[ j - 1 ] ) ) : new CKEDITOR.dom.element( table.$.rows[ i + 1 ] ).append( cell, 1 );
}
}
j += cell.$.colSpan - 1;
}
rowsToDelete.push( row );
}
var rows = table.$.rows;
// Where to put the cursor after rows been deleted?
// 1. Into next sibling row if any;
// 2. Into previous sibling row if any;
// 3. Into table's parent element if it's the very last row.
var cursorPosition = new CKEDITOR.dom.element( rows[ endRowIndex + 1 ] || ( startRowIndex > 0 ? rows[ startRowIndex - 1 ] : null ) || table.$.parentNode );
for ( i = rowsToDelete.length; i >= 0; i-- )
deleteRows( rowsToDelete[ i ] );
return cursorPosition;
} else if ( selectionOrRow instanceof CKEDITOR.dom.element ) {
table = selectionOrRow.getAscendant( 'table' );
if ( table.$.rows.length == 1 )
table.remove();
else
selectionOrRow.remove();
}
return null;
}
function getCellColIndex( cell, isStart ) {
var row = cell.getParent(),
rowCells = row.$.cells;
var colIndex = 0;
for ( var i = 0; i < rowCells.length; i++ ) {
var mapCell = rowCells[ i ];
colIndex += isStart ? 1 : mapCell.colSpan;
if ( mapCell == cell.$ )
break;
}
return colIndex - 1;
}
function getColumnsIndices( cells, isStart ) {
var retval = isStart ? Infinity : 0;
for ( var i = 0; i < cells.length; i++ ) {
var colIndex = getCellColIndex( cells[ i ], isStart );
if ( isStart ? colIndex < retval : colIndex > retval )
retval = colIndex;
}
return retval;
}
function insertColumn( selection, insertBefore ) {
var cells = getSelectedCells( selection ),
firstCell = cells[ 0 ],
table = firstCell.getAscendant( 'table' ),
startCol = getColumnsIndices( cells, 1 ),
lastCol = getColumnsIndices( cells ),
colIndex = insertBefore ? startCol : lastCol;
var map = CKEDITOR.tools.buildTableMap( table ),
cloneCol = [],
nextCol = [],
height = map.length;
for ( var i = 0; i < height; i++ ) {
cloneCol.push( map[ i ][ colIndex ] );
var nextCell = insertBefore ? map[ i ][ colIndex - 1 ] : map[ i ][ colIndex + 1 ];
nextCol.push( nextCell );
}
for ( i = 0; i < height; i++ ) {
var cell;
if ( !cloneCol[ i ] )
continue;
// Check whether there's a spanning column here, do not break it.
if ( cloneCol[ i ].colSpan > 1 && nextCol[ i ] == cloneCol[ i ] ) {
cell = cloneCol[ i ];
cell.colSpan += 1;
} else {
cell = new CKEDITOR.dom.element( cloneCol[ i ] ).clone();
cell.removeAttribute( 'colSpan' );
cell.appendBogus();
cell[ insertBefore ? 'insertBefore' : 'insertAfter' ].call( cell, new CKEDITOR.dom.element( cloneCol[ i ] ) );
cell = cell.$;
}
i += cell.rowSpan - 1;
}
}
function deleteColumns( selectionOrCell ) {
var cells = getSelectedCells( selectionOrCell ),
firstCell = cells[ 0 ],
lastCell = cells[ cells.length - 1 ],
table = firstCell.getAscendant( 'table' ),
map = CKEDITOR.tools.buildTableMap( table ),
startColIndex, endColIndex,
rowsToDelete = [];
// Figure out selected cells' column indices.
for ( var i = 0, rows = map.length; i < rows; i++ ) {
for ( var j = 0, cols = map[ i ].length; j < cols; j++ ) {
if ( map[ i ][ j ] == firstCell.$ )
startColIndex = j;
if ( map[ i ][ j ] == lastCell.$ )
endColIndex = j;
}
}
// Delete cell or reduce cell spans by checking through the table map.
for ( i = startColIndex; i <= endColIndex; i++ ) {
for ( j = 0; j < map.length; j++ ) {
var mapRow = map[ j ],
row = new CKEDITOR.dom.element( table.$.rows[ j ] ),
cell = new CKEDITOR.dom.element( mapRow[ i ] );
if ( cell.$ ) {
if ( cell.$.colSpan == 1 )
cell.remove();
// Reduce the col spans.
else
cell.$.colSpan -= 1;
j += cell.$.rowSpan - 1;
if ( !row.$.cells.length )
rowsToDelete.push( row );
}
}
}
var firstRowCells = table.$.rows[ 0 ] && table.$.rows[ 0 ].cells;
// Where to put the cursor after columns been deleted?
// 1. Into next cell of the first row if any;
// 2. Into previous cell of the first row if any;
// 3. Into table's parent element;
var cursorPosition = new CKEDITOR.dom.element( firstRowCells[ startColIndex ] || ( startColIndex ? firstRowCells[ startColIndex - 1 ] : table.$.parentNode ) );
// Delete table rows only if all columns are gone (do not remove empty row).
if ( rowsToDelete.length == rows )
table.remove();
return cursorPosition;
}
function insertCell( selection, insertBefore ) {
var startElement = selection.getStartElement();
var cell = startElement.getAscendant( 'td', 1 ) || startElement.getAscendant( 'th', 1 );
if ( !cell )
return;
// Create the new cell element to be added.
var newCell = cell.clone();
newCell.appendBogus();
if ( insertBefore )
newCell.insertBefore( cell );
else
newCell.insertAfter( cell );
}
function deleteCells( selectionOrCell ) {
if ( selectionOrCell instanceof CKEDITOR.dom.selection ) {
var cellsToDelete = getSelectedCells( selectionOrCell );
var table = cellsToDelete[ 0 ] && cellsToDelete[ 0 ].getAscendant( 'table' );
var cellToFocus = getFocusElementAfterDelCells( cellsToDelete );
for ( var i = cellsToDelete.length - 1; i >= 0; i-- )
deleteCells( cellsToDelete[ i ] );
if ( cellToFocus )
placeCursorInCell( cellToFocus, true );
else if ( table )
table.remove();
} else if ( selectionOrCell instanceof CKEDITOR.dom.element ) {
var tr = selectionOrCell.getParent();
if ( tr.getChildCount() == 1 )
tr.remove();
else
selectionOrCell.remove();
}
}
// Remove filler at end and empty spaces around the cell content.
function trimCell( cell ) {
var bogus = cell.getBogus();
bogus && bogus.remove();
cell.trim();
}
function placeCursorInCell( cell, placeAtEnd ) {
var docInner = cell.getDocument(),
docOuter = CKEDITOR.document;
// Fixing "Unspecified error" thrown in IE10 by resetting
// selection the dirty and shameful way (#10308).
// We can not apply this hack to IE8 because
// it causes error (#11058).
if ( CKEDITOR.env.ie && CKEDITOR.env.version == 10 ) {
docOuter.focus();
docInner.focus();
}
var range = new CKEDITOR.dom.range( docInner );
if ( !range[ 'moveToElementEdit' + ( placeAtEnd ? 'End' : 'Start' ) ]( cell ) ) {
range.selectNodeContents( cell );
range.collapse( placeAtEnd ? false : true );
}
range.select( true );
}
function cellInRow( tableMap, rowIndex, cell ) {
var oRow = tableMap[ rowIndex ];
if ( typeof cell == 'undefined' )
return oRow;
for ( var c = 0; oRow && c < oRow.length; c++ ) {
if ( cell.is && oRow[ c ] == cell.$ )
return c;
else if ( c == cell )
return new CKEDITOR.dom.element( oRow[ c ] );
}
return cell.is ? -1 : null;
}
function cellInCol( tableMap, colIndex ) {
var oCol = [];
for ( var r = 0; r < tableMap.length; r++ ) {
var row = tableMap[ r ];
oCol.push( row[ colIndex ] );
// Avoid adding duplicate cells.
if ( row[ colIndex ].rowSpan > 1 )
r += row[ colIndex ].rowSpan - 1;
}
return oCol;
}
function mergeCells( selection, mergeDirection, isDetect ) {
var cells = getSelectedCells( selection );
// Invalid merge request if:
// 1. In batch mode despite that less than two selected.
// 2. In solo mode while not exactly only one selected.
// 3. Cells distributed in different table groups (e.g. from both thead and tbody).
var commonAncestor;
if ( ( mergeDirection ? cells.length != 1 : cells.length < 2 ) || ( commonAncestor = selection.getCommonAncestor() ) && commonAncestor.type == CKEDITOR.NODE_ELEMENT && commonAncestor.is( 'table' ) )
return false;
var cell,
firstCell = cells[ 0 ],
table = firstCell.getAscendant( 'table' ),
map = CKEDITOR.tools.buildTableMap( table ),
mapHeight = map.length,
mapWidth = map[ 0 ].length,
startRow = firstCell.getParent().$.rowIndex,
startColumn = cellInRow( map, startRow, firstCell );
if ( mergeDirection ) {
var targetCell;
try {
var rowspan = parseInt( firstCell.getAttribute( 'rowspan' ), 10 ) || 1;
var colspan = parseInt( firstCell.getAttribute( 'colspan' ), 10 ) || 1;
targetCell = map[ mergeDirection == 'up' ? ( startRow - rowspan ) : mergeDirection == 'down' ? ( startRow + rowspan ) : startRow ][
mergeDirection == 'left' ?
( startColumn - colspan ) :
mergeDirection == 'right' ? ( startColumn + colspan ) : startColumn ];
} catch ( er ) {
return false;
}
// 1. No cell could be merged.
// 2. Same cell actually.
if ( !targetCell || firstCell.$ == targetCell )
return false;
// Sort in map order regardless of the DOM sequence.
cells[ ( mergeDirection == 'up' || mergeDirection == 'left' ) ? 'unshift' : 'push' ]( new CKEDITOR.dom.element( targetCell ) );
}
// Start from here are merging way ignorance (merge up/right, batch merge).
var doc = firstCell.getDocument(),
lastRowIndex = startRow,
totalRowSpan = 0,
totalColSpan = 0,
// Use a documentFragment as buffer when appending cell contents.
frag = !isDetect && new CKEDITOR.dom.documentFragment( doc ),
dimension = 0;
for ( var i = 0; i < cells.length; i++ ) {
cell = cells[ i ];
var tr = cell.getParent(),
cellFirstChild = cell.getFirst(),
colSpan = cell.$.colSpan,
rowSpan = cell.$.rowSpan,
rowIndex = tr.$.rowIndex,
colIndex = cellInRow( map, rowIndex, cell );
// Accumulated the actual places taken by all selected cells.
dimension += colSpan * rowSpan;
// Accumulated the maximum virtual spans from column and row.
totalColSpan = Math.max( totalColSpan, colIndex - startColumn + colSpan );
totalRowSpan = Math.max( totalRowSpan, rowIndex - startRow + rowSpan );
if ( !isDetect ) {
// Trim all cell fillers and check to remove empty cells.
if ( trimCell( cell ), cell.getChildren().count() ) {
// Merge vertically cells as two separated paragraphs.
if ( rowIndex != lastRowIndex && cellFirstChild && !( cellFirstChild.isBlockBoundary && cellFirstChild.isBlockBoundary( { br: 1 } ) ) ) {
var last = frag.getLast( CKEDITOR.dom.walker.whitespaces( true ) );
if ( last && !( last.is && last.is( 'br' ) ) )
frag.append( 'br' );
}
cell.moveChildren( frag );
}
i ? cell.remove() : cell.setHtml( '' );
}
lastRowIndex = rowIndex;
}
if ( !isDetect ) {
frag.moveChildren( firstCell );
firstCell.appendBogus();
if ( totalColSpan >= mapWidth )
firstCell.removeAttribute( 'rowSpan' );
else
firstCell.$.rowSpan = totalRowSpan;
if ( totalRowSpan >= mapHeight )
firstCell.removeAttribute( 'colSpan' );
else
firstCell.$.colSpan = totalColSpan;
// Swip empty left at the end of table due to the merging.
var trs = new CKEDITOR.dom.nodeList( table.$.rows ),
count = trs.count();
for ( i = count - 1; i >= 0; i-- ) {
var tailTr = trs.getItem( i );
if ( !tailTr.$.cells.length ) {
tailTr.remove();
count++;
continue;
}
}
return firstCell;
}
// Be able to merge cells only if actual dimension of selected
// cells equals to the caculated rectangle.
else {
return ( totalRowSpan * totalColSpan ) == dimension;
}
}
function horizontalSplitCell( selection, isDetect ) {
var cells = getSelectedCells( selection );
if ( cells.length > 1 )
return false;
else if ( isDetect )
return true;
var cell = cells[ 0 ],
tr = cell.getParent(),
table = tr.getAscendant( 'table' ),
map = CKEDITOR.tools.buildTableMap( table ),
rowIndex = tr.$.rowIndex,
colIndex = cellInRow( map, rowIndex, cell ),
rowSpan = cell.$.rowSpan,
newCell, newRowSpan, newCellRowSpan, newRowIndex;
if ( rowSpan > 1 ) {
newRowSpan = Math.ceil( rowSpan / 2 );
newCellRowSpan = Math.floor( rowSpan / 2 );
newRowIndex = rowIndex + newRowSpan;
var newCellTr = new CKEDITOR.dom.element( table.$.rows[ newRowIndex ] ),
newCellRow = cellInRow( map, newRowIndex ),
candidateCell;
newCell = cell.clone();
// Figure out where to insert the new cell by checking the vitual row.
for ( var c = 0; c < newCellRow.length; c++ ) {
candidateCell = newCellRow[ c ];
// Catch first cell actually following the column.
if ( candidateCell.parentNode == newCellTr.$ && c > colIndex ) {
newCell.insertBefore( new CKEDITOR.dom.element( candidateCell ) );
break;
} else {
candidateCell = null;
}
}
// The destination row is empty, append at will.
if ( !candidateCell )
newCellTr.append( newCell );
} else {
newCellRowSpan = newRowSpan = 1;
newCellTr = tr.clone();
newCellTr.insertAfter( tr );
newCellTr.append( newCell = cell.clone() );
var cellsInSameRow = cellInRow( map, rowIndex );
for ( var i = 0; i < cellsInSameRow.length; i++ )
cellsInSameRow[ i ].rowSpan++;
}
newCell.appendBogus();
cell.$.rowSpan = newRowSpan;
newCell.$.rowSpan = newCellRowSpan;
if ( newRowSpan == 1 )
cell.removeAttribute( 'rowSpan' );
if ( newCellRowSpan == 1 )
newCell.removeAttribute( 'rowSpan' );
return newCell;
}
function verticalSplitCell( selection, isDetect ) {
var cells = getSelectedCells( selection );
if ( cells.length > 1 )
return false;
else if ( isDetect )
return true;
var cell = cells[ 0 ],
tr = cell.getParent(),
table = tr.getAscendant( 'table' ),
map = CKEDITOR.tools.buildTableMap( table ),
rowIndex = tr.$.rowIndex,
colIndex = cellInRow( map, rowIndex, cell ),
colSpan = cell.$.colSpan,
newCell, newColSpan, newCellColSpan;
if ( colSpan > 1 ) {
newColSpan = Math.ceil( colSpan / 2 );
newCellColSpan = Math.floor( colSpan / 2 );
} else {
newCellColSpan = newColSpan = 1;
var cellsInSameCol = cellInCol( map, colIndex );
for ( var i = 0; i < cellsInSameCol.length; i++ )
cellsInSameCol[ i ].colSpan++;
}
newCell = cell.clone();
newCell.insertAfter( cell );
newCell.appendBogus();
cell.$.colSpan = newColSpan;
newCell.$.colSpan = newCellColSpan;
if ( newColSpan == 1 )
cell.removeAttribute( 'colSpan' );
if ( newCellColSpan == 1 )
newCell.removeAttribute( 'colSpan' );
return newCell;
}
CKEDITOR.plugins.tabletools = {
requires: 'table,dialog,contextmenu',
init: function( editor ) {
var lang = editor.lang.table;
function createDef( def ) {
return CKEDITOR.tools.extend( def || {}, {
contextSensitive: 1,
refresh: function( editor, path ) {
this.setState( path.contains( { td: 1, th: 1 }, 1 ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
}
} );
}
function addCmd( name, def ) {
var cmd = editor.addCommand( name, def );
editor.addFeature( cmd );
}
addCmd( 'cellProperties', new CKEDITOR.dialogCommand( 'cellProperties', createDef( {
allowedContent: 'td th{width,height,border-color,background-color,white-space,vertical-align,text-align}[colspan,rowspan]',
requiredContent: 'table'
} ) ) );
CKEDITOR.dialog.add( 'cellProperties', this.path + 'dialogs/tableCell.js' );
addCmd( 'rowDelete', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection();
placeCursorInCell( deleteRows( selection ) );
}
} ) );
addCmd( 'rowInsertBefore', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection();
insertRow( selection, true );
}
} ) );
addCmd( 'rowInsertAfter', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection();
insertRow( selection );
}
} ) );
addCmd( 'columnDelete', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection();
var element = deleteColumns( selection );
element && placeCursorInCell( element, true );
}
} ) );
addCmd( 'columnInsertBefore', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection();
insertColumn( selection, true );
}
} ) );
addCmd( 'columnInsertAfter', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection();
insertColumn( selection );
}
} ) );
addCmd( 'cellDelete', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection();
deleteCells( selection );
}
} ) );
addCmd( 'cellMerge', createDef( {
allowedContent: 'td[colspan,rowspan]',
requiredContent: 'td[colspan,rowspan]',
exec: function( editor ) {
placeCursorInCell( mergeCells( editor.getSelection() ), true );
}
} ) );
addCmd( 'cellMergeRight', createDef( {
allowedContent: 'td[colspan]',
requiredContent: 'td[colspan]',
exec: function( editor ) {
placeCursorInCell( mergeCells( editor.getSelection(), 'right' ), true );
}
} ) );
addCmd( 'cellMergeDown', createDef( {
allowedContent: 'td[rowspan]',
requiredContent: 'td[rowspan]',
exec: function( editor ) {
placeCursorInCell( mergeCells( editor.getSelection(), 'down' ), true );
}
} ) );
addCmd( 'cellVerticalSplit', createDef( {
allowedContent: 'td[rowspan]',
requiredContent: 'td[rowspan]',
exec: function( editor ) {
placeCursorInCell( verticalSplitCell( editor.getSelection() ) );
}
} ) );
addCmd( 'cellHorizontalSplit', createDef( {
allowedContent: 'td[colspan]',
requiredContent: 'td[colspan]',
exec: function( editor ) {
placeCursorInCell( horizontalSplitCell( editor.getSelection() ) );
}
} ) );
addCmd( 'cellInsertBefore', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection();
insertCell( selection, true );
}
} ) );
addCmd( 'cellInsertAfter', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection();
insertCell( selection );
}
} ) );
// If the "menu" plugin is loaded, register the menu items.
if ( editor.addMenuItems ) {
editor.addMenuItems( {
tablecell: {
label: lang.cell.menu,
group: 'tablecell',
order: 1,
getItems: function() {
var selection = editor.getSelection(),
cells = getSelectedCells( selection );
return {
tablecell_insertBefore: CKEDITOR.TRISTATE_OFF,
tablecell_insertAfter: CKEDITOR.TRISTATE_OFF,
tablecell_delete: CKEDITOR.TRISTATE_OFF,
tablecell_merge: mergeCells( selection, null, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
tablecell_merge_right: mergeCells( selection, 'right', true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
tablecell_merge_down: mergeCells( selection, 'down', true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
tablecell_split_vertical: verticalSplitCell( selection, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
tablecell_split_horizontal: horizontalSplitCell( selection, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
tablecell_properties: cells.length > 0 ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED
};
}
},
tablecell_insertBefore: {
label: lang.cell.insertBefore,
group: 'tablecell',
command: 'cellInsertBefore',
order: 5
},
tablecell_insertAfter: {
label: lang.cell.insertAfter,
group: 'tablecell',
command: 'cellInsertAfter',
order: 10
},
tablecell_delete: {
label: lang.cell.deleteCell,
group: 'tablecell',
command: 'cellDelete',
order: 15
},
tablecell_merge: {
label: lang.cell.merge,
group: 'tablecell',
command: 'cellMerge',
order: 16
},
tablecell_merge_right: {
label: lang.cell.mergeRight,
group: 'tablecell',
command: 'cellMergeRight',
order: 17
},
tablecell_merge_down: {
label: lang.cell.mergeDown,
group: 'tablecell',
command: 'cellMergeDown',
order: 18
},
tablecell_split_horizontal: {
label: lang.cell.splitHorizontal,
group: 'tablecell',
command: 'cellHorizontalSplit',
order: 19
},
tablecell_split_vertical: {
label: lang.cell.splitVertical,
group: 'tablecell',
command: 'cellVerticalSplit',
order: 20
},
tablecell_properties: {
label: lang.cell.title,
group: 'tablecellproperties',
command: 'cellProperties',
order: 21
},
tablerow: {
label: lang.row.menu,
group: 'tablerow',
order: 1,
getItems: function() {
return {
tablerow_insertBefore: CKEDITOR.TRISTATE_OFF,
tablerow_insertAfter: CKEDITOR.TRISTATE_OFF,
tablerow_delete: CKEDITOR.TRISTATE_OFF
};
}
},
tablerow_insertBefore: {
label: lang.row.insertBefore,
group: 'tablerow',
command: 'rowInsertBefore',
order: 5
},
tablerow_insertAfter: {
label: lang.row.insertAfter,
group: 'tablerow',
command: 'rowInsertAfter',
order: 10
},
tablerow_delete: {
label: lang.row.deleteRow,
group: 'tablerow',
command: 'rowDelete',
order: 15
},
tablecolumn: {
label: lang.column.menu,
group: 'tablecolumn',
order: 1,
getItems: function() {
return {
tablecolumn_insertBefore: CKEDITOR.TRISTATE_OFF,
tablecolumn_insertAfter: CKEDITOR.TRISTATE_OFF,
tablecolumn_delete: CKEDITOR.TRISTATE_OFF
};
}
},
tablecolumn_insertBefore: {
label: lang.column.insertBefore,
group: 'tablecolumn',
command: 'columnInsertBefore',
order: 5
},
tablecolumn_insertAfter: {
label: lang.column.insertAfter,
group: 'tablecolumn',
command: 'columnInsertAfter',
order: 10
},
tablecolumn_delete: {
label: lang.column.deleteColumn,
group: 'tablecolumn',
command: 'columnDelete',
order: 15
}
} );
}
// If the "contextmenu" plugin is laoded, register the listeners.
if ( editor.contextMenu ) {
editor.contextMenu.addListener( function( element, selection, path ) {
var cell = path.contains( { 'td': 1, 'th': 1 }, 1 );
if ( cell && !cell.isReadOnly() ) {
return {
tablecell: CKEDITOR.TRISTATE_OFF,
tablerow: CKEDITOR.TRISTATE_OFF,
tablecolumn: CKEDITOR.TRISTATE_OFF
};
}
return null;
} );
}
},
getSelectedCells: getSelectedCells
};
CKEDITOR.plugins.add( 'tabletools', CKEDITOR.plugins.tabletools );
} )();
/**
* Create a two-dimension array that reflects the actual layout of table cells,
* with cell spans, with mappings to the original td elements.
*
* @param {CKEDITOR.dom.element} table
* @member CKEDITOR.tools
*/
CKEDITOR.tools.buildTableMap = function( table ) {
var aRows = table.$.rows;
// Row and Column counters.
var r = -1;
var aMap = [];
for ( var i = 0; i < aRows.length; i++ ) {
r++;
!aMap[ r ] && ( aMap[ r ] = [] );
var c = -1;
for ( var j = 0; j < aRows[ i ].cells.length; j++ ) {
var oCell = aRows[ i ].cells[ j ];
c++;
while ( aMap[ r ][ c ] )
c++;
var iColSpan = isNaN( oCell.colSpan ) ? 1 : oCell.colSpan;
var iRowSpan = isNaN( oCell.rowSpan ) ? 1 : oCell.rowSpan;
for ( var rs = 0; rs < iRowSpan; rs++ ) {
if ( !aMap[ r + rs ] )
aMap[ r + rs ] = [];
for ( var cs = 0; cs < iColSpan; cs++ ) {
aMap[ r + rs ][ c + cs ] = aRows[ i ].cells[ j ];
}
}
c += iColSpan - 1;
}
}
return aMap;
};
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
CKEDITOR.plugins.add( 'templates', {
requires: 'dialog',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
CKEDITOR.dialog.add( 'templates', CKEDITOR.getUrl( this.path + 'dialogs/templates.js' ) );
editor.addCommand( 'templates', new CKEDITOR.dialogCommand( 'templates' ) );
editor.ui.addButton && editor.ui.addButton( 'Templates', {
label: editor.lang.templates.button,
command: 'templates',
toolbar: 'doctools,10'
} );
}
} );
var templates = {},
loadedTemplatesFiles = {};
CKEDITOR.addTemplates = function( name, definition ) {
templates[ name ] = definition;
};
CKEDITOR.getTemplates = function( name ) {
return templates[ name ];
};
CKEDITOR.loadTemplates = function( templateFiles, callback ) {
// Holds the templates files to be loaded.
var toLoad = [];
// Look for pending template files to get loaded.
for ( var i = 0, count = templateFiles.length; i < count; i++ ) {
if ( !loadedTemplatesFiles[ templateFiles[ i ] ] ) {
toLoad.push( templateFiles[ i ] );
loadedTemplatesFiles[ templateFiles[ i ] ] = 1;
}
}
if ( toLoad.length )
CKEDITOR.scriptLoader.load( toLoad, callback );
else
setTimeout( callback, 0 );
};
} )();
/**
* The templates definition set to use. It accepts a list of names separated by
* comma. It must match definitions loaded with the {@link #templates_files} setting.
*
* config.templates = 'my_templates';
*
* @cfg {String} [templates='default']
* @member CKEDITOR.config
*/
/**
* The list of templates definition files to load.
*
* config.templates_files = [
* '/editor_templates/site_default.js',
* 'http://www.example.com/user_templates.js
* ];
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.templates_files = [
CKEDITOR.getUrl( 'plugins/templates/templates/default.js' )
];
/**
* Whether the "Replace actual contents" checkbox is checked by default in the
* Templates dialog.
*
* config.templates_replaceContent = false;
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.templates_replaceContent = true;
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/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. (#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 ) {
// Note: code is duplicated in plugins\richcombo\plugin.js in keyDownFn().
editor.once( 'panelShow', function( evt ) {
evt.data._.panel._.currentBlock.onKeyDown( 40 );
} );
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 );
for ( var r = 0; r < toolbar.length; r++ ) {
var toolbarId,
toolbarObj = 0,
toolbarName,
row = toolbar[ r ],
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. (#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 #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 [classic](#!/guide/dev_framed)
* (`iframe`-based) editor. In case of [inline](#!/guide/dev_inline) editor the toolbar
* position is set dynamically depending on the position of the editable element on the screen.
*
* 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.
*
* // 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
* @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](http://ckeditor.com/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-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Undo/Redo system for saving a shapshot for document modification
* and other recordable changes.
*/
'use strict';
( function() {
var keystrokes = [
CKEDITOR.CTRL + 90 /*Z*/,
CKEDITOR.CTRL + 89 /*Y*/,
CKEDITOR.CTRL + CKEDITOR.SHIFT + 90 /*Z*/
],
backspaceOrDelete = { 8: 1, 46: 1 };
CKEDITOR.plugins.add( 'undo', {
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
var undoManager = editor.undoManager = new UndoManager( editor ),
editingHandler = undoManager.editingHandler = new NativeEditingHandler( undoManager );
var undoCommand = editor.addCommand( 'undo', {
exec: function() {
if ( undoManager.undo() ) {
editor.selectionChange();
this.fire( 'afterUndo' );
}
},
startDisabled: true,
canUndo: false
} );
var redoCommand = editor.addCommand( 'redo', {
exec: function() {
if ( undoManager.redo() ) {
editor.selectionChange();
this.fire( 'afterRedo' );
}
},
startDisabled: true,
canUndo: false
} );
editor.setKeystroke( [
[ keystrokes[ 0 ], 'undo' ],
[ keystrokes[ 1 ], 'redo' ],
[ keystrokes[ 2 ], 'redo' ]
] );
undoManager.onChange = function() {
undoCommand.setState( undoManager.undoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
redoCommand.setState( undoManager.redoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
};
function recordCommand( event ) {
// If the command hasn't been marked to not support undo.
if ( undoManager.enabled && event.data.command.canUndo !== false )
undoManager.save();
}
// We'll save snapshots before and after executing a command.
editor.on( 'beforeCommandExec', recordCommand );
editor.on( 'afterCommandExec', recordCommand );
// Save snapshots before doing custom changes.
editor.on( 'saveSnapshot', function( evt ) {
undoManager.save( evt.data && evt.data.contentOnly );
} );
// Event manager listeners should be attached on contentDom.
editor.on( 'contentDom', editingHandler.attachListeners, editingHandler );
editor.on( 'instanceReady', function() {
// Saves initial snapshot.
editor.fire( 'saveSnapshot' );
} );
// Always save an undo snapshot - the previous mode might have
// changed editor contents.
editor.on( 'beforeModeUnload', function() {
editor.mode == 'wysiwyg' && undoManager.save( true );
} );
function toggleUndoManager() {
undoManager.enabled = editor.readOnly ? false : editor.mode == 'wysiwyg';
undoManager.onChange();
}
// Make the undo manager available only in wysiwyg mode.
editor.on( 'mode', toggleUndoManager );
// Disable undo manager when in read-only mode.
editor.on( 'readOnly', toggleUndoManager );
if ( editor.ui.addButton ) {
editor.ui.addButton( 'Undo', {
label: editor.lang.undo.undo,
command: 'undo',
toolbar: 'undo,10'
} );
editor.ui.addButton( 'Redo', {
label: editor.lang.undo.redo,
command: 'redo',
toolbar: 'undo,20'
} );
}
/**
* Resets the undo stack.
*
* @member CKEDITOR.editor
*/
editor.resetUndo = function() {
// Reset the undo stack.
undoManager.reset();
// Create the first image.
editor.fire( 'saveSnapshot' );
};
/**
* Amends the top of the undo stack (last undo image) with the current DOM changes.
*
* function() {
* editor.fire( 'saveSnapshot' );
* editor.document.body.append(...);
* // Makes new changes following the last undo snapshot a part of it.
* editor.fire( 'updateSnapshot' );
* ..
* }
*
* @event updateSnapshot
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
*/
editor.on( 'updateSnapshot', function() {
if ( undoManager.currentImage )
undoManager.update();
} );
/**
* Locks the undo manager to prevent any save/update operations.
*
* It is convenient to lock the undo manager before performing DOM operations
* that should not be recored (e.g. auto paragraphing).
*
* See {@link CKEDITOR.plugins.undo.UndoManager#lock} for more details.
*
* **Note:** In order to unlock the undo manager, {@link #unlockSnapshot} has to be fired
* the same number of times that `lockSnapshot` has been fired.
*
* @since 4.0
* @event lockSnapshot
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {Boolean} [data.dontUpdate] When set to `true`, the last snapshot will not be updated
* with the current content and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.
* @param {Boolean} [data.forceUpdate] When set to `true`, the last snapshot will always be updated
* with the current content and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.
*/
editor.on( 'lockSnapshot', function( evt ) {
var data = evt.data;
undoManager.lock( data && data.dontUpdate, data && data.forceUpdate );
} );
/**
* Unlocks the undo manager and updates the latest snapshot.
*
* @since 4.0
* @event unlockSnapshot
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
*/
editor.on( 'unlockSnapshot', undoManager.unlock, undoManager );
}
} );
CKEDITOR.plugins.undo = {};
/**
* Main logic for the Redo/Undo feature.
*
* @private
* @class CKEDITOR.plugins.undo.UndoManager
* @constructor Creates an UndoManager class instance.
* @param {CKEDITOR.editor} editor
*/
var UndoManager = CKEDITOR.plugins.undo.UndoManager = function( editor ) {
/**
* An array storing the number of key presses, count in a row. Use {@link #keyGroups} members as index.
*
* **Note:** The keystroke count will be reset after reaching the limit of characters per snapshot.
*
* @since 4.4.4
*/
this.strokesRecorded = [ 0, 0 ];
/**
* When the `locked` property is not `null`, the undo manager is locked, so
* operations like `save` or `update` are forbidden.
*
* The manager can be locked and unlocked by the {@link #lock} and {@link #unlock}
* methods, respectively.
*
* @readonly
* @property {Object} [locked=null]
*/
this.locked = null;
/**
* Contains the previously processed key group, based on {@link #keyGroups}.
* `-1` means an unknown group.
*
* @since 4.4.4
* @readonly
* @property {Number} [previousKeyGroup=-1]
*/
this.previousKeyGroup = -1;
/**
* The maximum number of snapshots in the stack. Configurable via {@link CKEDITOR.config#undoStackSize}.
*
* @readonly
* @property {Number} [limit]
*/
this.limit = editor.config.undoStackSize || 20;
/**
* The maximum number of characters typed/deleted in one undo step.
*
* @since 4.4.5
* @readonly
*/
this.strokesLimit = 25;
this.editor = editor;
// Reset the undo stack.
this.reset();
};
UndoManager.prototype = {
/**
* Handles keystroke support for the undo manager. It is called on `keyup` event for
* keystrokes that can change the editor content.
*
* @param {Number} keyCode The key code.
* @param {Boolean} [strokesPerSnapshotExceeded] When set to `true`, the method will
* behave as if the strokes limit was exceeded regardless of the {@link #strokesRecorded} value.
*/
type: function( keyCode, strokesPerSnapshotExceeded ) {
var keyGroup = UndoManager.getKeyGroup( keyCode ),
// Count of keystrokes in current a row.
// Note if strokesPerSnapshotExceeded will be exceeded, it'll be restarted.
strokesRecorded = this.strokesRecorded[ keyGroup ] + 1;
strokesPerSnapshotExceeded =
( strokesPerSnapshotExceeded || strokesRecorded >= this.strokesLimit );
if ( !this.typing )
onTypingStart( this );
if ( strokesPerSnapshotExceeded ) {
// Reset the count of strokes, so it'll be later assigned to this.strokesRecorded.
strokesRecorded = 0;
this.editor.fire( 'saveSnapshot' );
} else {
// Fire change event.
this.editor.fire( 'change' );
}
// Store recorded strokes count.
this.strokesRecorded[ keyGroup ] = strokesRecorded;
// This prop will tell in next itaration what kind of group was processed previously.
this.previousKeyGroup = keyGroup;
},
/**
* Whether the new `keyCode` belongs to a different group than the previous one ({@link #previousKeyGroup}).
*
* @since 4.4.5
* @param {Number} keyCode
* @returns {Boolean}
*/
keyGroupChanged: function( keyCode ) {
return UndoManager.getKeyGroup( keyCode ) != this.previousKeyGroup;
},
/**
* Resets the undo stack.
*/
reset: function() {
// Stack for all the undo and redo snapshots, they're always created/removed
// in consistency.
this.snapshots = [];
// Current snapshot history index.
this.index = -1;
this.currentImage = null;
this.hasUndo = false;
this.hasRedo = false;
this.locked = null;
this.resetType();
},
/**
* Resets all typing variables.
*
* @see #type
*/
resetType: function() {
this.strokesRecorded = [ 0, 0 ];
this.typing = false;
this.previousKeyGroup = -1;
},
/**
* Refreshes the state of the {@link CKEDITOR.plugins.undo.UndoManager undo manager}
* as well as the state of the `undo` and `redo` commands.
*/
refreshState: function() {
// These lines can be handled within onChange() too.
this.hasUndo = !!this.getNextImage( true );
this.hasRedo = !!this.getNextImage( false );
// Reset typing
this.resetType();
this.onChange();
},
/**
* Saves a snapshot of the document image for later retrieval.
*
* @param {Boolean} onContentOnly If set to `true`, the snapshot will be saved only if the content has changed.
* @param {CKEDITOR.plugins.undo.Image} image An optional image to save. If skipped, current editor will be used.
* @param {Boolean} [autoFireChange=true] If set to `false`, will not trigger the {@link CKEDITOR.editor#change} event to editor.
*/
save: function( onContentOnly, image, autoFireChange ) {
var editor = this.editor;
// Do not change snapshots stack when locked, editor is not ready,
// editable is not ready or when editor is in mode difference than 'wysiwyg'.
if ( this.locked || editor.status != 'ready' || editor.mode != 'wysiwyg' )
return false;
var editable = editor.editable();
if ( !editable || editable.status != 'ready' )
return false;
var snapshots = this.snapshots;
// Get a content image.
if ( !image )
image = new Image( editor );
// Do nothing if it was not possible to retrieve an image.
if ( image.contents === false )
return false;
// Check if this is a duplicate. In such case, do nothing.
if ( this.currentImage ) {
if ( image.equalsContent( this.currentImage ) ) {
if ( onContentOnly )
return false;
if ( image.equalsSelection( this.currentImage ) )
return false;
} else if ( autoFireChange !== false ) {
editor.fire( 'change' );
}
}
// Drop future snapshots.
snapshots.splice( this.index + 1, snapshots.length - this.index - 1 );
// If we have reached the limit, remove the oldest one.
if ( snapshots.length == this.limit )
snapshots.shift();
// Add the new image, updating the current index.
this.index = snapshots.push( image ) - 1;
this.currentImage = image;
if ( autoFireChange !== false )
this.refreshState();
return true;
},
/**
* Sets editor content/selection to the one stored in `image`.
*
* @param {CKEDITOR.plugins.undo.Image} image
*/
restoreImage: function( image ) {
// Bring editor focused to restore selection.
var editor = this.editor,
sel;
if ( image.bookmarks ) {
editor.focus();
// Retrieve the selection beforehand. (#8324)
sel = editor.getSelection();
}
// Start transaction - do not allow any mutations to the
// snapshots stack done when selecting bookmarks (much probably
// by selectionChange listener).
this.locked = { level: 999 };
this.editor.loadSnapshot( image.contents );
if ( image.bookmarks )
sel.selectBookmarks( image.bookmarks );
else if ( CKEDITOR.env.ie ) {
// IE BUG: If I don't set the selection to *somewhere* after setting
// document contents, then IE would create an empty paragraph at the bottom
// the next time the document is modified.
var $range = this.editor.document.getBody().$.createTextRange();
$range.collapse( true );
$range.select();
}
this.locked = null;
this.index = image.index;
this.currentImage = this.snapshots[ this.index ];
// Update current image with the actual editor
// content, since actualy content may differ from
// the original snapshot due to dom change. (#4622)
this.update();
this.refreshState();
editor.fire( 'change' );
},
/**
* Gets the closest available image.
*
* @param {Boolean} isUndo If `true`, it will return the previous image.
* @returns {CKEDITOR.plugins.undo.Image} Next image or `null`.
*/
getNextImage: function( isUndo ) {
var snapshots = this.snapshots,
currentImage = this.currentImage,
image, i;
if ( currentImage ) {
if ( isUndo ) {
for ( i = this.index - 1; i >= 0; i-- ) {
image = snapshots[ i ];
if ( !currentImage.equalsContent( image ) ) {
image.index = i;
return image;
}
}
} else {
for ( i = this.index + 1; i < snapshots.length; i++ ) {
image = snapshots[ i ];
if ( !currentImage.equalsContent( image ) ) {
image.index = i;
return image;
}
}
}
}
return null;
},
/**
* Checks the current redo state.
*
* @returns {Boolean} Whether the document has a previous state to retrieve.
*/
redoable: function() {
return this.enabled && this.hasRedo;
},
/**
* Checks the current undo state.
*
* @returns {Boolean} Whether the document has a future state to restore.
*/
undoable: function() {
return this.enabled && this.hasUndo;
},
/**
* Performs an undo operation on current index.
*/
undo: function() {
if ( this.undoable() ) {
this.save( true );
var image = this.getNextImage( true );
if ( image )
return this.restoreImage( image ), true;
}
return false;
},
/**
* Performs a redo operation on current index.
*/
redo: function() {
if ( this.redoable() ) {
// Try to save. If no changes have been made, the redo stack
// will not change, so it will still be redoable.
this.save( true );
// If instead we had changes, we can't redo anymore.
if ( this.redoable() ) {
var image = this.getNextImage( false );
if ( image )
return this.restoreImage( image ), true;
}
}
return false;
},
/**
* Updates the last snapshot of the undo stack with the current editor content.
*
* @param {CKEDITOR.plugins.undo.Image} [newImage] The image which will replace the current one.
* If it is not set, it defaults to the image taken from the editor.
*/
update: function( newImage ) {
// Do not change snapshots stack is locked.
if ( this.locked )
return;
if ( !newImage )
newImage = new Image( this.editor );
var i = this.index,
snapshots = this.snapshots;
// Find all previous snapshots made for the same content (which differ
// only by selection) and replace all of them with the current image.
while ( i > 0 && this.currentImage.equalsContent( snapshots[ i - 1 ] ) )
i -= 1;
snapshots.splice( i, this.index - i + 1, newImage );
this.index = i;
this.currentImage = newImage;
},
/**
* Amends the last snapshot and changes its selection (only in case when content
* is equal between these two).
*
* @since 4.4.4
* @param {CKEDITOR.plugins.undo.Image} newSnapshot New snapshot with new selection.
* @returns {Boolean} Returns `true` if selection was amended.
*/
updateSelection: function( newSnapshot ) {
if ( !this.snapshots.length )
return false;
var snapshots = this.snapshots,
lastImage = snapshots[ snapshots.length - 1 ];
if ( lastImage.equalsContent( newSnapshot ) ) {
if ( !lastImage.equalsSelection( newSnapshot ) ) {
snapshots[ snapshots.length - 1 ] = newSnapshot;
this.currentImage = newSnapshot;
return true;
}
}
return false;
},
/**
* Locks the snapshot stack to prevent any save/update operations and when necessary,
* updates the tip of the snapshot stack with the DOM changes introduced during the
* locked period, after the {@link #unlock} method is called.
*
* It is mainly used to ensure any DOM operations that should not be recorded
* (e.g. auto paragraphing) are not added to the stack.
*
* **Note:** For every `lock` call you must call {@link #unlock} once to unlock the undo manager.
*
* @since 4.0
* @param {Boolean} [dontUpdate] When set to `true`, the last snapshot will not be updated
* with current content and selection. By default, if undo manager was up to date when the lock started,
* the last snapshot will be updated to the current state when unlocking. This means that all changes
* done during the lock will be merged into the previous snapshot or the next one. Use this option to gain
* more control over this behavior. For example, it is possible to group changes done during the lock into
* a separate snapshot.
* @param {Boolean} [forceUpdate] When set to `true`, the last snapshot will always be updated with the
* current content and selection regardless of the current state of the undo manager.
* When not set, the last snapshot will be updated only if the undo manager was up to date when locking.
* Additionally, this option makes it possible to lock the snapshot when the editor is not in the `wysiwyg` mode,
* because when it is passed, the snapshots will not need to be compared.
*/
lock: function( dontUpdate, forceUpdate ) {
if ( !this.locked ) {
if ( dontUpdate )
this.locked = { level: 1 };
else {
var update = null;
if ( forceUpdate )
update = true;
else {
// Make a contents image. Don't include bookmarks, because:
// * we don't compare them,
// * there's a chance that DOM has been changed since
// locked (e.g. fake) selection was made, so createBookmark2 could fail.
// http://dev.ckeditor.com/ticket/11027#comment:3
var imageBefore = new Image( this.editor, true );
// If current editor content matches the tip of snapshot stack,
// the stack tip must be updated by unlock, to include any changes made
// during this period.
if ( this.currentImage && this.currentImage.equalsContent( imageBefore ) )
update = imageBefore;
}
this.locked = { update: update, level: 1 };
}
// Increase the level of lock.
} else {
this.locked.level++;
}
},
/**
* Unlocks the snapshot stack and checks to amend the last snapshot.
*
* See {@link #lock} for more details.
*
* @since 4.0
*/
unlock: function() {
if ( this.locked ) {
// Decrease level of lock and check if equals 0, what means that undoM is completely unlocked.
if ( !--this.locked.level ) {
var update = this.locked.update;
this.locked = null;
// forceUpdate was passed to lock().
if ( update === true )
this.update();
// update is instance of Image.
else if ( update ) {
var newImage = new Image( this.editor, true );
if ( !update.equalsContent( newImage ) )
this.update();
}
}
}
}
};
/**
* Codes for navigation keys like *Arrows*, *Page Up/Down*, etc.
* Used by the {@link #isNavigationKey} method.
*
* @since 4.4.5
* @readonly
* @static
*/
UndoManager.navigationKeyCodes = {
37: 1, 38: 1, 39: 1, 40: 1, // Arrows.
36: 1, 35: 1, // Home, End.
33: 1, 34: 1 // PgUp, PgDn.
};
/**
* Key groups identifier mapping. Used for accessing members in
* {@link #strokesRecorded}.
*
* * `FUNCTIONAL` – identifier for the *Backspace* / *Delete* key.
* * `PRINTABLE` – identifier for printable keys.
*
* Example usage:
*
* undoManager.strokesRecorded[ undoManager.keyGroups.FUNCTIONAL ];
*
* @since 4.4.5
* @readonly
* @static
*/
UndoManager.keyGroups = {
PRINTABLE: 0,
FUNCTIONAL: 1
};
/**
* Checks whether a key is one of navigation keys (*Arrows*, *Page Up/Down*, etc.).
* See also the {@link #navigationKeyCodes} property.
*
* @since 4.4.5
* @static
* @param {Number} keyCode
* @returns {Boolean}
*/
UndoManager.isNavigationKey = function( keyCode ) {
return !!UndoManager.navigationKeyCodes[ keyCode ];
};
/**
* Returns the group to which the passed `keyCode` belongs.
*
* @since 4.4.5
* @static
* @param {Number} keyCode
* @returns {Number}
*/
UndoManager.getKeyGroup = function( keyCode ) {
var keyGroups = UndoManager.keyGroups;
return backspaceOrDelete[ keyCode ] ? keyGroups.FUNCTIONAL : keyGroups.PRINTABLE;
};
/**
* @since 4.4.5
* @static
* @param {Number} keyGroup
* @returns {Number}
*/
UndoManager.getOppositeKeyGroup = function( keyGroup ) {
var keyGroups = UndoManager.keyGroups;
return ( keyGroup == keyGroups.FUNCTIONAL ? keyGroups.PRINTABLE : keyGroups.FUNCTIONAL );
};
/**
* Whether we need to use a workaround for functional (*Backspace*, *Delete*) keys not firing
* the `keypress` event in Internet Explorer in this environment and for the specified `keyCode`.
*
* @since 4.4.5
* @static
* @param {Number} keyCode
* @returns {Boolean}
*/
UndoManager.ieFunctionalKeysBug = function( keyCode ) {
return CKEDITOR.env.ie && UndoManager.getKeyGroup( keyCode ) == UndoManager.keyGroups.FUNCTIONAL;
};
// Helper method called when undoManager.typing val was changed to true.
function onTypingStart( undoManager ) {
// It's safe to now indicate typing state.
undoManager.typing = true;
// Manually mark snapshot as available.
undoManager.hasUndo = true;
undoManager.hasRedo = false;
undoManager.onChange();
}
/**
* Contains a snapshot of the editor content and selection at a given point in time.
*
* @private
* @class CKEDITOR.plugins.undo.Image
* @constructor Creates an Image class instance.
* @param {CKEDITOR.editor} editor The editor instance on which the image is created.
* @param {Boolean} [contentsOnly] If set to `true`, the image will only contain content without the selection.
*/
var Image = CKEDITOR.plugins.undo.Image = function( editor, contentsOnly ) {
this.editor = editor;
editor.fire( 'beforeUndoImage' );
var contents = editor.getSnapshot();
// In IE, we need to remove the expando attributes.
if ( CKEDITOR.env.ie && contents )
contents = contents.replace( /\s+data-cke-expando=".*?"/g, '' );
this.contents = contents;
if ( !contentsOnly ) {
var selection = contents && editor.getSelection();
this.bookmarks = selection && selection.createBookmarks2( true );
}
editor.fire( 'afterUndoImage' );
};
// Attributes that browser may changing them when setting via innerHTML.
var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi;
Image.prototype = {
/**
* @param {CKEDITOR.plugins.undo.Image} otherImage Image to compare to.
* @returns {Boolean} Returns `true` if content in `otherImage` is the same.
*/
equalsContent: function( otherImage ) {
var thisContents = this.contents,
otherContents = otherImage.contents;
// For IE7 and IE QM: Comparing only the protected attribute values but not the original ones.(#4522)
if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
thisContents = thisContents.replace( protectedAttrs, '' );
otherContents = otherContents.replace( protectedAttrs, '' );
}
if ( thisContents != otherContents )
return false;
return true;
},
/**
* @param {CKEDITOR.plugins.undo.Image} otherImage Image to compare to.
* @returns {Boolean} Returns `true` if selection in `otherImage` is the same.
*/
equalsSelection: function( otherImage ) {
var bookmarksA = this.bookmarks,
bookmarksB = otherImage.bookmarks;
if ( bookmarksA || bookmarksB ) {
if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length )
return false;
for ( var i = 0; i < bookmarksA.length; i++ ) {
var bookmarkA = bookmarksA[ i ],
bookmarkB = bookmarksB[ i ];
if ( bookmarkA.startOffset != bookmarkB.startOffset || bookmarkA.endOffset != bookmarkB.endOffset ||
!CKEDITOR.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) ||
!CKEDITOR.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) ) {
return false;
}
}
}
return true;
}
/**
* Editor content.
*
* @readonly
* @property {String} contents
*/
/**
* Bookmarks representing the selection in an image.
*
* @readonly
* @property {Object[]} bookmarks Array of bookmark2 objects, see {@link CKEDITOR.dom.range#createBookmark2} for definition.
*/
};
/**
* A class encapsulating all native event listeners which have to be used in
* order to handle undo manager integration for native editing actions (excluding drag and drop and paste support
* handled by the Clipboard plugin).
*
* @since 4.4.4
* @private
* @class CKEDITOR.plugins.undo.NativeEditingHandler
* @member CKEDITOR.plugins.undo Undo manager owning the handler.
* @constructor
* @param {CKEDITOR.plugins.undo.UndoManager} undoManager
*/
var NativeEditingHandler = CKEDITOR.plugins.undo.NativeEditingHandler = function( undoManager ) {
// We'll use keyboard + input events to determine if snapshot should be created.
// Since `input` event is fired before `keyup`. We can tell in `keyup` event if input occured.
// That will tell us if any printable data was inserted.
// On `input` event we'll increase input fired counter for proper key code.
// Eventually it might be canceled by paste/drop using `ignoreInputEvent` flag.
// Order of events can be found in http://www.w3.org/TR/DOM-Level-3-Events/
/**
* An undo manager instance owning the editing handler.
*
* @property {CKEDITOR.plugins.undo.UndoManager} undoManager
*/
this.undoManager = undoManager;
/**
* See {@link #ignoreInputEventListener}.
*
* @since 4.4.5
* @private
*/
this.ignoreInputEvent = false;
/**
* A stack of pressed keys.
*
* @since 4.4.5
* @property {CKEDITOR.plugins.undo.KeyEventsStack} keyEventsStack
*/
this.keyEventsStack = new KeyEventsStack();
/**
* An image of the editor during the `keydown` event (therefore without DOM modification).
*
* @property {CKEDITOR.plugins.undo.Image} lastKeydownImage
*/
this.lastKeydownImage = null;
};
NativeEditingHandler.prototype = {
/**
* The `keydown` event listener.
*
* @param {CKEDITOR.dom.event} evt
*/
onKeydown: function( evt ) {
var keyCode = evt.data.getKey();
// The composition is in progress - ignore the key. (#12597)
if ( keyCode === 229 ) {
return;
}
// Block undo/redo keystrokes when at the bottom/top of the undo stack (#11126 and #11677).
if ( CKEDITOR.tools.indexOf( keystrokes, evt.data.getKeystroke() ) > -1 ) {
evt.data.preventDefault();
return;
}
// Cleaning tab functional keys.
this.keyEventsStack.cleanUp( evt );
var undoManager = this.undoManager;
// Gets last record for provided keyCode. If not found will create one.
var last = this.keyEventsStack.getLast( keyCode );
if ( !last ) {
this.keyEventsStack.push( keyCode );
}
// We need to store an image which will be used in case of key group
// change.
this.lastKeydownImage = new Image( undoManager.editor );
if ( UndoManager.isNavigationKey( keyCode ) || this.undoManager.keyGroupChanged( keyCode ) ) {
if ( undoManager.strokesRecorded[ 0 ] || undoManager.strokesRecorded[ 1 ] ) {
// We already have image, so we'd like to reuse it.
// #12300
undoManager.save( false, this.lastKeydownImage, false );
undoManager.resetType();
}
}
},
/**
* The `input` event listener.
*/
onInput: function() {
// Input event is ignored if paste/drop event were fired before.
if ( this.ignoreInputEvent ) {
// Reset flag - ignore only once.
this.ignoreInputEvent = false;
return;
}
var lastInput = this.keyEventsStack.getLast();
// Nothing in key events stack, but input event called. Interesting...
// That's because on Android order of events is buggy and also keyCode is set to 0.
if ( !lastInput ) {
lastInput = this.keyEventsStack.push( 0 );
}
// Increment inputs counter for provided key code.
this.keyEventsStack.increment( lastInput.keyCode );
// Exceeded limit.
if ( this.keyEventsStack.getTotalInputs() >= this.undoManager.strokesLimit ) {
this.undoManager.type( lastInput.keyCode, true );
this.keyEventsStack.resetInputs();
}
},
/**
* The `keyup` event listener.
*
* @param {CKEDITOR.dom.event} evt
*/
onKeyup: function( evt ) {
var undoManager = this.undoManager,
keyCode = evt.data.getKey(),
totalInputs = this.keyEventsStack.getTotalInputs();
// Remove record from stack for provided key code.
this.keyEventsStack.remove( keyCode );
// Second part of the workaround for IEs functional keys bug. We need to check whether something has really
// changed because we blindly mocked the keypress event.
// Also we need to be aware that lastKeydownImage might not be available (#12327).
if ( UndoManager.ieFunctionalKeysBug( keyCode ) && this.lastKeydownImage &&
this.lastKeydownImage.equalsContent( new Image( undoManager.editor, true ) ) ) {
return;
}
if ( totalInputs > 0 ) {
undoManager.type( keyCode );
} else if ( UndoManager.isNavigationKey( keyCode ) ) {
// Note content snapshot has been checked in keydown.
this.onNavigationKey( true );
}
},
/**
* Method called for navigation change. At first it will check if current content does not differ
* from the last saved snapshot.
*
* * If the content is different, the method creates a standard, extra snapshot.
* * If the content is not different, the method will compare the selection, and will
* amend the last snapshot selection if it changed.
*
* @param {Boolean} skipContentCompare If set to `true`, it will not compare content, and only do a selection check.
*/
onNavigationKey: function( skipContentCompare ) {
var undoManager = this.undoManager;
// We attempt to save content snapshot, if content didn't change, we'll
// only amend selection.
if ( skipContentCompare || !undoManager.save( true, null, false ) )
undoManager.updateSelection( new Image( undoManager.editor ) );
undoManager.resetType();
},
/**
* Makes the next `input` event to be ignored.
*/
ignoreInputEventListener: function() {
this.ignoreInputEvent = true;
},
/**
* Attaches editable listeners required to provide the undo functionality.
*/
attachListeners: function() {
var editor = this.undoManager.editor,
editable = editor.editable(),
that = this;
// We'll create a snapshot here (before DOM modification), because we'll
// need unmodified content when we got keygroup toggled in keyup.
editable.attachListener( editable, 'keydown', function( evt ) {
that.onKeydown( evt );
// On IE keypress isn't fired for functional (backspace/delete) keys.
// Let's pretend that something's changed.
if ( UndoManager.ieFunctionalKeysBug( evt.data.getKey() ) ) {
that.onInput();
}
}, null, null, 999 );
// Only IE can't use input event, because it's not fired in contenteditable.
editable.attachListener( editable, ( CKEDITOR.env.ie ? 'keypress' : 'input' ), that.onInput, that, null, 999 );
// Keyup executes main snapshot logic.
editable.attachListener( editable, 'keyup', that.onKeyup, that, null, 999 );
// On paste and drop we need to ignore input event.
// It would result with calling undoManager.type() on any following key.
editable.attachListener( editable, 'paste', that.ignoreInputEventListener, that, null, 999 );
editable.attachListener( editable, 'drop', that.ignoreInputEventListener, that, null, 999 );
// Click should create a snapshot if needed, but shouldn't cause change event.
// Don't pass onNavigationKey directly as a listener because it accepts one argument which
// will conflict with evt passed to listener.
// #12324 comment:4
editable.attachListener( editable.isInline() ? editable : editor.document.getDocumentElement(), 'click', function() {
that.onNavigationKey();
}, null, null, 999 );
// When pressing `Tab` key while editable is focused, `keyup` event is not fired.
// Which means that record for `tab` key stays in key events stack.
// We assume that when editor is blurred `tab` key is already up.
editable.attachListener( this.undoManager.editor, 'blur', function() {
that.keyEventsStack.remove( 9 /*Tab*/ );
}, null, null, 999 );
}
};
/**
* This class represents a stack of pressed keys and stores information
* about how many `input` events each key press has caused.
*
* @since 4.4.5
* @private
* @class CKEDITOR.plugins.undo.KeyEventsStack
* @constructor
*/
var KeyEventsStack = CKEDITOR.plugins.undo.KeyEventsStack = function() {
/**
* @readonly
*/
this.stack = [];
};
KeyEventsStack.prototype = {
/**
* Pushes a literal object with two keys: `keyCode` and `inputs` (whose initial value is set to `0`) to stack.
* It is intended to be called on the `keydown` event.
*
* @param {Number} keyCode
*/
push: function( keyCode ) {
var length = this.stack.push( { keyCode: keyCode, inputs: 0 } );
return this.stack[ length - 1 ];
},
/**
* Returns the index of the last registered `keyCode` in the stack.
* If no `keyCode` is provided, then the function will return the index of the last item.
* If an item is not found, it will return `-1`.
*
* @param {Number} [keyCode]
* @returns {Number}
*/
getLastIndex: function( keyCode ) {
if ( typeof keyCode != 'number' ) {
return this.stack.length - 1; // Last index or -1.
} else {
var i = this.stack.length;
while ( i-- ) {
if ( this.stack[ i ].keyCode == keyCode ) {
return i;
}
}
return -1;
}
},
/**
* Returns the last key recorded in the stack. If `keyCode` is provided, then it will return
* the last record for this `keyCode`.
*
* @param {Number} [keyCode]
* @returns {Object} Last matching record or `null`.
*/
getLast: function( keyCode ) {
var index = this.getLastIndex( keyCode );
if ( index != -1 ) {
return this.stack[ index ];
} else {
return null;
}
},
/**
* Increments registered input events for stack record for a given `keyCode`.
*
* @param {Number} keyCode
*/
increment: function( keyCode ) {
var found = this.getLast( keyCode );
found.inputs++;
},
/**
* Removes the last record from the stack for the provided `keyCode`.
*
* @param {Number} keyCode
*/
remove: function( keyCode ) {
var index = this.getLastIndex( keyCode );
if ( index != -1 ) {
this.stack.splice( index, 1 );
}
},
/**
* Resets the `inputs` value to `0` for a given `keyCode` or in entire stack if a
* `keyCode` is not specified.
*
* @param {Number} [keyCode]
*/
resetInputs: function( keyCode ) {
if ( typeof keyCode == 'number' ) {
var last = this.getLast( keyCode );
last.inputs = 0;
} else {
var i = this.stack.length;
while ( i-- ) {
this.stack[ i ].inputs = 0;
}
}
},
/**
* Sums up inputs number for each key code and returns it.
*
* @returns {Number}
*/
getTotalInputs: function() {
var i = this.stack.length,
total = 0;
while ( i-- ) {
total += this.stack[ i ].inputs;
}
return total;
},
/**
* Cleans the stack based on a provided `keydown` event object. The rationale behind this method
* is that some keystrokes cause the `keydown` event to be fired in the editor, but not the `keyup` event.
* For instance, *Alt+Tab* will fire `keydown`, but since the editor is blurred by it, then there is
* no `keyup`, so the keystroke is not removed from the stack.
*
* @param {CKEDITOR.dom.event} event
*/
cleanUp: function( event ) {
var nativeEvent = event.data.$;
if ( !( nativeEvent.ctrlKey || nativeEvent.metaKey ) ) {
this.remove( 17 );
}
if ( !nativeEvent.shiftKey ) {
this.remove( 16 );
}
if ( !nativeEvent.altKey ) {
this.remove( 18 );
}
}
};
} )();
/**
* The number of undo steps to be saved. The higher value is set, the more
* memory is used for it.
*
* config.undoStackSize = 50;
*
* @cfg {Number} [undoStackSize=20]
* @member CKEDITOR.config
*/
/**
* Fired when the editor is about to save an undo snapshot. This event can be
* fired by plugins and customizations to make the editor save undo snapshots.
*
* @event saveSnapshot
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
*/
/**
* Fired before an undo image is to be created. An *undo image* represents the
* editor state at some point. It is saved into the undo store, so the editor is
* able to recover the editor state on undo and redo operations.
*
* @since 3.5.3
* @event beforeUndoImage
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @see CKEDITOR.editor#afterUndoImage
*/
/**
* Fired after an undo image is created. An *undo image* represents the
* editor state at some point. It is saved into the undo store, so the editor is
* able to recover the editor state on undo and redo operations.
*
* @since 3.5.3
* @event afterUndoImage
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @see CKEDITOR.editor#beforeUndoImage
*/
/**
* Fired when the content of the editor is changed.
*
* Due to performance reasons, it is not verified if the content really changed.
* The editor instead watches several editing actions that usually result in
* changes. This event may thus in some cases be fired when no changes happen
* or may even get fired twice.
*
* If it is important not to get the `change` event fired too often, you should compare the
* previous and the current editor content inside the event listener. It is
* not recommended to do that on every `change` event.
*
* Please note that the `change` event is only fired in the {@link #property-mode wysiwyg mode}.
* In order to implement similar functionality in the source mode, you can listen for example to the {@link #key}
* event or the native [`input`](https://developer.mozilla.org/en-US/docs/Web/Reference/Events/input)
* event (not supported by Internet Explorer 8).
*
* editor.on( 'mode', function() {
* if ( this.mode == 'source' ) {
* var editable = editor.editable();
* editable.attachListener( editable, 'input', function() {
* // Handle changes made in the source mode.
* } );
* }
* } );
*
* @since 4.2
* @event change
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
*/
/**
* Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview The WYSIWYG Area plugin. It registers the "wysiwyg" editing
* mode, which handles the main editing area space.
*/
( function() {
CKEDITOR.plugins.add( 'wysiwygarea', {
init: function( editor ) {
if ( editor.config.fullPage ) {
editor.addFeature( {
allowedContent: 'html head title; style [media,type]; body (*)[id]; meta link [*]',
requiredContent: 'body'
} );
}
editor.addMode( 'wysiwyg', function( callback ) {
var src = 'document.open();' +
// In IE, the document domain must be set any time we call document.open().
( CKEDITOR.env.ie ? '(' + CKEDITOR.tools.fixDomain + ')();' : '' ) +
'document.close();';
// 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.
src = CKEDITOR.env.air ? 'javascript:void(0)' : CKEDITOR.env.ie ? 'javascript:void(function(){' + encodeURIComponent( src ) + '}())' // jshint ignore:line
:
'';
var iframe = CKEDITOR.dom.element.createFromHtml( ' ' );
iframe.setStyles( { width: '100%', height: '100%' } );
iframe.addClass( 'cke_wysiwyg_frame' ).addClass( 'cke_reset' );
var contentSpace = editor.ui.space( 'contents' );
contentSpace.append( iframe );
// Asynchronous iframe loading is only required in IE>8 and Gecko (other reasons probably).
// Do not use it on WebKit as it'll break the browser-back navigation.
var useOnloadEvent = CKEDITOR.env.ie || CKEDITOR.env.gecko;
if ( useOnloadEvent )
iframe.on( 'load', onLoad );
var frameLabel = editor.title,
helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
if ( frameLabel ) {
if ( CKEDITOR.env.ie && helpLabel )
frameLabel += ', ' + helpLabel;
iframe.setAttribute( 'title', frameLabel );
}
if ( helpLabel ) {
var labelId = CKEDITOR.tools.getNextId(),
desc = CKEDITOR.dom.element.createFromHtml( '' + helpLabel + ' ' );
contentSpace.append( desc, 1 );
iframe.setAttribute( 'aria-describedby', labelId );
}
// Remove the ARIA description.
editor.on( 'beforeModeUnload', function( evt ) {
evt.removeListener();
if ( desc )
desc.remove();
} );
iframe.setAttributes( {
tabIndex: editor.tabIndex,
allowTransparency: 'true'
} );
// Execute onLoad manually for all non IE||Gecko browsers.
!useOnloadEvent && onLoad();
if ( CKEDITOR.env.webkit ) {
// Webkit: iframe size doesn't auto fit well. (#7360)
var onResize = function() {
// Hide the iframe to get real size of the holder. (#8941)
contentSpace.setStyle( 'width', '100%' );
iframe.hide();
iframe.setSize( 'width', contentSpace.getSize( 'width' ) );
contentSpace.removeStyle( 'width' );
iframe.show();
};
iframe.setCustomData( 'onResize', onResize );
CKEDITOR.document.getWindow().on( 'resize', onResize );
}
editor.fire( 'ariaWidget', iframe );
function onLoad( evt ) {
evt && evt.removeListener();
editor.editable( new framedWysiwyg( editor, iframe.$.contentWindow.document.body ) );
editor.setData( editor.getData( 1 ), callback );
}
} );
}
} );
/**
* Adds the path to a stylesheet file to the exisiting {@link CKEDITOR.config#contentsCss} value.
*
* **Note:** This method is available only with the `wysiwygarea` plugin and only affects
* classic editors based on it (so it does not affect inline editors).
*
* editor.addContentsCss( 'assets/contents.css' );
*
* @since 4.4
* @param {String} cssPath The path to the stylesheet file which should be added.
* @member CKEDITOR.editor
*/
CKEDITOR.editor.prototype.addContentsCss = function( cssPath ) {
var cfg = this.config,
curContentsCss = cfg.contentsCss;
// Convert current value into array.
if ( !CKEDITOR.tools.isArray( curContentsCss ) )
cfg.contentsCss = curContentsCss ? [ curContentsCss ] : [];
cfg.contentsCss.push( cssPath );
};
function onDomReady( win ) {
var editor = this.editor,
doc = win.document,
body = doc.body;
// Remove helper scripts from the DOM.
var script = doc.getElementById( 'cke_actscrpt' );
script && script.parentNode.removeChild( script );
script = doc.getElementById( 'cke_shimscrpt' );
script && script.parentNode.removeChild( script );
script = doc.getElementById( 'cke_basetagscrpt' );
script && script.parentNode.removeChild( script );
body.contentEditable = true;
if ( CKEDITOR.env.ie ) {
// Don't display the focus border.
body.hideFocus = true;
// Disable and re-enable the body to avoid IE from
// taking the editing focus at startup. (#141 / #523)
body.disabled = true;
body.removeAttribute( 'disabled' );
}
delete this._.isLoadingData;
// Play the magic to alter element reference to the reloaded one.
this.$ = body;
doc = new CKEDITOR.dom.document( doc );
this.setup();
this.fixInitialSelection();
if ( CKEDITOR.env.ie ) {
doc.getDocumentElement().addClass( doc.$.compatMode );
// Prevent IE from leaving new paragraph after deleting all contents in body. (#6966)
editor.config.enterMode != CKEDITOR.ENTER_P && this.attachListener( doc, 'selectionchange', function() {
var body = doc.getBody(),
sel = editor.getSelection(),
range = sel && sel.getRanges()[ 0 ];
if ( range && body.getHtml().match( /^(?: | )<\/p>$/i ) && range.startContainer.equals( body ) ) {
// Avoid the ambiguity from a real user cursor position.
setTimeout( function() {
range = editor.getSelection().getRanges()[ 0 ];
if ( !range.startContainer.equals( 'body' ) ) {
body.getFirst().remove( 1 );
range.moveToElementEditEnd( body );
range.select();
}
}, 0 );
}
} );
}
// Fix problem with cursor not appearing in Webkit and IE11+ when clicking below the body (#10945, #10906).
// Fix for older IEs (8-10 and QM) is placed inside selection.js.
if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version > 10 ) ) {
doc.getDocumentElement().on( 'mousedown', function( evt ) {
if ( evt.data.getTarget().is( 'html' ) ) {
// IE needs this timeout. Webkit does not, but it does not cause problems too.
setTimeout( function() {
editor.editable().focus();
} );
}
} );
}
// Config props: disableObjectResizing and disableNativeTableHandles handler.
objectResizeDisabler( editor );
// Enable dragging of position:absolute elements in IE.
try {
editor.document.$.execCommand( '2D-position', false, true );
} catch ( e ) {}
if ( CKEDITOR.env.gecko || CKEDITOR.env.ie && editor.document.$.compatMode == 'CSS1Compat' ) {
this.attachListener( this, 'keydown', function( evt ) {
var keyCode = evt.data.getKeystroke();
// PageUp OR PageDown
if ( keyCode == 33 || keyCode == 34 ) {
// PageUp/PageDown scrolling is broken in document
// with standard doctype, manually fix it. (#4736)
if ( CKEDITOR.env.ie ) {
setTimeout( function() {
editor.getSelection().scrollIntoView();
}, 0 );
}
// Page up/down cause editor selection to leak
// outside of editable thus we try to intercept
// the behavior, while it affects only happen
// when editor contents are not overflowed. (#7955)
else if ( editor.window.$.innerHeight > this.$.offsetHeight ) {
var range = editor.createRange();
range[ keyCode == 33 ? 'moveToElementEditStart' : 'moveToElementEditEnd' ]( this );
range.select();
evt.data.preventDefault();
}
}
} );
}
if ( CKEDITOR.env.ie ) {
// [IE] Iframe will still keep the selection when blurred, if
// focus is moved onto a non-editing host, e.g. link or button, but
// it becomes a problem for the object type selection, since the resizer
// handler attached on it will mark other part of the UI, especially
// for the dialog. (#8157)
// [IE<8 & Opera] Even worse For old IEs, the cursor will not vanish even if
// the selection has been moved to another text input in some cases. (#4716)
//
// Now the range restore is disabled, so we simply force IE to clean
// up the selection before blur.
this.attachListener( doc, 'blur', function() {
// Error proof when the editor is not visible. (#6375)
try {
doc.$.selection.empty();
} catch ( er ) {}
} );
}
if ( CKEDITOR.env.iOS ) {
// [iOS] If touch is bound to any parent of the iframe blur happens on any touch
// event and body becomes the focused element (#10714).
this.attachListener( doc, 'touchend', function() {
win.focus();
} );
}
var title = editor.document.getElementsByTag( 'title' ).getItem( 0 );
// document.title is malfunctioning on Chrome, so get value from the element (#12402).
title.data( 'cke-title', title.getText() );
// [IE] JAWS will not recognize the aria label we used on the iframe
// unless the frame window title string is used as the voice label,
// backup the original one and restore it on output.
if ( CKEDITOR.env.ie )
editor.document.$.title = this._.docTitle;
CKEDITOR.tools.setTimeout( function() {
// Editable is ready after first setData.
if ( this.status == 'unloaded' )
this.status = 'ready';
editor.fire( 'contentDom' );
if ( this._.isPendingFocus ) {
editor.focus();
this._.isPendingFocus = false;
}
setTimeout( function() {
editor.fire( 'dataReady' );
}, 0 );
}, 0, this );
}
var framedWysiwyg = CKEDITOR.tools.createClass( {
$: function() {
this.base.apply( this, arguments );
this._.frameLoadedHandler = CKEDITOR.tools.addFunction( function( win ) {
// Avoid opening design mode in a frame window thread,
// which will cause host page scrolling.(#4397)
CKEDITOR.tools.setTimeout( onDomReady, 0, this, win );
}, this );
this._.docTitle = this.getWindow().getFrame().getAttribute( 'title' );
},
base: CKEDITOR.editable,
proto: {
setData: function( data, isSnapshot ) {
var editor = this.editor;
if ( isSnapshot ) {
this.setHtml( data );
this.fixInitialSelection();
// Fire dataReady for the consistency with inline editors
// and because it makes sense. (#10370)
editor.fire( 'dataReady' );
}
else {
this._.isLoadingData = true;
editor._.dataStore = { id: 1 };
var config = editor.config,
fullPage = config.fullPage,
docType = config.docType;
// Build the additional stuff to be included into
.
var headExtra = CKEDITOR.tools.buildStyleHtml( iframeCssFixes() ).replace( /