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.0
* @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.0
* @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. (https://dev.ckeditor.com/ticket/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.0
* @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 ({@glink features/styles#style-types read more about 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.0
* @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 or comments.
if ( ( nodeName && currentNode.data( 'cke-bookmark' ) ) || currentNode.type === CKEDITOR.NODE_COMMENT ) {
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 completely, but,
// if this is the last node in its parent, we must also
// check if the parent itself can be added completely
// 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. (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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,
alwaysRemoveElement = this._.definition.alwaysRemoveElement;
if ( range.collapsed ) {
var startPath = new CKEDITOR.dom.elementPath( startNode.getParent(), range.root ),
// The topmost element in elements path 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 preserved.(https://dev.ckeditor.com/ticket/3309)
//
// 3. Force removing the element even if it's an boundary element when alwaysRemoveElement is true.
// Without it, the links won't be unlinked if the cursor is placed right before/after it. (https://dev.ckeditor.com/ticket/13062)
if ( element == startPath.block || element == startPath.blockLimit ) {
break;
}
if ( this.checkElementRemovable( element ) ) {
var isStart;
if ( !alwaysRemoveElement && 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. (https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/1318).
if ( ( attName == 'class' || this._.definition.fullMatch ) && element.getAttribute( attName ) != normalizeProperty( attName, attributes[ attName ] ) )
continue;
// Do not touch data-* attributes (https://dev.ckeditor.com/ticket/11011) (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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 );
// https://dev.ckeditor.com/ticket/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 );
el.getDocument().removeCustomData( 'doc_processing_style' );
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
// @returns {Boolean}
function compareCssText( source, target ) {
function filter( string, propertyName ) {
// In case of font-families we'll skip quotes. (https://dev.ckeditor.com/ticket/10750)
return propertyName.toLowerCase() == 'font-family' ? string.replace( /["']/g, '' ) : string;
}
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 ) ) {
return false;
}
if ( !( filter( target[ name ], name ) == filter( source[ name ], name ) ||
source[ name ] == 'inherit' ||
target[ name ] == 'inherit' ) ) {
return false;
}
}
return true;
}
function applyStyleOnSelection( selection, remove, editor ) {
var ranges = selection.getRanges(),
func = remove ? this.removeFromRange : this.applyToRange,
originalRanges,
range,
i;
// In case of fake table selection, we would like to apply all styles and then select
// the original ranges. Otherwise browsers would complain about discontiguous selection.
if ( selection.isFake && selection.isInTable() ) {
originalRanges = [];
for ( i = 0; i < ranges.length; i++ ) {
originalRanges.push( ranges[ i ].clone() );
}
}
var iterator = ranges.createIterator();
while ( ( range = iterator.getNextRange() ) )
func.call( this, range, editor );
selection.selectRanges( originalRanges || ranges );
}
} )();
/**
* 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.styleCommand( 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.0
* @class
* @singleton
* @extends CKEDITOR.resourceManager
*/
CKEDITOR.stylesSet = new CKEDITOR.resourceManager( '', 'stylesSet' );
// Backward compatibility (https://dev.ckeditor.com/ticket/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;
}
// https://dev.ckeditor.com/ticket/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.0
* @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.0
* @property {Boolean} [alwaysRemoveElement=false]
* @member CKEDITOR.style
*/
/**
* Disables inline styling on read-only elements.
*
* @since 3.5.0
* @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 the styles file will be loaded.
* Note that the `name` has to be equal to the name used in
* {@link CKEDITOR.stylesSet#add} while registering the styles set.
*
* **Note**: Since 4.1.0 it is possible to set `stylesSet` to `false`
* to prevent loading any styles set.
*
* Read more in the {@glink features/styles documentation}
* and see the {@glink examples/styles example}.
*
* // Do not load any file. The styles set is empty.
* config.stylesSet = false;
*
* // Load the 'mystyles' styles set from the styles.js file.
* config.stylesSet = 'mystyles';
*
* // Load the 'mystyles' styles set from a relative URL.
* config.stylesSet = 'mystyles:/editorstyles/styles.js';
*
* // Load the 'mystyles' styles set 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.0
* @cfg {String/Array/Boolean} [stylesSet='default']
* @member CKEDITOR.config
*/
/**
* Abstract class describing the definition of a style.
*
* This virtual class illustrates the properties that developers can use to define and create
* style definitions.
*
* A style definition object represents a style as a set of properties defining the element structure, its attributes and CSS styles.
* The {@link CKEDITOR.style} based on such definition can be applied to and removed from the selection
* through various {@link CKEDITOR.style} methods.
*
* ```javascript
* {
* name: 'Special Title',
* element: 'h1',
* attributes: { class: 'my_class' },
* styles: { color: 'red', 'font-size': '16px', 'font-width': 'bold' }
* }
* ```
*
* Refer to the {@glink guide/dev_howtos_styles Styles guide} for more information about how editor content styles are handled.
*
* @class CKEDITOR.style.definition
* @abstract
*/
/**
* Defines the style type.
*
* There are three standard style types:
*
* * {@link CKEDITOR#STYLE_INLINE},
* * {@link CKEDITOR#STYLE_BLOCK},
* * {@link CKEDITOR#STYLE_OBJECT}.
*
* Each type is related to the element used in the style rule and the types of elements to which a specific style can be applied.
*
* Plugins may define {@link CKEDITOR.style.customHandlers special style handlers} to customize style operations.
* To use a special style handler, the `type` property should be set to the name of the style handler, e.g. `widget`.
*
* Refer to the {@glink features/styles#style-types Style Types section of the Applying Styles to Editor Content guide} for more information about style types.
*
* ```javascript
* { type: CKEDITOR.STYLE_INLINE }
* ```
*
* @property {String/Number} type=CKEDITOR.STYLE_INLINE
*/
/**
* A unique style definition name. It can be used to differentiate style definitions, like in the {@glink features/styles Styles Combo} plugin
* drop-down where it represents item labels.
*
* ```javascript
* { name: 'Special title' }
* ```
*
* @property {String} name
*/
/**
* A set of properties specifying attributes of the HTML style element.
* If the `style` attribute is present, it will be merged with the existing {@link CKEDITOR.style.definition#styles} property.
*
* ```javascript
* {
* attributes: {
* style: 'color: red',
* class: 'link'
* }
* }
* ```
*
* @property {Object.} attributes
*/
/**
* An element type that will be applied to the selection when applying a style. It should be a valid HTML element, for example `span`.
*
* ```javascript
* { element: 'h1' }
* ```
*
* @property {String} element
*/
/**
* A set of properties specifying CSS style rules of the HTML style element.
*
* ```javascript
* {
* styles: {
* color: 'red',
* 'font-size': '12px'
* 'font-weight': 'bold'
* }
* }
* ```
*
* @property {Object.} styles
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/* global Promise, ES6Promise */
( function() {
'use strict';
if ( window.Promise ) {
CKEDITOR.tools.promise = Promise;
} else {
var polyfillURL = CKEDITOR.getUrl( 'vendor/promise.js' );
CKEDITOR.scriptLoader.load( polyfillURL, function( success ) {
if ( success ) {
CKEDITOR.tools.promise = ES6Promise;
} else {
CKEDITOR.error( 'no-vendor-lib', {
path: polyfillURL
} );
}
} );
}
/**
* An alias for the [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
* object representing an asynchronous operation.
*
* It uses the native `Promise` browser implementation if it is available. For older browsers with lack of `Promise` support,
* the [`ES6-Promise`](https://github.com/stefanpenner/es6-promise) polyfill is used.
* See the [Promise Browser Compatibility table](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#Browser_compatibility)
* to learn more.
*
* Refer to [MDN Using Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) guide for
* more details on how to work with promises.
*
* **NOTE:** `catch` and `finally` are reserved keywords in IE<9 browsers. Use bracket notation instead:
*
* ```js
* promise[ 'catch' ]( function( err ) {
* // ...
* } );
*
* promise[ 'finally' ]( function() {
* // ...
* } );
* ```
*
* @since 4.12.0
* @class CKEDITOR.tools.promise
*/
/**
* Creates a new `Promise` instance.
*
* ```js
* new CKEDITOR.tools.promise( function( resolve, reject ) {
* setTimeout( function() {
* var timestamp;
* try {
* timestamp = ( new Date() ).getTime();
* } catch ( e ) {
* reject( e );
* }
* resolve( timestamp );
* }, 5000 );
* } );
* ```
*
* @param {Function} resolver
* @param {Function} resolver.resolve
* @param {Function} resolver.reject
* @constructor
*/
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Defines the {@link CKEDITOR.dom.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-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-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();
// Assign root value if startNode is null (#424)(https://dev.ckeditor.com/ticket/17028).
if ( !e ) {
e = root;
}
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. (https://dev.ckeditor.com/ticket/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 i = 0,
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;
if ( excludeRoot ) {
if ( !fromTop ) {
length -= 1;
} else {
i += 1;
}
}
if ( fromTop ) {
elements = Array.prototype.slice.call( elements, 0 );
elements.reverse();
}
for ( ; 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-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Defines the {@link CKEDITOR.dom.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. (https://dev.ckeditor.com/ticket/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-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-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.(https://dev.ckeditor.com/ticket/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-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Defines the {@link CKEDITOR.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';
// If we use apostrophes in background-image, we must escape apostrophes in path (just to be sure). (https://dev.ckeditor.com/ticket/13361)
if ( path )
path = path.replace( /'/g, '\\\'' );
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,
iframe = panel.element.getElementsByTag( 'iframe' ).getItem( 0 ).getFrameDocument();
// Add the stylesheet if missing.
if ( !iframe.getById( 'cke_ui_color' ) ) {
var node = getStylesheet( iframe );
uiColorMenus.push( node );
// Cleanup after destroying the editor (#589).
editor.on( 'destroy', function() {
uiColorMenus = CKEDITOR.tools.array.filter( uiColorMenus, function( storedNode ) {
return node !== storedNode;
} );
} );
var color = editor.getUiColor();
// Set uiColor for the 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.
*/
/**
* To help implement browser-specific "hacks" to the skin files and make it easy to maintain,
* it is possible to have dedicated files for such browsers. The browser files must be named after the main file names,
* appended by an underscore and the browser name (e.g. `editor_ie.css`, `editor_ie8.css`). The accepted browser names
* must match the {@link CKEDITOR.env} properties. You can find more information about browser "hacks" in the
* {@glink guide/skin_sdk_browser_hacks Dedicated Browser Hacks} guide.
*
* CKEDITOR.skin.ua_editor = 'ie,iequirks,ie8,gecko';
*
* @property {String} ua_editor
*/
/**
* Similar to {@link #ua_editor} but used for dialog stylesheets.
*
* CKEDITOR.skin.ua_dialog = 'ie,iequirks,ie8,gecko';
*
* @property {String} ua_dialog
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview API initialization code.
*/
( function() {
// Disable HC detection in WebKit. (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-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
// -----------------------------------------
// (https://ckeditor.com/docs/ckeditor4/latest/guide/skin_sdk_browser_hacks.html)
//
// 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:
// https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_env.html
//
// 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
// -----------------------------------
// (https://ckeditor.com/docs/ckeditor4/latest/guide/skin_sdk_chameleon.html)
//
// "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-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-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. (https://dev.ckeditor.com/ticket/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.
*
* Since 4.11.0 it also represents the phone number input.
*
* @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.(https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/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();
// https://dev.ckeditor.com/ticket/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 exists onClick in elementDefinition, then it is called and checked response type.
// If it's possible, then XHR is used, what prevents of using submit.
var responseType = onClick ? onClick.call( this, evt ) : false;
if ( responseType !== false ) {
if ( responseType !== 'xhr' ) {
dialog.getContentElement( target[ 0 ], target[ 1 ] ).submit();
}
this.disable();
}
};
dialog.on( 'load', function() {
dialog.getContentElement( elementDefinition[ 'for' ][ 0 ], elementDefinition[ 'for' ][ 1 ] )._.buttons.push( me );
} );
CKEDITOR.ui.dialog.button.call( this, dialog, myDefinition, htmlList );
},
html: ( function() {
var myHtmlRe = /^\s*<[\w:]+\s+([^>]*)?>/,
theirHtmlRe = /^(\s*<[\w:]+(?:\s+[^>]*)?)((?:.|\r|\n)+)$/,
emptyTagRe = /\/$/;
/**
* A dialog window element made from raw HTML code.
*
* @class CKEDITOR.ui.dialog.html
* @extends CKEDITOR.ui.dialog.uiElement
* @constructor Creates a html class instance.
* @param {CKEDITOR.dialog} dialog Parent dialog window object.
* @param {CKEDITOR.dialog.definition.uiElement} elementDefinition Element definition.
* Accepted fields:
*
* * `html` (Required) HTML code of this element.
*
* @param {Array} htmlList List of HTML code to be added to the dialog's content area.
*/
return function( dialog, elementDefinition, htmlList ) {
if ( arguments.length < 3 )
return;
var myHtmlList = [],
myHtml,
theirHtml = elementDefinition.html,
myMatch, theirMatch;
// If the HTML input doesn't contain any tags at the beginning, add a
tag around it.
if ( theirHtml.charAt( 0 ) != '<' )
theirHtml = '' + theirHtml + ' ';
// Look for focus function in definition.
var focus = elementDefinition.focus;
if ( focus ) {
var oldFocus = this.focus;
this.focus = function() {
( typeof focus == 'function' ? focus : oldFocus ).call( this );
this.fire( 'focus' );
};
if ( elementDefinition.isFocusable ) {
var oldIsFocusable = this.isFocusable;
this.isFocusable = oldIsFocusable;
}
this.keyboardFocusable = true;
}
CKEDITOR.ui.dialog.uiElement.call( this, dialog, elementDefinition, myHtmlList, 'span', null, null, '' );
// Append the attributes created by the uiElement call to the real HTML.
myHtml = myHtmlList.join( '' );
myMatch = myHtml.match( myHtmlRe );
theirMatch = theirHtml.match( theirHtmlRe ) || [ '', '', '' ];
if ( emptyTagRe.test( theirMatch[ 1 ] ) ) {
theirMatch[ 1 ] = theirMatch[ 1 ].slice( 0, -1 );
theirMatch[ 2 ] = '/' + theirMatch[ 2 ];
}
htmlList.push( [ theirMatch[ 1 ], ' ', myMatch[ 1 ] || '', theirMatch[ 2 ] ].join( '' ) );
};
} )(),
/**
* Form fieldset for grouping dialog UI elements.
*
* @class CKEDITOR.ui.dialog.fieldset
* @extends CKEDITOR.ui.dialog.uiElement
* @constructor Creates a fieldset class instance.
* @param {CKEDITOR.dialog} dialog Parent dialog window object.
* @param {Array} childObjList
* Array of {@link CKEDITOR.ui.dialog.uiElement} objects inside this container.
* @param {Array} childHtmlList Array of HTML code that corresponds to the HTML output of all the
* objects in childObjList.
* @param {Array} htmlList Array of HTML code that this element will output to.
* @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
* The element definition. Accepted fields:
*
* * `label` (Optional) The legend of the this fieldset.
* * `children` (Required) An array of dialog window field definitions which will be grouped inside this fieldset.
*
*/
fieldset: function( dialog, childObjList, childHtmlList, htmlList, elementDefinition ) {
var legendLabel = elementDefinition.label;
/** @ignore */
var innerHTML = function() {
var html = [];
legendLabel && html.push( '' + legendLabel + ' ' );
for ( var i = 0; i < childHtmlList.length; i++ )
html.push( childHtmlList[ i ] );
return html.join( '' );
};
this._ = { children: childObjList };
CKEDITOR.ui.dialog.uiElement.call( this, dialog, elementDefinition, htmlList, 'fieldset', null, null, innerHTML );
}
}, true );
CKEDITOR.ui.dialog.html.prototype = new CKEDITOR.ui.dialog.uiElement();
/** @class CKEDITOR.ui.dialog.labeledElement */
CKEDITOR.ui.dialog.labeledElement.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.uiElement(), {
/**
* Sets the label text of the element.
*
* @param {String} label The new label text.
* @returns {CKEDITOR.ui.dialog.labeledElement} The current labeled element.
*/
setLabel: function( label ) {
var node = CKEDITOR.document.getById( this._.labelId );
if ( node.getChildCount() < 1 )
( new CKEDITOR.dom.text( label, CKEDITOR.document ) ).appendTo( node );
else
node.getChild( 0 ).$.nodeValue = label;
return this;
},
/**
* Retrieves the current label text of the elment.
*
* @returns {String} The current label text.
*/
getLabel: function() {
var node = CKEDITOR.document.getById( this._.labelId );
if ( !node || node.getChildCount() < 1 )
return '';
else
return node.getChild( 0 ).getText();
},
/**
* Defines the `onChange` event for UI element definitions.
* @property {Object}
*/
eventProcessors: commonEventProcessors
}, true );
/** @class CKEDITOR.ui.dialog.button */
CKEDITOR.ui.dialog.button.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.uiElement(), {
/**
* Simulates a click to the button.
*
* @returns {Object} Return value of the `click` event.
*/
click: function() {
if ( !this._.disabled )
return this.fire( 'click', { dialog: this._.dialog } );
return false;
},
/**
* Enables the button.
*/
enable: function() {
this._.disabled = false;
var element = this.getElement();
element && element.removeClass( 'cke_disabled' );
},
/**
* Disables the button.
*/
disable: function() {
this._.disabled = true;
this.getElement().addClass( 'cke_disabled' );
},
/**
* Checks whether a field is visible.
*
* @returns {Boolean}
*/
isVisible: function() {
return this.getElement().getFirst().isVisible();
},
/**
* Checks whether a field is enabled. Fields can be disabled by using the
* {@link #disable} method and enabled by using the {@link #enable} method.
*
* @returns {Boolean}
*/
isEnabled: function() {
return !this._.disabled;
},
/**
* Defines the `onChange` event and `onClick` for button element definitions.
*
* @property {Object}
*/
eventProcessors: CKEDITOR.tools.extend( {}, CKEDITOR.ui.dialog.uiElement.prototype.eventProcessors, {
onClick: function( dialog, func ) {
this.on( 'click', function() {
func.apply( this, arguments );
} );
}
}, true ),
/**
* Handler for the element's access key up event. Simulates a click to
* the button.
*/
accessKeyUp: function() {
this.click();
},
/**
* Handler for the element's access key down event. Simulates a mouse
* down to the button.
*/
accessKeyDown: function() {
this.focus();
},
keyboardFocusable: true
}, true );
/** @class CKEDITOR.ui.dialog.textInput */
CKEDITOR.ui.dialog.textInput.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.labeledElement(), {
/**
* Gets the text input DOM element under this UI object.
*
* @returns {CKEDITOR.dom.element} The DOM element of the text input.
*/
getInputElement: function() {
return CKEDITOR.document.getById( this._.inputId );
},
/**
* Puts focus into the text input.
*/
focus: function() {
var me = this.selectParentTab();
// GECKO BUG: setTimeout() is needed to workaround invisible selections.
setTimeout( function() {
var element = me.getInputElement();
element && element.$.focus();
}, 0 );
},
/**
* Selects all the text in the text input.
*/
select: function() {
var me = this.selectParentTab();
// GECKO BUG: setTimeout() is needed to workaround invisible selections.
setTimeout( function() {
var e = me.getInputElement();
if ( e ) {
e.$.focus();
e.$.select();
}
}, 0 );
},
/**
* Handler for the text input's access key up event. Makes a `select()`
* call to the text input.
*/
accessKeyUp: function() {
this.select();
},
/**
* Sets the value of this text input object.
*
* uiElement.setValue( 'Blamo' );
*
* @param {Object} value The new value.
* @returns {CKEDITOR.ui.dialog.textInput} The current UI element.
*/
setValue: function( value ) {
if ( this.bidi ) {
var marker = value && value.charAt( 0 ),
dir = ( marker == '\u202A' ? 'ltr' : marker == '\u202B' ? 'rtl' : null );
if ( dir ) {
value = value.slice( 1 );
}
// Set the marker or reset it (if dir==null).
this.setDirectionMarker( dir );
}
if ( !value ) {
value = '';
}
return CKEDITOR.ui.dialog.uiElement.prototype.setValue.apply( this, arguments );
},
/**
* Gets the value of this text input object.
*
* @returns {String} The value.
*/
getValue: function() {
var value = CKEDITOR.ui.dialog.uiElement.prototype.getValue.call( this );
if ( this.bidi && value ) {
var dir = this.getDirectionMarker();
if ( dir ) {
value = ( dir == 'ltr' ? '\u202A' : '\u202B' ) + value;
}
}
return value;
},
/**
* Sets the text direction marker and the `dir` attribute of the input element.
*
* @since 4.5.0
* @param {String} dir The text direction. Pass `null` to reset.
*/
setDirectionMarker: function( dir ) {
var inputElement = this.getInputElement();
if ( dir ) {
inputElement.setAttributes( {
dir: dir,
'data-cke-dir-marker': dir
} );
// Don't remove the dir attribute if this field hasn't got the marker,
// because the dir attribute could be set independently.
} else if ( this.getDirectionMarker() ) {
inputElement.removeAttributes( [ 'dir', 'data-cke-dir-marker' ] );
}
},
/**
* Gets the value of the text direction marker.
*
* @since 4.5.0
* @returns {String} `'ltr'`, `'rtl'` or `null` if the marker is not set.
*/
getDirectionMarker: function() {
return this.getInputElement().data( 'cke-dir-marker' );
},
keyboardFocusable: true
}, commonPrototype, true );
CKEDITOR.ui.dialog.textarea.prototype = new CKEDITOR.ui.dialog.textInput();
/** @class CKEDITOR.ui.dialog.select */
CKEDITOR.ui.dialog.select.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.labeledElement(), {
/**
* Gets the DOM element of the select box.
*
* @returns {CKEDITOR.dom.element} The `` element of this 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 || ( CKEDITOR.env.version > 8 ) )
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 ` ' );
event.data.html += output.join( '' );
} );
editor.on( 'destroy', function() {
if ( this.toolbox ) {
var toolbars,
index = 0,
i, items, instance;
toolbars = this.toolbox.toolbars;
for ( ; index < toolbars.length; index++ ) {
items = toolbars[ index ].items;
for ( i = 0; i < items.length; i++ ) {
instance = items[ i ];
if ( instance.clickFn )
CKEDITOR.tools.removeFunction( instance.clickFn );
if ( instance.keyDownFn )
CKEDITOR.tools.removeFunction( instance.keyDownFn );
}
}
}
} );
// Manage editor focus when navigating the toolbar.
editor.on( 'uiReady', function() {
var toolbox = editor.ui.space( 'toolbox' );
toolbox && editor.focusManager.add( toolbox, 1 );
} );
editor.addCommand( 'toolbarFocus', commands.toolbarFocus );
editor.setKeystroke( CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' );
editor.ui.add( '-', CKEDITOR.UI_SEPARATOR, {} );
editor.ui.addHandler( CKEDITOR.UI_SEPARATOR, {
create: function() {
return {
render: function( editor, output ) {
output.push( '
' );
return {};
}
};
}
} );
}
} );
function getToolbarConfig( editor ) {
var removeButtons = editor.config.removeButtons;
removeButtons = removeButtons && removeButtons.split( ',' );
function buildToolbarConfig() {
// Object containing all toolbar groups used by ui items.
var lookup = getItemDefinedGroups();
// Take the base for the new toolbar, which is basically a toolbar
// definition without items.
var toolbar = CKEDITOR.tools.clone( editor.config.toolbarGroups ) || getPrivateToolbarGroups( editor );
// Fill the toolbar groups with the available ui items.
for ( var i = 0; i < toolbar.length; i++ ) {
var toolbarGroup = toolbar[ i ];
// Skip toolbar break.
if ( toolbarGroup == '/' )
continue;
// Handle simply group name item.
else if ( typeof toolbarGroup == 'string' )
toolbarGroup = toolbar[ i ] = { name: toolbarGroup };
var items, subGroups = toolbarGroup.groups;
// Look for items that match sub groups.
if ( subGroups ) {
for ( var j = 0, sub; j < subGroups.length; j++ ) {
sub = subGroups[ j ];
// If any ui item is registered for this subgroup.
items = lookup[ sub ];
items && fillGroup( toolbarGroup, items );
}
}
// Add the main group items as well.
items = lookup[ toolbarGroup.name ];
items && fillGroup( toolbarGroup, items );
}
return toolbar;
}
// Returns an object containing all toolbar groups used by ui items.
function getItemDefinedGroups() {
var groups = {},
itemName, item, itemToolbar, group, order;
for ( itemName in editor.ui.items ) {
item = editor.ui.items[ itemName ];
itemToolbar = item.toolbar || 'others';
if ( itemToolbar ) {
// Break the toolbar property into its parts: "group_name[,order]".
itemToolbar = itemToolbar.split( ',' );
group = itemToolbar[ 0 ];
order = parseInt( itemToolbar[ 1 ] || -1, 10 );
// Initialize the group, if necessary.
groups[ group ] || ( groups[ group ] = [] );
// Push the data used to build the toolbar later.
groups[ group ].push( { name: itemName, order: order } );
}
}
// Put the items in the right order.
for ( group in groups ) {
groups[ group ] = groups[ group ].sort( function( a, b ) {
return a.order == b.order ? 0 :
b.order < 0 ? -1 :
a.order < 0 ? 1 :
a.order < b.order ? -1 :
1;
} );
}
return groups;
}
function fillGroup( toolbarGroup, uiItems ) {
if ( uiItems.length ) {
if ( toolbarGroup.items )
toolbarGroup.items.push( editor.ui.create( '-' ) );
else
toolbarGroup.items = [];
var item, name;
while ( ( item = uiItems.shift() ) ) {
name = typeof item == 'string' ? item : item.name;
// Ignore items that are configured to be removed.
if ( !removeButtons || CKEDITOR.tools.indexOf( removeButtons, name ) == -1 ) {
item = editor.ui.create( name );
if ( !item )
continue;
if ( !editor.addFeature( item ) )
continue;
toolbarGroup.items.push( item );
}
}
}
}
function populateToolbarConfig( config ) {
var toolbar = [],
i, group, newGroup;
for ( i = 0; i < config.length; ++i ) {
group = config[ i ];
newGroup = {};
if ( group == '/' )
toolbar.push( group );
else if ( CKEDITOR.tools.isArray( group ) ) {
fillGroup( newGroup, CKEDITOR.tools.clone( group ) );
toolbar.push( newGroup );
}
else if ( group.items ) {
fillGroup( newGroup, CKEDITOR.tools.clone( group.items ) );
newGroup.name = group.name;
toolbar.push( newGroup );
}
}
return toolbar;
}
var toolbar = editor.config.toolbar;
// If it is a string, return the relative "toolbar_name" config.
if ( typeof toolbar == 'string' )
toolbar = editor.config[ 'toolbar_' + toolbar ];
return ( editor.toolbar = toolbar ? populateToolbarConfig( toolbar ) : buildToolbarConfig() );
}
/**
* Adds a toolbar group. See {@link CKEDITOR.config#toolbarGroups} for more details.
*
* **Note:** This method will not modify toolbar groups set explicitly by
* {@link CKEDITOR.config#toolbarGroups}. It will only extend the default setting.
*
* @param {String} name Toolbar group name.
* @param {Number/String} previous The name of the toolbar group after which this one
* should be added or `0` if this group should be the first one.
* @param {String} [subgroupOf] The name of the parent group.
* @member CKEDITOR.ui
*/
CKEDITOR.ui.prototype.addToolbarGroup = function( name, previous, subgroupOf ) {
// The toolbarGroups from the privates is the one we gonna use for automatic toolbar creation.
var toolbarGroups = getPrivateToolbarGroups( this.editor ),
atStart = previous === 0,
newGroup = { name: name };
if ( subgroupOf ) {
// Transform the subgroupOf name in the real subgroup object.
subgroupOf = CKEDITOR.tools.search( toolbarGroups, function( group ) {
return group.name == subgroupOf;
} );
if ( subgroupOf ) {
!subgroupOf.groups && ( subgroupOf.groups = [] ) ;
if ( previous ) {
// Search the "previous" item and add the new one after it.
previous = CKEDITOR.tools.indexOf( subgroupOf.groups, previous );
if ( previous >= 0 ) {
subgroupOf.groups.splice( previous + 1, 0, name );
return;
}
}
// If no previous found.
if ( atStart )
subgroupOf.groups.splice( 0, 0, name );
else
subgroupOf.groups.push( name );
return;
} else {
// Ignore "previous" if subgroupOf has not been found.
previous = null;
}
}
if ( previous ) {
// Transform the "previous" name into its index.
previous = CKEDITOR.tools.indexOf( toolbarGroups, function( group ) {
return group.name == previous;
} );
}
if ( atStart )
toolbarGroups.splice( 0, 0, name );
else if ( typeof previous == 'number' )
toolbarGroups.splice( previous + 1, 0, newGroup );
else
toolbarGroups.push( name );
};
function getPrivateToolbarGroups( editor ) {
return editor._.toolbarGroups || ( editor._.toolbarGroups = [
{ name: 'document', groups: [ 'mode', 'document', 'doctools' ] },
{ name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
{ name: 'editing', groups: [ 'find', 'selection', 'spellchecker' ] },
{ name: 'forms' },
'/',
{ name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
{ name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ] },
{ name: 'links' },
{ name: 'insert' },
'/',
{ name: 'styles' },
{ name: 'colors' },
{ name: 'tools' },
{ name: 'others' },
{ name: 'about' }
] );
}
} )();
/**
* Separator UI element.
*
* @readonly
* @property {String} [='separator']
* @member CKEDITOR
*/
CKEDITOR.UI_SEPARATOR = 'separator';
/**
* The part of the user interface where the toolbar will be rendered. For the default
* editor implementation, the recommended options are `'top'` and `'bottom'`.
*
* Please note that this option is only applicable to {@glink guide/dev_framed classic}
* (`iframe`-based) editor. In case of {@glink guide/dev_inline inline} editor the toolbar
* position is set dynamically depending on the position of the editable element on the screen.
*
* Read more in the {@glink features/toolbarlocation documentation}
* and see the {@glink examples/toolbarlocation example}.
*
* config.toolbarLocation = 'bottom';
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.toolbarLocation = 'top';
/**
* The toolbox (alias toolbar) definition. It is a toolbar name or an array of
* toolbars (strips), each one being also an array, containing a list of UI items.
*
* If set to `null`, the toolbar will be generated automatically using all available buttons
* and {@link #toolbarGroups} as a toolbar groups layout.
*
* In CKEditor 4.5.0+ you can generate your toolbar customization code by using the {@glink features/toolbar visual
* toolbar configurator}.
*
* // Defines a toolbar with only one strip containing the "Source" button, a
* // separator, and the "Bold" and "Italic" buttons.
* config.toolbar = [
* [ 'Source', '-', 'Bold', 'Italic' ]
* ];
*
* // Similar to the example above, defines a "Basic" toolbar with only one strip containing three buttons.
* // Note that this setting is composed by "toolbar_" added to the toolbar name, which in this case is called "Basic".
* // This second part of the setting name can be anything. You must use this name in the CKEDITOR.config.toolbar setting
* // in order to instruct the editor which `toolbar_(name)` setting should be used.
* config.toolbar_Basic = [
* [ 'Source', '-', 'Bold', 'Italic' ]
* ];
* // Load toolbar_Name where Name = Basic.
* config.toolbar = 'Basic';
*
* @cfg {Array/String} [toolbar=null]
* @member CKEDITOR.config
*/
/**
* The toolbar groups definition.
*
* If the toolbar layout is not explicitly defined by the {@link #toolbar} setting, then
* this setting is used to group all defined buttons (see {@link CKEDITOR.ui#addButton}).
* Buttons are associated with toolbar groups by the `toolbar` property in their definition objects.
*
* New groups may be dynamically added during the editor and plugin initialization by
* {@link CKEDITOR.ui#addToolbarGroup}. This is only possible if the default setting was used.
*
* // Default setting.
* config.toolbarGroups = [
* { name: 'document', groups: [ 'mode', 'document', 'doctools' ] },
* { name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
* { name: 'editing', groups: [ 'find', 'selection', 'spellchecker' ] },
* { name: 'forms' },
* '/',
* { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
* { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ] },
* { name: 'links' },
* { name: 'insert' },
* '/',
* { name: 'styles' },
* { name: 'colors' },
* { name: 'tools' },
* { name: 'others' },
* { name: 'about' }
* ];
*
* @cfg {Array} [toolbarGroups=see example]
* @member CKEDITOR.config
*/
/**
* Whether the toolbar can be collapsed by the user. If disabled, the Collapse Toolbar
* button will not be displayed.
*
* config.toolbarCanCollapse = true;
*
* @cfg {Boolean} [toolbarCanCollapse=false]
* @member CKEDITOR.config
*/
/**
* Whether the toolbar must start expanded when the editor is loaded.
*
* Setting this option to `false` will affect the toolbar only when
* {@link #toolbarCanCollapse} is set to `true`:
*
* config.toolbarCanCollapse = true;
* config.toolbarStartupExpanded = false;
*
* @cfg {Boolean} [toolbarStartupExpanded=true]
* @member CKEDITOR.config
*/
/**
* When enabled, causes the *Arrow* keys navigation to cycle within the current
* toolbar group. Otherwise the *Arrow* keys will move through all items available in
* the toolbar. The *Tab* key will still be used to quickly jump among the
* toolbar groups.
*
* config.toolbarGroupCycling = false;
*
* @since 3.6.0
* @cfg {Boolean} [toolbarGroupCycling=true]
* @member CKEDITOR.config
*/
/**
* List of toolbar button names that must not be rendered. This will also work
* for non-button toolbar items, like the Font drop-down list.
*
* config.removeButtons = 'Underline,JustifyCenter';
*
* This configuration option should not be overused. The recommended way is to use the
* {@link CKEDITOR.config#removePlugins} setting to remove features from the editor
* or even better, [create a custom editor build](https://ckeditor.com/cke4/builder) with
* just the features that you will use.
* In some cases though, a single plugin may define a set of toolbar buttons and
* `removeButtons` may be useful when just a few of them are to be removed.
*
* @cfg {String} [removeButtons]
* @member CKEDITOR.config
*/
/**
* The toolbar definition used by the editor. It is created from the
* {@link CKEDITOR.config#toolbar} option if it is set or automatically
* based on {@link CKEDITOR.config#toolbarGroups}.
*
* @readonly
* @property {Object} toolbar
* @member CKEDITOR.editor
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @ignore
* File overview: Clipboard support.
*/
//
// COPY & PASTE EXECUTION FLOWS:
// -- CTRL+C
// * if ( isCustomCopyCutSupported )
// * dataTransfer.setData( 'text/html', getSelectedHtml )
// * else
// * browser's default behavior
// -- CTRL+X
// * listen onKey (onkeydown)
// * fire 'saveSnapshot' on editor
// * if ( isCustomCopyCutSupported )
// * dataTransfer.setData( 'text/html', getSelectedHtml )
// * extractSelectedHtml // remove selected contents
// * else
// * browser's default behavior
// * deferred second 'saveSnapshot' event
// -- CTRL+V
// * listen onKey (onkeydown)
// * simulate 'beforepaste' for non-IEs on editable
// * listen 'onpaste' on editable ('onbeforepaste' for IE)
// * fire 'beforePaste' on editor
// * if ( !canceled && ( htmlInDataTransfer || !external paste) && dataTransfer is not empty ) getClipboardDataByPastebin
// * fire 'paste' on editor
// * !canceled && fire 'afterPaste' on editor
// -- Copy command
// * tryToCutCopy
// * execCommand
// * !success && notification
// -- Cut command
// * fixCut
// * tryToCutCopy
// * execCommand
// * !success && notification
// -- Paste command
// * fire 'paste' on editable ('beforepaste' for IE)
// * !canceled && execCommand 'paste'
// -- Paste from native context menu & menubar
// (Fx & Webkits are handled in 'paste' default listener.
// Opera cannot be handled at all because it doesn't fire any events
// Special treatment is needed for IE, for which is this part of doc)
// * listen 'onpaste'
// * cancel native event
// * fire 'beforePaste' on editor
// * if ( !canceled && ( htmlInDataTransfer || !external paste) && dataTransfer is not empty ) getClipboardDataByPastebin
// * execIECommand( 'paste' ) -> this fires another 'paste' event, so cancel it
// * fire 'paste' on editor
// * !canceled && fire 'afterPaste' on editor
//
//
// PASTE EVENT - PREPROCESSING:
// -- Possible dataValue types: auto, text, html.
// -- Possible dataValue contents:
// * text (possible \n\r)
// * htmlified text (text + br,div,p - no presentational markup & attrs - depends on browser)
// * html
// -- Possible flags:
// * htmlified - if true then content is a HTML even if no markup inside. This flag is set
// for content from editable pastebins, because they 'htmlify' pasted content.
//
// -- Type: auto:
// * content: htmlified text -> filter, unify text markup (brs, ps, divs), set type: text
// * content: html -> filter, set type: html
// -- Type: text:
// * content: htmlified text -> filter, unify text markup
// * content: html -> filter, strip presentational markup, unify text markup
// -- Type: html:
// * content: htmlified text -> filter, unify text markup
// * content: html -> filter
//
// -- Phases:
// * if dataValue is empty copy data from dataTransfer to dataValue (priority 1)
// * filtering (priorities 3-5) - e.g. pastefromword filters
// * content type sniffing (priority 6)
// * markup transformations for text (priority 6)
//
// DRAG & DROP EXECUTION FLOWS:
// -- Drag
// * save to the global object:
// * drag timestamp (with 'cke-' prefix),
// * selected html,
// * drag range,
// * editor instance.
// * put drag timestamp into event.dataTransfer.text
// -- Drop
// * if events text == saved timestamp && editor == saved editor
// internal drag & drop occurred
// * getRangeAtDropPosition
// * create bookmarks for drag and drop ranges starting from the end of the document
// * dragRange.deleteContents()
// * fire 'paste' with saved html and drop range
// * if events text == saved timestamp && editor != saved editor
// cross editor drag & drop occurred
// * getRangeAtDropPosition
// * fire 'paste' with saved html
// * dragRange.deleteContents()
// * FF: refreshCursor on afterPaste
// * if events text != saved timestamp
// drop form external source occurred
// * getRangeAtDropPosition
// * if event contains html data then fire 'paste' with html
// * else if event contains text data then fire 'paste' with encoded text
// * FF: refreshCursor on afterPaste
'use strict';
( function() {
var clipboardIdDataType;
// Register the plugin.
CKEDITOR.plugins.add( 'clipboard', {
requires: 'dialog,notification,toolbar',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
var filterType,
filtersFactory = filtersFactoryFactory( editor );
if ( editor.config.forcePasteAsPlainText ) {
filterType = 'plain-text';
} else if ( editor.config.pasteFilter ) {
filterType = editor.config.pasteFilter;
}
// On Webkit the pasteFilter defaults 'semantic-content' because pasted data is so terrible
// that it must be always filtered.
else if ( CKEDITOR.env.webkit && !( 'pasteFilter' in editor.config ) ) {
filterType = 'semantic-content';
}
editor.pasteFilter = filtersFactory.get( filterType );
initPasteClipboard( editor );
initDragDrop( editor );
CKEDITOR.dialog.add( 'paste', CKEDITOR.getUrl( this.path + 'dialogs/paste.js' ) );
// Convert image file (if present) to base64 string for Firefox. Do it as the first
// step as the conversion is asynchronous and should hold all further paste processing.
if ( CKEDITOR.env.gecko ) {
var supportedImageTypes = [ 'image/png', 'image/jpeg', 'image/gif' ],
latestId;
editor.on( 'paste', function( evt ) {
var dataObj = evt.data,
data = dataObj.dataValue,
dataTransfer = dataObj.dataTransfer;
// If data empty check for image content inside data transfer. https://dev.ckeditor.com/ticket/16705
if ( !data && dataObj.method == 'paste' && dataTransfer && dataTransfer.getFilesCount() == 1 && latestId != dataTransfer.id ) {
var file = dataTransfer.getFile( 0 );
if ( CKEDITOR.tools.indexOf( supportedImageTypes, file.type ) != -1 ) {
var fileReader = new FileReader();
// Convert image file to img tag with base64 image.
fileReader.addEventListener( 'load', function() {
evt.data.dataValue = '
';
editor.fire( 'paste', evt.data );
}, false );
// Proceed with normal flow if reading file was aborted.
fileReader.addEventListener( 'abort', function() {
editor.fire( 'paste', evt.data );
}, false );
// Proceed with normal flow if reading file failed.
fileReader.addEventListener( 'error', function() {
editor.fire( 'paste', evt.data );
}, false );
fileReader.readAsDataURL( file );
latestId = dataObj.dataTransfer.id;
evt.stop();
}
}
}, null, null, 1 );
}
editor.on( 'paste', function( evt ) {
// Init `dataTransfer` if `paste` event was fired without it, so it will be always available.
if ( !evt.data.dataTransfer ) {
evt.data.dataTransfer = new CKEDITOR.plugins.clipboard.dataTransfer();
}
// If dataValue is already set (manually or by paste bin), so do not override it.
if ( evt.data.dataValue ) {
return;
}
var dataTransfer = evt.data.dataTransfer,
// IE support only text data and throws exception if we try to get html data.
// This html data object may also be empty if we drag content of the textarea.
value = dataTransfer.getData( 'text/html' );
if ( value ) {
evt.data.dataValue = value;
evt.data.type = 'html';
} else {
// Try to get text data otherwise.
value = dataTransfer.getData( 'text/plain' );
if ( value ) {
evt.data.dataValue = editor.editable().transformPlainTextToHtml( value );
evt.data.type = 'text';
}
}
}, null, null, 1 );
editor.on( 'paste', function( evt ) {
var data = evt.data.dataValue,
blockElements = CKEDITOR.dtd.$block;
// Filter webkit garbage.
if ( data.indexOf( 'Apple-' ) > -1 ) {
// Replace special webkit's with simple space, because webkit
// produces them even for normal spaces.
data = data.replace( /
<\/span>/gi, ' ' );
// Strip around white-spaces when not in forced 'html' content type.
// This spans are created only when pasting plain text into Webkit,
// but for safety reasons remove them always.
if ( evt.data.type != 'html' ) {
data = data.replace( /]*>([^<]*)<\/span>/gi, function( all, spaces ) {
// Replace tabs with 4 spaces like Fx does.
return spaces.replace( /\t/g, ' ' );
} );
}
// This br is produced only when copying & pasting HTML content.
if ( data.indexOf( ' ' ) > -1 ) {
evt.data.startsWithEOL = 1;
evt.data.preSniffing = 'html'; // Mark as not text.
data = data.replace( / /, '' );
}
// Remove all other classes.
data = data.replace( /(<[^>]+) class="Apple-[^"]*"/gi, '$1' );
}
// Strip editable that was copied from inside. (https://dev.ckeditor.com/ticket/9534)
if ( data.match( /^<[^<]+cke_(editable|contents)/i ) ) {
var tmp,
editable_wrapper,
wrapper = new CKEDITOR.dom.element( 'div' );
wrapper.setHtml( data );
// Verify for sure and check for nested editor UI parts. (https://dev.ckeditor.com/ticket/9675)
while ( wrapper.getChildCount() == 1 &&
( tmp = wrapper.getFirst() ) &&
tmp.type == CKEDITOR.NODE_ELEMENT && // Make sure first-child is element.
( tmp.hasClass( 'cke_editable' ) || tmp.hasClass( 'cke_contents' ) ) ) {
wrapper = editable_wrapper = tmp;
}
// If editable wrapper was found strip it and bogus (added on FF).
if ( editable_wrapper )
data = editable_wrapper.getHtml().replace( / $/i, '' );
}
if ( CKEDITOR.env.ie ) {
// ->
(br.cke-pasted-remove will be removed later)
data = data.replace( /^ (?: |\r\n)?<(\w+)/g, function( match, elementName ) {
if ( elementName.toLowerCase() in blockElements ) {
evt.data.preSniffing = 'html'; // Mark as not a text.
return '<' + elementName;
}
return match;
} );
} else if ( CKEDITOR.env.webkit ) {
//
->
// We don't mark br, because this situation can happen for htmlified text too.
data = data.replace( /<\/(\w+)><\/div>$/, function( match, elementName ) {
if ( elementName in blockElements ) {
evt.data.endsWithEOL = 1;
return '' + elementName + '>';
}
return match;
} );
} else if ( CKEDITOR.env.gecko ) {
// Firefox adds bogus
when user pasted text followed by space(s).
data = data.replace( /(\s)
$/, '$1' );
}
evt.data.dataValue = data;
}, null, null, 3 );
editor.on( 'paste', function( evt ) {
var dataObj = evt.data,
type = editor._.nextPasteType || dataObj.type,
data = dataObj.dataValue,
trueType,
// Default is 'html'.
defaultType = editor.config.clipboard_defaultContentType || 'html',
transferType = dataObj.dataTransfer.getTransferType( editor ),
isExternalPaste = transferType == CKEDITOR.DATA_TRANSFER_EXTERNAL,
isActiveForcePAPT = editor.config.forcePasteAsPlainText === true;
// If forced type is 'html' we don't need to know true data type.
if ( type == 'html' || dataObj.preSniffing == 'html' ) {
trueType = 'html';
} else {
trueType = recogniseContentType( data );
}
delete editor._.nextPasteType;
// Unify text markup.
if ( trueType == 'htmlifiedtext' ) {
data = htmlifiedTextHtmlification( editor.config, data );
}
// Strip presentational markup & unify text markup.
// Forced plain text (dialog or forcePAPT).
// Note: we do not check dontFilter option in this case, because forcePAPT was implemented
// before pasteFilter and pasteFilter is automatically used on Webkit&Blink since 4.5, so
// forcePAPT should have priority as it had before 4.5.
if ( type == 'text' && trueType == 'html' ) {
data = filterContent( editor, data, filtersFactory.get( 'plain-text' ) );
}
// External paste and pasteFilter exists and filtering isn't disabled.
// Or force filtering even for internal and cross-editor paste, when forcePAPT is active (#620).
else if ( isExternalPaste && editor.pasteFilter && !dataObj.dontFilter || isActiveForcePAPT ) {
data = filterContent( editor, data, editor.pasteFilter );
}
if ( dataObj.startsWithEOL ) {
data = '
' + data;
}
if ( dataObj.endsWithEOL ) {
data += '
';
}
if ( type == 'auto' ) {
type = ( trueType == 'html' || defaultType == 'html' ) ? 'html' : 'text';
}
dataObj.type = type;
dataObj.dataValue = data;
delete dataObj.preSniffing;
delete dataObj.startsWithEOL;
delete dataObj.endsWithEOL;
}, null, null, 6 );
// Inserts processed data into the editor at the end of the
// events chain.
editor.on( 'paste', function( evt ) {
var data = evt.data;
if ( data.dataValue ) {
editor.insertHtml( data.dataValue, data.type, data.range );
// Defer 'afterPaste' so all other listeners for 'paste' will be fired first.
// Fire afterPaste only if paste inserted some HTML.
setTimeout( function() {
editor.fire( 'afterPaste' );
}, 0 );
}
}, null, null, 1000 );
editor.on( 'pasteDialog', function( evt ) {
// TODO it's possible that this setTimeout is not needed any more,
// because of changes introduced in the same commit as this comment.
// Editor.getClipboardData adds listener to the dialog's events which are
// fired after a while (not like 'showDialog').
setTimeout( function() {
// Open default paste dialog.
editor.openDialog( 'paste', evt.data );
}, 0 );
} );
}
} );
function firePasteEvents( editor, data, withBeforePaste ) {
if ( !data.type ) {
data.type = 'auto';
}
if ( withBeforePaste ) {
// Fire 'beforePaste' event so clipboard flavor get customized
// by other plugins.
if ( editor.fire( 'beforePaste', data ) === false )
return false; // Event canceled
}
// Do not fire paste if there is no data (dataValue and dataTranfser are empty).
// This check should be done after firing 'beforePaste' because for native paste
// 'beforePaste' is by default fired even for empty clipboard.
if ( !data.dataValue && data.dataTransfer.isEmpty() ) {
return false;
}
if ( !data.dataValue ) {
data.dataValue = '';
}
// Because of FF bug we need to use this hack, otherwise cursor is hidden
// or it is not possible to move it (https://dev.ckeditor.com/ticket/12420).
// Also, check that editor.toolbox exists, because the toolbar plugin might not be loaded (https://dev.ckeditor.com/ticket/13305).
if ( CKEDITOR.env.gecko && data.method == 'drop' && editor.toolbox ) {
editor.once( 'afterPaste', function() {
editor.toolbox.focus();
} );
}
return editor.fire( 'paste', data );
}
function initPasteClipboard( editor ) {
var clipboard = CKEDITOR.plugins.clipboard,
preventBeforePasteEvent = 0,
preventPasteEvent = 0;
addListeners();
addButtonsCommands();
/**
* Gets clipboard data by directly accessing the clipboard (IE only) or opening the paste dialog window.
*
* editor.getClipboardData( function( data ) {
* if ( data )
* alert( data.type + ' ' + data.dataValue );
* } );
*
* @member CKEDITOR.editor
* @param {Function/Object} callbackOrOptions For function, see the `callback` parameter documentation. The object was used before 4.7.0 with the `title` property, to set the paste dialog's title.
* @param {Function} callback A function that will be executed with the `data` property of the
* {@link CKEDITOR.editor#event-paste paste event} or `null` if none of the capturing methods succeeded.
* Since 4.7.0 the `callback` should be provided as a first argument, just like in the example above. This parameter will be removed in
* an upcoming major release.
*/
editor.getClipboardData = function( callbackOrOptions, callback ) {
var beforePasteNotCanceled = false,
dataType = 'auto';
// Options are optional - args shift.
if ( !callback ) {
callback = callbackOrOptions;
callbackOrOptions = null;
}
// Listen at the end of listeners chain to see if event wasn't canceled
// and to retrieve modified data.type.
editor.on( 'beforePaste', onBeforePaste, null, null, 1000 );
// Listen with maximum priority to handle content before everyone else.
// This callback will handle paste event that will be fired if direct
// access to the clipboard succeed in IE.
editor.on( 'paste', onPaste, null, null, 0 );
// If command didn't succeed (only IE allows to access clipboard and only if
// user agrees) invoke callback with null, meaning that paste is not blocked.
if ( getClipboardDataDirectly() === false ) {
// Direct access to the clipboard wasn't successful so remove listener.
editor.removeListener( 'paste', onPaste );
// If beforePaste was canceled do not open dialog.
// Add listeners only if dialog really opened. 'pasteDialog' can be canceled.
if ( editor._.forcePasteDialog && beforePasteNotCanceled && editor.fire( 'pasteDialog' ) ) {
editor.on( 'pasteDialogCommit', onDialogCommit );
// 'dialogHide' will be fired after 'pasteDialogCommit'.
editor.on( 'dialogHide', function( evt ) {
evt.removeListener();
evt.data.removeListener( 'pasteDialogCommit', onDialogCommit );
// Notify even if user canceled dialog (clicked 'cancel', ESC, etc).
if ( !evt.data._.committed ) {
callback( null );
}
} );
} else {
callback( null );
}
}
function onPaste( evt ) {
evt.removeListener();
evt.cancel();
callback( evt.data );
}
function onBeforePaste( evt ) {
evt.removeListener();
beforePasteNotCanceled = true;
dataType = evt.data.type;
}
function onDialogCommit( evt ) {
evt.removeListener();
// Cancel pasteDialogCommit so paste dialog won't automatically fire
// 'paste' evt by itself.
evt.cancel();
callback( {
type: dataType,
dataValue: evt.data.dataValue,
dataTransfer: evt.data.dataTransfer,
method: 'paste'
} );
}
};
function addButtonsCommands() {
addButtonCommand( 'Cut', 'cut', createCutCopyCmd( 'cut' ), 10, 1 );
addButtonCommand( 'Copy', 'copy', createCutCopyCmd( 'copy' ), 20, 4 );
addButtonCommand( 'Paste', 'paste', createPasteCmd(), 30, 8 );
// Force adding touchend handler to paste button (#595).
if ( !editor._.pasteButtons ) {
editor._.pasteButtons = [];
}
editor._.pasteButtons.push( 'Paste' );
function addButtonCommand( buttonName, commandName, command, toolbarOrder, ctxMenuOrder ) {
var lang = editor.lang.clipboard[ commandName ];
editor.addCommand( commandName, command );
editor.ui.addButton && editor.ui.addButton( buttonName, {
label: lang,
command: commandName,
toolbar: 'clipboard,' + toolbarOrder
} );
// If the "menu" plugin is loaded, register the menu item.
if ( editor.addMenuItems ) {
editor.addMenuItem( commandName, {
label: lang,
command: commandName,
group: 'clipboard',
order: ctxMenuOrder
} );
}
}
}
function addListeners() {
editor.on( 'key', onKey );
editor.on( 'contentDom', addPasteListenersToEditable );
// For improved performance, we're checking the readOnly state on selectionChange instead of hooking a key event for that.
editor.on( 'selectionChange', setToolbarStates );
// If the "contextmenu" plugin is loaded, register the listeners.
if ( editor.contextMenu ) {
editor.contextMenu.addListener( function() {
return {
cut: stateFromNamedCommand( 'cut' ),
copy: stateFromNamedCommand( 'copy' ),
paste: stateFromNamedCommand( 'paste' )
};
} );
// Adds 'touchend' integration with context menu paste item (#1347).
var pasteListener = null;
editor.on( 'menuShow', function() {
// Remove previous listener.
if ( pasteListener ) {
pasteListener.removeListener();
pasteListener = null;
}
// Attach new 'touchend' listeners to context menu paste items.
var item = editor.contextMenu.findItemByCommandName( 'paste' );
if ( item && item.element ) {
pasteListener = item.element.on( 'touchend', function() {
editor._.forcePasteDialog = true;
} );
}
} );
}
// Detect if any of paste buttons was touched. In such case we assume that user is using
// touch device and force displaying paste dialog (#595).
if ( editor.ui.addButton ) {
// Waiting for editor instance to be ready seems to be the most reliable way to
// be sure that paste buttons are already created.
editor.once( 'instanceReady', function() {
if ( !editor._.pasteButtons ) {
return;
}
CKEDITOR.tools.array.forEach( editor._.pasteButtons, function( name ) {
var pasteButton = editor.ui.get( name );
// Check if button was not removed by `removeButtons` config.
if ( pasteButton ) {
var buttonElement = CKEDITOR.document.getById( pasteButton._.id );
if ( buttonElement ) {
buttonElement.on( 'touchend', function() {
editor._.forcePasteDialog = true;
} );
}
}
} );
} );
}
}
// Add events listeners to editable.
function addPasteListenersToEditable() {
var editable = editor.editable();
if ( CKEDITOR.plugins.clipboard.isCustomCopyCutSupported ) {
var initOnCopyCut = function( evt ) {
// There shouldn't be anything to copy/cut when selection is collapsed (#869).
if ( editor.getSelection().isCollapsed() ) {
return;
}
// If user tries to cut in read-only editor, we must prevent default action (https://dev.ckeditor.com/ticket/13872).
if ( !editor.readOnly || evt.name != 'cut' ) {
clipboard.initPasteDataTransfer( evt, editor );
}
evt.data.preventDefault();
};
editable.on( 'copy', initOnCopyCut );
editable.on( 'cut', initOnCopyCut );
// Delete content with the low priority so one can overwrite cut data.
editable.on( 'cut', function() {
// If user tries to cut in read-only editor, we must prevent default action. (https://dev.ckeditor.com/ticket/13872)
if ( !editor.readOnly ) {
editor.extractSelectedHtml();
}
}, null, null, 999 );
}
// We'll be catching all pasted content in one line, regardless of whether
// it's introduced by a document command execution (e.g. toolbar buttons) or
// user paste behaviors (e.g. CTRL+V).
editable.on( clipboard.mainPasteEvent, function( evt ) {
if ( clipboard.mainPasteEvent == 'beforepaste' && preventBeforePasteEvent ) {
return;
}
// If you've just asked yourself why preventPasteEventNow() is not here, but
// in listener for CTRL+V and exec method of 'paste' command
// you've asked the same question we did.
//
// THE ANSWER:
//
// First thing to notice - this answer makes sense only for IE,
// because other browsers don't listen for 'paste' event.
//
// What would happen if we move preventPasteEventNow() here?
// For:
// * CTRL+V - IE fires 'beforepaste', so we prevent 'paste' and pasteDataFromClipboard(). OK.
// * editor.execCommand( 'paste' ) - we fire 'beforepaste', so we prevent
// 'paste' and pasteDataFromClipboard() and doc.execCommand( 'Paste' ). OK.
// * native context menu - IE fires 'beforepaste', so we prevent 'paste', but unfortunately
// on IE we fail with pasteDataFromClipboard() here, because of... we don't know why, but
// we just fail, so... we paste nothing. FAIL.
// * native menu bar - the same as for native context menu.
//
// But don't you know any way to distinguish first two cases from last two?
// Only one - special flag set in CTRL+V handler and exec method of 'paste'
// command. And that's what we did using preventPasteEventNow().
pasteDataFromClipboard( evt );
} );
// It's not possible to clearly handle all four paste methods (ctrl+v, native menu bar
// native context menu, editor's command) in one 'paste/beforepaste' event in IE.
//
// For ctrl+v & editor's command it's easy to handle pasting in 'beforepaste' listener,
// so we do this. For another two methods it's better to use 'paste' event.
//
// 'paste' is always being fired after 'beforepaste' (except of weird one on opening native
// context menu), so for two methods handled in 'beforepaste' we're canceling 'paste'
// using preventPasteEvent state.
//
// 'paste' event in IE is being fired before getClipboardDataByPastebin executes its callback.
//
// QUESTION: Why didn't you handle all 4 paste methods in handler for 'paste'?
// Wouldn't this just be simpler?
// ANSWER: Then we would have to evt.data.preventDefault() only for native
// context menu and menu bar pastes. The same with execIECommand().
// That would force us to mark CTRL+V and editor's paste command with
// special flag, other than preventPasteEvent. But we still would have to
// have preventPasteEvent for the second event fired by execIECommand.
// Code would be longer and not cleaner.
if ( clipboard.mainPasteEvent == 'beforepaste' ) {
editable.on( 'paste', function( evt ) {
if ( preventPasteEvent ) {
return;
}
// Cancel next 'paste' event fired by execIECommand( 'paste' )
// at the end of this callback.
preventPasteEventNow();
// Prevent native paste.
evt.data.preventDefault();
pasteDataFromClipboard( evt );
// Force IE to paste content into pastebin so pasteDataFromClipboard will work.
execIECommand( 'paste' );
} );
// If mainPasteEvent is 'beforePaste' (IE before Edge),
// dismiss the (wrong) 'beforepaste' event fired on context/toolbar menu open. (https://dev.ckeditor.com/ticket/7953)
editable.on( 'contextmenu', preventBeforePasteEventNow, null, null, 0 );
editable.on( 'beforepaste', function( evt ) {
// Do not prevent event on CTRL+V and SHIFT+INS because it blocks paste (https://dev.ckeditor.com/ticket/11970).
if ( evt.data && !evt.data.$.ctrlKey && !evt.data.$.shiftKey )
preventBeforePasteEventNow();
}, null, null, 0 );
}
editable.on( 'beforecut', function() {
!preventBeforePasteEvent && fixCut( editor );
} );
var mouseupTimeout;
// Use editor.document instead of editable in non-IEs for observing mouseup
// since editable won't fire the event if selection process started within
// iframe and ended out of the editor (https://dev.ckeditor.com/ticket/9851).
editable.attachListener( CKEDITOR.env.ie ? editable : editor.document.getDocumentElement(), 'mouseup', function() {
mouseupTimeout = setTimeout( setToolbarStates, 0 );
} );
// Make sure that deferred mouseup callback isn't executed after editor instance
// had been destroyed. This may happen when editor.destroy() is called in parallel
// with mouseup event (i.e. a button with onclick callback) (https://dev.ckeditor.com/ticket/10219).
editor.on( 'destroy', function() {
clearTimeout( mouseupTimeout );
} );
editable.on( 'keyup', setToolbarStates );
}
// Create object representing Cut or Copy commands.
function createCutCopyCmd( type ) {
return {
type: type,
canUndo: type == 'cut', // We can't undo copy to clipboard.
startDisabled: true,
fakeKeystroke: type == 'cut' ? CKEDITOR.CTRL + 88 /*X*/ : CKEDITOR.CTRL + 67 /*C*/,
exec: function() {
// Attempts to execute the Cut and Copy operations.
function tryToCutCopy( type ) {
if ( CKEDITOR.env.ie )
return execIECommand( type );
// non-IEs part
try {
// Other browsers throw an error if the command is disabled.
return editor.document.$.execCommand( type, false, null );
} catch ( e ) {
return false;
}
}
this.type == 'cut' && fixCut();
var success = tryToCutCopy( this.type );
if ( !success ) {
// Show cutError or copyError.
editor.showNotification( editor.lang.clipboard[ this.type + 'Error' ] ); // jshint ignore:line
}
return success;
}
};
}
function createPasteCmd() {
return {
// Snapshots are done manually by editable.insertXXX methods.
canUndo: false,
async: true,
fakeKeystroke: CKEDITOR.CTRL + 86 /*V*/,
/**
* The default implementation of the paste command.
*
* @private
* @param {CKEDITOR.editor} editor An instance of the editor where the command is being executed.
* @param {Object/String} data If `data` is a string, then it is considered content that is being pasted.
* Otherwise it is treated as an object with options.
* @param {Boolean/String} [data.notification=true] Content for a notification shown after an unsuccessful
* paste attempt. If `false`, the notification will not be displayed. This parameter was added in 4.7.0.
* @param {String} [data.type='html'] The type of pasted content. There are two allowed values:
* * 'html'
* * 'text'
* @param {String/Object} data.dataValue Content being pasted. If this parameter is an object, it
* is supposed to be a `data` property of the {@link CKEDITOR.editor#paste} event.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer Data transfer instance connected
* with the current paste action.
* @member CKEDITOR.editor.commands.paste
*/
exec: function( editor, data ) {
data = typeof data !== 'undefined' && data !== null ? data : {};
var cmd = this,
notification = typeof data.notification !== 'undefined' ? data.notification : true,
forcedType = data.type,
keystroke = CKEDITOR.tools.keystrokeToString( editor.lang.common.keyboard,
editor.getCommandKeystroke( this ) ),
msg = typeof notification === 'string' ? notification : editor.lang.clipboard.pasteNotification
.replace( /%1/, '
' + keystroke.display + ' ' ),
pastedContent = typeof data === 'string' ? data : data.dataValue;
function callback( data, withBeforePaste ) {
withBeforePaste = typeof withBeforePaste !== 'undefined' ? withBeforePaste : true;
if ( data ) {
data.method = 'paste';
if ( !data.dataTransfer ) {
data.dataTransfer = clipboard.initPasteDataTransfer();
}
firePasteEvents( editor, data, withBeforePaste );
} else if ( notification && !editor._.forcePasteDialog ) {
editor.showNotification( msg, 'info', editor.config.clipboard_notificationDuration );
}
// Reset dialog mode (#595).
editor._.forcePasteDialog = false;
editor.fire( 'afterCommandExec', {
name: 'paste',
command: cmd,
returnValue: !!data
} );
}
// Force type for the next paste. Do not force if `config.forcePasteAsPlainText` set to true or 'allow-word' (#1013).
if ( forcedType && editor.config.forcePasteAsPlainText !== true && editor.config.forcePasteAsPlainText !== 'allow-word' ) {
editor._.nextPasteType = forcedType;
} else {
delete editor._.nextPasteType;
}
if ( typeof pastedContent === 'string' ) {
callback( {
dataValue: pastedContent
} );
} else {
editor.getClipboardData( callback );
}
}
};
}
function preventPasteEventNow() {
preventPasteEvent = 1;
// For safety reason we should wait longer than 0/1ms.
// We don't know how long execution of quite complex getClipboardData will take
// and in for example 'paste' listener execCommand() (which fires 'paste') is called
// after getClipboardData finishes.
// Luckily, it's impossible to immediately fire another 'paste' event we want to handle,
// because we only handle there native context menu and menu bar.
setTimeout( function() {
preventPasteEvent = 0;
}, 100 );
}
function preventBeforePasteEventNow() {
preventBeforePasteEvent = 1;
setTimeout( function() {
preventBeforePasteEvent = 0;
}, 10 );
}
// Tries to execute any of the paste, cut or copy commands in IE. Returns a
// boolean indicating that the operation succeeded.
// @param {String} command *LOWER CASED* name of command ('paste', 'cut', 'copy').
function execIECommand( command ) {
var doc = editor.document,
body = doc.getBody(),
enabled = false,
onExec = function() {
enabled = true;
};
// The following seems to be the only reliable way to detect that
// clipboard commands are enabled in IE. It will fire the
// onpaste/oncut/oncopy events only if the security settings allowed
// the command to execute.
body.on( command, onExec );
// IE7: document.execCommand has problem to paste into positioned element.
if ( CKEDITOR.env.version > 7 ) {
doc.$.execCommand( command );
} else {
doc.$.selection.createRange().execCommand( command );
}
body.removeListener( command, onExec );
return enabled;
}
// Cutting off control type element in IE standards breaks the selection entirely. (https://dev.ckeditor.com/ticket/4881)
function fixCut() {
if ( !CKEDITOR.env.ie || CKEDITOR.env.quirks )
return;
var sel = editor.getSelection(),
control, range, dummy;
if ( ( sel.getType() == CKEDITOR.SELECTION_ELEMENT ) && ( control = sel.getSelectedElement() ) ) {
range = sel.getRanges()[ 0 ];
dummy = editor.document.createText( '' );
dummy.insertBefore( control );
range.setStartBefore( dummy );
range.setEndAfter( control );
sel.selectRanges( [ range ] );
// Clear up the fix if the paste wasn't succeeded.
setTimeout( function() {
// Element still online?
if ( control.getParent() ) {
dummy.remove();
sel.selectElement( control );
}
}, 0 );
}
}
// Allow to peek clipboard content by redirecting the
// pasting content into a temporary bin and grab the content of it.
function getClipboardDataByPastebin( evt, callback ) {
var doc = editor.document,
editable = editor.editable(),
cancel = function( evt ) {
evt.cancel();
},
blurListener;
// Avoid recursions on 'paste' event or consequent paste too fast. (https://dev.ckeditor.com/ticket/5730)
if ( doc.getById( 'cke_pastebin' ) )
return;
var sel = editor.getSelection();
var bms = sel.createBookmarks();
// https://dev.ckeditor.com/ticket/11384. On IE9+ we use native selectionchange (i.e. editor#selectionCheck) to cache the most
// recent selection which we then lock on editable blur. See selection.js for more info.
// selectionchange fired before getClipboardDataByPastebin() cached selection
// before creating bookmark (cached selection will be invalid, because bookmarks modified the DOM),
// so we need to fire selectionchange one more time, to store current seleciton.
// Selection will be locked when we focus pastebin.
if ( CKEDITOR.env.ie )
sel.root.fire( 'selectionchange' );
// Create container to paste into.
// For rich content we prefer to use "body" since it holds
// the least possibility to be splitted by pasted content, while this may
// breaks the text selection on a frame-less editable, "div" would be
// the best one in that case.
// In another case on old IEs moving the selection into a "body" paste bin causes error panic.
// Body can't be also used for Opera which fills it with
// what is indistinguishable from pasted
(copying
in Opera isn't possible,
// but it can be copied from other browser).
var pastebin = new CKEDITOR.dom.element(
( CKEDITOR.env.webkit || editable.is( 'body' ) ) && !CKEDITOR.env.ie ? 'body' : 'div', doc );
pastebin.setAttributes( {
id: 'cke_pastebin',
'data-cke-temp': '1'
} );
var containerOffset = 0,
offsetParent,
win = doc.getWindow();
if ( CKEDITOR.env.webkit ) {
// It's better to paste close to the real paste destination, so inherited styles
// (which Webkits will try to compensate by styling span) differs less from the destination's one.
editable.append( pastebin );
// Style pastebin like .cke_editable, to minimize differences between origin and destination. (https://dev.ckeditor.com/ticket/9754)
pastebin.addClass( 'cke_editable' );
// Compensate position of offsetParent.
if ( !editable.is( 'body' ) ) {
// We're not able to get offsetParent from pastebin (body element), so check whether
// its parent (editable) is positioned.
if ( editable.getComputedStyle( 'position' ) != 'static' )
offsetParent = editable;
// And if not - safely get offsetParent from editable.
else
offsetParent = CKEDITOR.dom.element.get( editable.$.offsetParent );
containerOffset = offsetParent.getDocumentPosition().y;
}
} else {
// Opera and IE doesn't allow to append to html element.
editable.getAscendant( CKEDITOR.env.ie ? 'body' : 'html', 1 ).append( pastebin );
}
pastebin.setStyles( {
position: 'absolute',
// Position the bin at the top (+10 for safety) of viewport to avoid any subsequent document scroll.
top: ( win.getScrollPosition().y - containerOffset + 10 ) + 'px',
width: '1px',
// Caret has to fit in that height, otherwise browsers like Chrome & Opera will scroll window to show it.
// Set height equal to viewport's height - 20px (safety gaps), minimum 1px.
height: Math.max( 1, win.getViewPaneSize().height - 20 ) + 'px',
overflow: 'hidden',
// Reset styles that can mess up pastebin position.
margin: 0,
padding: 0
} );
// Paste fails in Safari when the body tag has 'user-select: none'. (https://dev.ckeditor.com/ticket/12506)
if ( CKEDITOR.env.safari )
pastebin.setStyles( CKEDITOR.tools.cssVendorPrefix( 'user-select', 'text' ) );
// Check if the paste bin now establishes new editing host.
var isEditingHost = pastebin.getParent().isReadOnly();
if ( isEditingHost ) {
// Hide the paste bin.
pastebin.setOpacity( 0 );
// And make it editable.
pastebin.setAttribute( 'contenteditable', true );
}
// Transparency is not enough since positioned non-editing host always shows
// resize handler, pull it off the screen instead.
else {
pastebin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-10000px' );
}
editor.on( 'selectionChange', cancel, null, null, 0 );
// Webkit fill fire blur on editable when moving selection to
// pastebin (if body is used). Cancel it because it causes incorrect
// selection lock in case of inline editor (https://dev.ckeditor.com/ticket/10644).
// The same seems to apply to Firefox (https://dev.ckeditor.com/ticket/10787).
if ( CKEDITOR.env.webkit || CKEDITOR.env.gecko )
blurListener = editable.once( 'blur', cancel, null, null, -100 );
// Temporarily move selection to the pastebin.
isEditingHost && pastebin.focus();
var range = new CKEDITOR.dom.range( pastebin );
range.selectNodeContents( pastebin );
var selPastebin = range.select();
// If non-native paste is executed, IE will open security alert and blur editable.
// Editable will then lock selection inside itself and after accepting security alert
// this selection will be restored. We overwrite stored selection, so it's restored
// in pastebin. (https://dev.ckeditor.com/ticket/9552)
if ( CKEDITOR.env.ie ) {
blurListener = editable.once( 'blur', function() {
editor.lockSelection( selPastebin );
} );
}
var scrollTop = CKEDITOR.document.getWindow().getScrollPosition().y;
// Wait a while and grab the pasted contents.
setTimeout( function() {
// Restore main window's scroll position which could have been changed
// by browser in cases described in https://dev.ckeditor.com/ticket/9771.
if ( CKEDITOR.env.webkit )
CKEDITOR.document.getBody().$.scrollTop = scrollTop;
// Blur will be fired only on non-native paste. In other case manually remove listener.
blurListener && blurListener.removeListener();
// Restore properly the document focus. (https://dev.ckeditor.com/ticket/8849)
if ( CKEDITOR.env.ie )
editable.focus();
// IE7: selection must go before removing pastebin. (https://dev.ckeditor.com/ticket/8691)
sel.selectBookmarks( bms );
pastebin.remove();
// Grab the HTML contents.
// We need to look for a apple style wrapper on webkit it also adds
// a div wrapper if you copy/paste the body of the editor.
// Remove hidden div and restore selection.
var bogusSpan;
if ( CKEDITOR.env.webkit && ( bogusSpan = pastebin.getFirst() ) && ( bogusSpan.is && bogusSpan.hasClass( 'Apple-style-span' ) ) )
pastebin = bogusSpan;
editor.removeListener( 'selectionChange', cancel );
callback( pastebin.getHtml() );
}, 0 );
}
// Try to get content directly on IE from clipboard, without native event
// being fired before. In other words - synthetically get clipboard data, if it's possible.
// mainPasteEvent will be fired, so if forced native paste:
// * worked, getClipboardDataByPastebin will grab it,
// * didn't work, dataValue and dataTransfer will be empty and editor#paste won't be fired.
// Clipboard data can be accessed directly only on IEs older than Edge.
// On other browsers we should fire beforePaste event and return false.
function getClipboardDataDirectly() {
if ( clipboard.mainPasteEvent == 'paste' ) {
editor.fire( 'beforePaste', { type: 'auto', method: 'paste' } );
return false;
}
// Prevent IE from pasting at the begining of the document.
editor.focus();
// Command will be handled by 'beforepaste', but as
// execIECommand( 'paste' ) will fire also 'paste' event
// we're canceling it.
preventPasteEventNow();
// https://dev.ckeditor.com/ticket/9247: Lock focus to prevent IE from hiding toolbar for inline editor.
var focusManager = editor.focusManager;
focusManager.lock();
if ( editor.editable().fire( clipboard.mainPasteEvent ) && !execIECommand( 'paste' ) ) {
focusManager.unlock();
return false;
}
focusManager.unlock();
return true;
}
// Listens for some clipboard related keystrokes, so they get customized.
// Needs to be bind to keydown event.
function onKey( event ) {
if ( editor.mode != 'wysiwyg' )
return;
switch ( event.data.keyCode ) {
// Paste
case CKEDITOR.CTRL + 86: // CTRL+V
case CKEDITOR.SHIFT + 45: // SHIFT+INS
var editable = editor.editable();
// Cancel 'paste' event because ctrl+v is for IE handled
// by 'beforepaste'.
preventPasteEventNow();
// Simulate 'beforepaste' event for all browsers using 'paste' as main event.
if ( clipboard.mainPasteEvent == 'paste' ) {
editable.fire( 'beforepaste' );
}
return;
// Cut
case CKEDITOR.CTRL + 88: // CTRL+X
case CKEDITOR.SHIFT + 46: // SHIFT+DEL
// Save Undo snapshot.
editor.fire( 'saveSnapshot' ); // Save before cut
setTimeout( function() {
editor.fire( 'saveSnapshot' ); // Save after cut
}, 50 ); // OSX is slow (https://dev.ckeditor.com/ticket/11416).
}
}
function pasteDataFromClipboard( evt ) {
// Default type is 'auto', but can be changed by beforePaste listeners.
var eventData = {
type: 'auto',
method: 'paste',
dataTransfer: clipboard.initPasteDataTransfer( evt )
};
eventData.dataTransfer.cacheData();
// Fire 'beforePaste' event so clipboard flavor get customized by other plugins.
// If 'beforePaste' is canceled continue executing getClipboardDataByPastebin and then do nothing
// (do not fire 'paste', 'afterPaste' events). This way we can grab all - synthetically
// and natively pasted content and prevent its insertion into editor
// after canceling 'beforePaste' event.
var beforePasteNotCanceled = editor.fire( 'beforePaste', eventData ) !== false;
// Do not use paste bin if the browser let us get HTML or files from dataTranfer.
if ( beforePasteNotCanceled && clipboard.canClipboardApiBeTrusted( eventData.dataTransfer, editor ) ) {
evt.data.preventDefault();
setTimeout( function() {
firePasteEvents( editor, eventData );
}, 0 );
} else {
getClipboardDataByPastebin( evt, function( data ) {
// Clean up.
eventData.dataValue = data.replace( /
]+data-cke-bookmark[^<]*?<\/span>/ig, '' );
// Fire remaining events (without beforePaste)
beforePasteNotCanceled && firePasteEvents( editor, eventData );
} );
}
}
function setToolbarStates() {
if ( editor.mode != 'wysiwyg' )
return;
var pasteState = stateFromNamedCommand( 'paste' );
editor.getCommand( 'cut' ).setState( stateFromNamedCommand( 'cut' ) );
editor.getCommand( 'copy' ).setState( stateFromNamedCommand( 'copy' ) );
editor.getCommand( 'paste' ).setState( pasteState );
editor.fire( 'pasteState', pasteState );
}
function stateFromNamedCommand( command ) {
var selection = editor.getSelection(),
range = selection && selection.getRanges()[ 0 ],
// We need to correctly update toolbar states on readOnly (#2775).
inReadOnly = editor.readOnly || ( range && range.checkReadOnly() );
if ( inReadOnly && command in { paste: 1, cut: 1 } ) {
return CKEDITOR.TRISTATE_DISABLED;
}
if ( command == 'paste' ) {
return CKEDITOR.TRISTATE_OFF;
}
// Cut, copy - check if the selection is not empty.
var sel = editor.getSelection(),
ranges = sel.getRanges(),
selectionIsEmpty = sel.getType() == CKEDITOR.SELECTION_NONE || ( ranges.length == 1 && ranges[ 0 ].collapsed );
return selectionIsEmpty ? CKEDITOR.TRISTATE_DISABLED : CKEDITOR.TRISTATE_OFF;
}
}
// Returns:
// * 'htmlifiedtext' if content looks like transformed by browser from plain text.
// See clipboard/paste.html TCs for more info.
// * 'html' if it is not 'htmlifiedtext'.
function recogniseContentType( data ) {
if ( CKEDITOR.env.webkit ) {
// Plain text or (
and text inside ).
if ( !data.match( /^[^<]*$/g ) && !data.match( /^(
<\/div>|
[^<]*<\/div>)*$/gi ) )
return 'html';
} else if ( CKEDITOR.env.ie ) {
// Text and
or ( text and
in
- paragraphs can be separated by new \r\n ).
if ( !data.match( /^([^<]| )*$/gi ) && !data.match( /^(
([^<]| )*<\/p>|(\r\n))*$/gi ) )
return 'html';
} else if ( CKEDITOR.env.gecko ) {
// Text or .
if ( !data.match( /^([^<]| )*$/gi ) )
return 'html';
} else {
return 'html';
}
return 'htmlifiedtext';
}
// This function transforms what browsers produce when
// pasting plain text into editable element (see clipboard/paste.html TCs
// for more info) into correct HTML (similar to that produced by text2Html).
function htmlifiedTextHtmlification( config, data ) {
function repeatParagraphs( repeats ) {
// Repeat blocks floor((n+1)/2) times.
// Even number of repeats - add at the beginning of last
.
return CKEDITOR.tools.repeat( '
', ~~( repeats / 2 ) ) + ( repeats % 2 == 1 ? ' ' : '' );
}
// Replace adjacent white-spaces (EOLs too - Fx sometimes keeps them) with one space.
// We have to skip \u3000 (IDEOGRAPHIC SPACE) character - it's special space character correctly rendered by the browsers (#1321).
data = data.replace( /(?!\u3000)\s+/g, ' ' )
// Remove spaces from between tags.
.replace( /> +<' )
// Normalize XHTML syntax and upper cased tags.
.replace( / /gi, ' ' );
// IE - lower cased tags.
data = data.replace( /<\/?[A-Z]+>/g, function( match ) {
return match.toLowerCase();
} );
// Don't touch single lines (no ) - nothing to do here.
if ( data.match( /^[^<]$/ ) )
return data;
// Webkit.
if ( CKEDITOR.env.webkit && data.indexOf( '
' ) > -1 ) {
// One line break at the beginning - insert
data = data.replace( /^(
(
|)<\/div>)(?!$|(
(
|)<\/div>))/g, '
' )
// Two or more - reduce number of new lines by one.
.replace( /^(
(
|)<\/div>){2}(?!$)/g, '
' );
// Two line breaks create one paragraph in Webkit.
if ( data.match( /
(
|)<\/div>/ ) ) {
data = '
' + data.replace( /(
( |)<\/div>)+/g, function( match ) {
return repeatParagraphs( match.split( '
' ).length + 1 );
} ) + '';
}
// One line break create br.
data = data.replace( /<\/div>
/g, '
' );
// Remove remaining divs.
data = data.replace( /<\/?div>/g, '' );
}
// Opera and Firefox and enterMode != BR.
if ( CKEDITOR.env.gecko && config.enterMode != CKEDITOR.ENTER_BR ) {
// Remove bogus
- Fx generates two
for one line break.
// For two line breaks it still produces two , but it's better to ignore this case than the first one.
if ( CKEDITOR.env.gecko )
data = data.replace( /^ $/, ' ' );
// This line satisfy edge case when for Opera we have two line breaks
//data = data.replace( /)
if ( data.indexOf( ' ' ) > -1 ) {
// Two line breaks create one paragraph, three - 2, four - 3, etc.
data = '' + data.replace( /( ){2,}/g, function( match ) {
return repeatParagraphs( match.length / 4 );
} ) + '
';
}
}
return switchEnterMode( config, data );
}
function filtersFactoryFactory( editor ) {
var filters = {};
function setUpTags() {
var tags = {};
for ( var tag in CKEDITOR.dtd ) {
if ( tag.charAt( 0 ) != '$' && tag != 'div' && tag != 'span' ) {
tags[ tag ] = 1;
}
}
return tags;
}
function createSemanticContentFilter() {
var filter = new CKEDITOR.filter( editor, {} );
filter.allow( {
$1: {
elements: setUpTags(),
attributes: true,
styles: false,
classes: false
}
} );
return filter;
}
return {
get: function( type ) {
if ( type == 'plain-text' ) {
// Does this look confusing to you? Did we forget about enter mode?
// It is a trick that let's us creating one filter for edidtor, regardless of its
// activeEnterMode (which as the name indicates can change during runtime).
//
// How does it work?
// The active enter mode is passed to the filter.applyTo method.
// The filter first marks all elements except as disallowed and then tries to remove
// them. However, it cannot remove e.g. a element completely, because it's a basic structural element,
// so it tries to replace it with an element created based on the active enter mode, eventually doing nothing.
//
// Now you can sleep well.
return filters.plainText || ( filters.plainText = new CKEDITOR.filter( editor, 'br' ) );
} else if ( type == 'semantic-content' ) {
return filters.semanticContent || ( filters.semanticContent = createSemanticContentFilter() );
} else if ( type ) {
// Create filter based on rules (string or object).
return new CKEDITOR.filter( editor, type );
}
return null;
}
};
}
function filterContent( editor, data, filter ) {
var fragment = CKEDITOR.htmlParser.fragment.fromHtml( data ),
writer = new CKEDITOR.htmlParser.basicWriter();
filter.applyTo( fragment, true, false, editor.activeEnterMode );
fragment.writeHtml( writer );
return writer.getHtml();
}
function switchEnterMode( config, data ) {
if ( config.enterMode == CKEDITOR.ENTER_BR ) {
data = data.replace( /(<\/p>
)+/g, function( match ) {
return CKEDITOR.tools.repeat( ' ', match.length / 7 * 2 );
} ).replace( /<\/?p>/g, '' );
} else if ( config.enterMode == CKEDITOR.ENTER_DIV ) {
data = data.replace( /<(\/)?p>/g, '<$1div>' );
}
return data;
}
function preventDefaultSetDropEffectToNone( evt ) {
evt.data.preventDefault();
evt.data.$.dataTransfer.dropEffect = 'none';
}
function initDragDrop( editor ) {
var clipboard = CKEDITOR.plugins.clipboard;
editor.on( 'contentDom', function() {
var editable = editor.editable(),
dropTarget = CKEDITOR.plugins.clipboard.getDropTarget( editor ),
top = editor.ui.space( 'top' ),
bottom = editor.ui.space( 'bottom' );
// -------------- DRAGOVER TOP & BOTTOM --------------
// Not allowing dragging on toolbar and bottom (https://dev.ckeditor.com/ticket/12613).
clipboard.preventDefaultDropOnElement( top );
clipboard.preventDefaultDropOnElement( bottom );
// -------------- DRAGSTART --------------
// Listed on dragstart to mark internal and cross-editor drag & drop
// and save range and selected HTML.
editable.attachListener( dropTarget, 'dragstart', fireDragEvent );
// Make sure to reset data transfer (in case dragend was not called or was canceled).
editable.attachListener( editor, 'dragstart', clipboard.resetDragDataTransfer, clipboard, null, 1 );
// Create a dataTransfer object and save it globally.
editable.attachListener( editor, 'dragstart', function( evt ) {
clipboard.initDragDataTransfer( evt, editor );
}, null, null, 2 );
editable.attachListener( editor, 'dragstart', function() {
// Save drag range globally for cross editor D&D.
var dragRange = clipboard.dragRange = editor.getSelection().getRanges()[ 0 ];
// Store number of children, so we can later tell if any text node was split on drop. (https://dev.ckeditor.com/ticket/13011, https://dev.ckeditor.com/ticket/13447)
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 10 ) {
clipboard.dragStartContainerChildCount = dragRange ? getContainerChildCount( dragRange.startContainer ) : null;
clipboard.dragEndContainerChildCount = dragRange ? getContainerChildCount( dragRange.endContainer ) : null;
}
}, null, null, 100 );
// -------------- DRAGEND --------------
// Clean up on dragend.
editable.attachListener( dropTarget, 'dragend', fireDragEvent );
// Init data transfer if someone wants to use it in dragend.
editable.attachListener( editor, 'dragend', clipboard.initDragDataTransfer, clipboard, null, 1 );
// When drag & drop is done we need to reset dataTransfer so the future
// external drop will be not recognize as internal.
editable.attachListener( editor, 'dragend', clipboard.resetDragDataTransfer, clipboard, null, 100 );
// -------------- DRAGOVER --------------
// We need to call preventDefault on dragover because otherwise if
// we drop image it will overwrite document.
editable.attachListener( dropTarget, 'dragover', function( evt ) {
// Edge requires this handler to have `preventDefault()` regardless of the situation.
if ( CKEDITOR.env.edge ) {
evt.data.preventDefault();
return;
}
var target = evt.data.getTarget();
// Prevent reloading page when dragging image on empty document (https://dev.ckeditor.com/ticket/12619).
if ( target && target.is && target.is( 'html' ) ) {
evt.data.preventDefault();
return;
}
// If we do not prevent default dragover on IE the file path
// will be loaded and we will lose content. On the other hand
// if we prevent it the cursor will not we shown, so we prevent
// dragover only on IE, on versions which support file API and only
// if the event contains files.
if ( CKEDITOR.env.ie &&
CKEDITOR.plugins.clipboard.isFileApiSupported &&
evt.data.$.dataTransfer.types.contains( 'Files' ) ) {
evt.data.preventDefault();
}
} );
// -------------- DROP --------------
editable.attachListener( dropTarget, 'drop', function( evt ) {
// Do nothing if event was already prevented. (https://dev.ckeditor.com/ticket/13879)
if ( evt.data.$.defaultPrevented ) {
return;
}
// Cancel native drop.
evt.data.preventDefault();
var target = evt.data.getTarget(),
readOnly = target.isReadOnly();
// Do nothing if drop on non editable element (https://dev.ckeditor.com/ticket/13015).
// The tag isn't editable (body is), but we want to allow drop on it
// (so it is possible to drop below editor contents).
if ( readOnly && !( target.type == CKEDITOR.NODE_ELEMENT && target.is( 'html' ) ) ) {
return;
}
// Getting drop position is one of the most complex parts.
var dropRange = clipboard.getRangeAtDropPosition( evt, editor ),
dragRange = clipboard.dragRange;
// Do nothing if it was not possible to get drop range.
if ( !dropRange ) {
return;
}
// Fire drop.
fireDragEvent( evt, dragRange, dropRange );
}, null, null, 9999 );
// Create dataTransfer or get it, if it was created before.
editable.attachListener( editor, 'drop', clipboard.initDragDataTransfer, clipboard, null, 1 );
// Execute drop action, fire paste.
editable.attachListener( editor, 'drop', function( evt ) {
var data = evt.data;
if ( !data ) {
return;
}
// Let user modify drag and drop range.
var dropRange = data.dropRange,
dragRange = data.dragRange,
dataTransfer = data.dataTransfer;
if ( dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_INTERNAL ) {
// Execute drop with a timeout because otherwise selection, after drop,
// on IE is in the drag position, instead of drop position.
setTimeout( function() {
clipboard.internalDrop( dragRange, dropRange, dataTransfer, editor );
}, 0 );
} else if ( dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_CROSS_EDITORS ) {
crossEditorDrop( dragRange, dropRange, dataTransfer );
} else {
externalDrop( dropRange, dataTransfer );
}
}, null, null, 9999 );
// Cross editor drag and drop (drag in one Editor and drop in the other).
function crossEditorDrop( dragRange, dropRange, dataTransfer ) {
// Paste event should be fired before delete contents because otherwise
// Chrome have a problem with drop range (Chrome split the drop
// range container so the offset is bigger then container length).
dropRange.select();
firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop' }, 1 );
// Remove dragged content and make a snapshot.
dataTransfer.sourceEditor.fire( 'saveSnapshot' );
dataTransfer.sourceEditor.editable().extractHtmlFromRange( dragRange );
// Make some selection before saving snapshot, otherwise error will be thrown, because
// there will be no valid selection after content is removed.
dataTransfer.sourceEditor.getSelection().selectRanges( [ dragRange ] );
dataTransfer.sourceEditor.fire( 'saveSnapshot' );
}
// Drop from external source.
function externalDrop( dropRange, dataTransfer ) {
// Paste content into the drop position.
dropRange.select();
firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop' }, 1 );
// Usually we reset DataTranfer on dragend,
// but dragend is called on the same element as dragstart
// so it will not be called on on external drop.
clipboard.resetDragDataTransfer();
}
// Fire drag/drop events (dragstart, dragend, drop).
function fireDragEvent( evt, dragRange, dropRange ) {
var eventData = {
$: evt.data.$,
target: evt.data.getTarget()
};
if ( dragRange ) {
eventData.dragRange = dragRange;
}
if ( dropRange ) {
eventData.dropRange = dropRange;
}
if ( editor.fire( evt.name, eventData ) === false ) {
evt.data.preventDefault();
}
}
function getContainerChildCount( container ) {
if ( container.type != CKEDITOR.NODE_ELEMENT ) {
container = container.getParent();
}
return container.getChildCount();
}
} );
}
/**
* @singleton
* @class CKEDITOR.plugins.clipboard
*/
CKEDITOR.plugins.clipboard = {
/**
* True if the environment allows to set data on copy or cut manually. This value is false in IE, because this browser
* shows the security dialog window when the script tries to set clipboard data and on iOS, because custom data is
* not saved to clipboard there.
*
* @since 4.5.0
* @readonly
* @property {Boolean}
*/
isCustomCopyCutSupported: ( !CKEDITOR.env.ie || CKEDITOR.env.version >= 16 ) && !CKEDITOR.env.iOS,
/**
* True if the environment supports MIME types and custom data types in dataTransfer/cliboardData getData/setData methods.
*
* @since 4.5.0
* @readonly
* @property {Boolean}
*/
isCustomDataTypesSupported: !CKEDITOR.env.ie || CKEDITOR.env.version >= 16,
/**
* True if the environment supports File API.
*
* @since 4.5.0
* @readonly
* @property {Boolean}
*/
isFileApiSupported: !CKEDITOR.env.ie || CKEDITOR.env.version > 9,
/**
* Main native paste event editable should listen to.
*
* **Note:** Safari does not like the {@link CKEDITOR.editor#beforePaste} event — it sometimes does not
* handle Ctrl+C properly. This is probably caused by some race condition between events.
* Chrome, Firefox and Edge work well with both events, so it is better to use {@link CKEDITOR.editor#paste}
* which will handle pasting from e.g. browsers' menu bars.
* IE7/8 does not like the {@link CKEDITOR.editor#paste} event for which it is throwing random errors.
*
* @since 4.5.0
* @readonly
* @property {String}
*/
mainPasteEvent: ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) ? 'beforepaste' : 'paste',
/**
* Adds a new paste button to the editor.
*
* This method should be called for buttons that should display the Paste Dialog fallback in mobile environments.
* See [the rationale](https://github.com/ckeditor/ckeditor-dev/issues/595#issuecomment-345971174) for more
* details.
*
* @since 4.9.0
* @param {CKEDITOR.editor} editor The editor instance.
* @param {String} name Name of the button.
* @param {Object} definition Definition of the button.
*/
addPasteButton: function( editor, name, definition ) {
if ( !editor.ui.addButton ) {
return;
}
editor.ui.addButton( name, definition );
if ( !editor._.pasteButtons ) {
editor._.pasteButtons = [];
}
editor._.pasteButtons.push( name );
},
/**
* Returns `true` if it is expected that a browser provides HTML data through the Clipboard API.
* If not, this method returns `false` and as a result CKEditor will use the paste bin. Read more in
* the [Clipboard Integration](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_clipboard.html#clipboard-api) guide.
*
* @since 4.5.2
* @returns {Boolean}
*/
canClipboardApiBeTrusted: function( dataTransfer, editor ) {
// If it's an internal or cross-editor data transfer, then it means that custom cut/copy/paste support works
// and that the data were put manually on the data transfer so we can be sure that it's available.
if ( dataTransfer.getTransferType( editor ) != CKEDITOR.DATA_TRANSFER_EXTERNAL ) {
return true;
}
// In Chrome we can trust Clipboard API, with the exception of Chrome on Android (in both - mobile and desktop modes), where
// clipboard API is not available so we need to check it (https://dev.ckeditor.com/ticket/13187).
if ( CKEDITOR.env.chrome && !dataTransfer.isEmpty() ) {
return true;
}
// Because of a Firefox bug HTML data are not available in some cases (e.g. paste from Word), in such cases we
// need to use the pastebin (https://dev.ckeditor.com/ticket/13528, https://bugzilla.mozilla.org/show_bug.cgi?id=1183686).
if ( CKEDITOR.env.gecko && ( dataTransfer.getData( 'text/html' ) || dataTransfer.getFilesCount() ) ) {
return true;
}
// Safari fixed clipboard in 10.1 (https://bugs.webkit.org/show_bug.cgi?id=19893) (https://dev.ckeditor.com/ticket/16982).
// However iOS version still doesn't work well enough (https://bugs.webkit.org/show_bug.cgi?id=19893#c34).
if ( CKEDITOR.env.safari && CKEDITOR.env.version >= 603 && !CKEDITOR.env.iOS ) {
return true;
}
// Edge 15 added support for Clipboard API
// (https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/6515107-clipboard-api), however it is
// usable for our case starting from Edge 16 (#468).
if ( CKEDITOR.env.edge && CKEDITOR.env.version >= 16 ) {
return true;
}
// In older Safari and IE HTML data is not available through the Clipboard API.
// In older Edge version things are also a bit messy -
// https://connect.microsoft.com/IE/feedback/details/1572456/edge-clipboard-api-text-html-content-messed-up-in-event-clipboarddata
// It is safer to use the paste bin in unknown cases.
return false;
},
/**
* Returns the element that should be used as the target for the drop event.
*
* @since 4.5.0
* @param {CKEDITOR.editor} editor The editor instance.
* @returns {CKEDITOR.dom.domObject} the element that should be used as the target for the drop event.
*/
getDropTarget: function( editor ) {
var editable = editor.editable();
// https://dev.ckeditor.com/ticket/11123 Firefox needs to listen on document, because otherwise event won't be fired.
// https://dev.ckeditor.com/ticket/11086 IE8 cannot listen on document.
if ( ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) || editable.isInline() ) {
return editable;
} else {
return editor.document;
}
},
/**
* IE 8 & 9 split text node on drop so the first node contains the
* text before the drop position and the second contains the rest. If you
* drag the content from the same node you will be not be able to get
* it (the range becomes invalid), so you need to join them back.
*
* Note that the first node in IE 8 & 9 is the original node object
* but with shortened content.
*
* Before:
* --- Text Node A ----------------------------------
* /\
* Drag position
*
* After (IE 8 & 9):
* --- Text Node A ----- --- Text Node B -----------
* /\ /\
* Drop position Drag position
* (invalid)
*
* After (other browsers):
* --- Text Node A ----------------------------------
* /\ /\
* Drop position Drag position
*
* **Note:** This function is in the public scope for tests usage only.
*
* @since 4.5.0
* @private
* @param {CKEDITOR.dom.range} dragRange The drag range.
* @param {CKEDITOR.dom.range} dropRange The drop range.
* @param {Number} preDragStartContainerChildCount The number of children of the drag range start container before the drop.
* @param {Number} preDragEndContainerChildCount The number of children of the drag range end container before the drop.
*/
fixSplitNodesAfterDrop: function( dragRange, dropRange, preDragStartContainerChildCount, preDragEndContainerChildCount ) {
var dropContainer = dropRange.startContainer;
if (
typeof preDragEndContainerChildCount != 'number' ||
typeof preDragStartContainerChildCount != 'number'
) {
return;
}
// We are only concerned about ranges anchored in elements.
if ( dropContainer.type != CKEDITOR.NODE_ELEMENT ) {
return;
}
if ( handleContainer( dragRange.startContainer, dropContainer, preDragStartContainerChildCount ) ) {
return;
}
if ( handleContainer( dragRange.endContainer, dropContainer, preDragEndContainerChildCount ) ) {
return;
}
function handleContainer( dragContainer, dropContainer, preChildCount ) {
var dragElement = dragContainer;
if ( dragElement.type == CKEDITOR.NODE_TEXT ) {
dragElement = dragContainer.getParent();
}
if ( dragElement.equals( dropContainer ) && preChildCount != dropContainer.getChildCount() ) {
applyFix( dropRange );
return true;
}
}
function applyFix( dropRange ) {
var nodeBefore = dropRange.startContainer.getChild( dropRange.startOffset - 1 ),
nodeAfter = dropRange.startContainer.getChild( dropRange.startOffset );
if (
nodeBefore && nodeBefore.type == CKEDITOR.NODE_TEXT &&
nodeAfter && nodeAfter.type == CKEDITOR.NODE_TEXT
) {
var offset = nodeBefore.getLength();
nodeBefore.setText( nodeBefore.getText() + nodeAfter.getText() );
nodeAfter.remove();
dropRange.setStart( nodeBefore, offset );
dropRange.collapse( true );
}
}
},
/**
* Checks whether turning the drag range into bookmarks will invalidate the drop range.
* This usually happens when the drop range shares the container with the drag range and is
* located after the drag range, but there are countless edge cases.
*
* This function is stricly related to {@link #internalDrop} which toggles
* order in which it creates bookmarks for both ranges based on a value returned
* by this method. In some cases this method returns a value which is not necessarily
* true in terms of what it was meant to check, but it is convenient, because
* we know how it is interpreted in {@link #internalDrop}, so the correct
* behavior of the entire algorithm is assured.
*
* **Note:** This function is in the public scope for tests usage only.
*
* @since 4.5.0
* @private
* @param {CKEDITOR.dom.range} dragRange The first range to compare.
* @param {CKEDITOR.dom.range} dropRange The second range to compare.
* @returns {Boolean} `true` if the first range is before the second range.
*/
isDropRangeAffectedByDragRange: function( dragRange, dropRange ) {
var dropContainer = dropRange.startContainer,
dropOffset = dropRange.endOffset;
// Both containers are the same and drop offset is at the same position or later.
// " A L] A " " M A "
// ^ ^
if ( dragRange.endContainer.equals( dropContainer ) && dragRange.endOffset <= dropOffset ) {
return true;
}
// Bookmark for drag start container will mess up with offsets.
// " O [L A " " M A "
// ^ ^
if (
dragRange.startContainer.getParent().equals( dropContainer ) &&
dragRange.startContainer.getIndex() < dropOffset
) {
return true;
}
// Bookmark for drag end container will mess up with offsets.
// " O] L A " " M A "
// ^ ^
if (
dragRange.endContainer.getParent().equals( dropContainer ) &&
dragRange.endContainer.getIndex() < dropOffset
) {
return true;
}
return false;
},
/**
* Internal drag and drop (drag and drop in the same editor instance).
*
* **Note:** This function is in the public scope for tests usage only.
*
* @since 4.5.0
* @private
* @param {CKEDITOR.dom.range} dragRange The first range to compare.
* @param {CKEDITOR.dom.range} dropRange The second range to compare.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} dataTransfer
* @param {CKEDITOR.editor} editor
*/
internalDrop: function( dragRange, dropRange, dataTransfer, editor ) {
var clipboard = CKEDITOR.plugins.clipboard,
editable = editor.editable(),
dragBookmark, dropBookmark, isDropRangeAffected;
// Save and lock snapshot so there will be only
// one snapshot for both remove and insert content.
editor.fire( 'saveSnapshot' );
editor.fire( 'lockSnapshot', { dontUpdate: 1 } );
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 10 ) {
this.fixSplitNodesAfterDrop(
dragRange,
dropRange,
clipboard.dragStartContainerChildCount,
clipboard.dragEndContainerChildCount
);
}
// Because we manipulate multiple ranges we need to do it carefully,
// changing one range (event creating a bookmark) may make other invalid.
// We need to change ranges into bookmarks so we can manipulate them easily in the future.
// We can change the range which is later in the text before we change the preceding range.
// We call isDropRangeAffectedByDragRange to test the order of ranges.
isDropRangeAffected = this.isDropRangeAffectedByDragRange( dragRange, dropRange );
if ( !isDropRangeAffected ) {
dragBookmark = dragRange.createBookmark( false );
}
dropBookmark = dropRange.clone().createBookmark( false );
if ( isDropRangeAffected ) {
dragBookmark = dragRange.createBookmark( false );
}
// Check if drop range is inside range.
// This is an edge case when we drop something on editable's margin/padding.
// That space is not treated as a part of the range we drag, so it is possible to drop there.
// When we drop, browser tries to find closest drop position and it finds it inside drag range. (https://dev.ckeditor.com/ticket/13453)
var startNode = dragBookmark.startNode,
endNode = dragBookmark.endNode,
dropNode = dropBookmark.startNode,
dropInsideDragRange =
// Must check endNode because dragRange could be collapsed in some edge cases (simulated DnD).
endNode &&
( startNode.getPosition( dropNode ) & CKEDITOR.POSITION_PRECEDING ) &&
( endNode.getPosition( dropNode ) & CKEDITOR.POSITION_FOLLOWING );
// If the drop range happens to be inside drag range change it's position to the beginning of the drag range.
if ( dropInsideDragRange ) {
// We only change position of bookmark span that is connected with dropBookmark.
// dropRange will be overwritten and set to the dropBookmark later.
dropNode.insertBefore( startNode );
}
// No we can safely delete content for the drag range...
dragRange = editor.createRange();
dragRange.moveToBookmark( dragBookmark );
editable.extractHtmlFromRange( dragRange, 1 );
// ...and paste content into the drop position.
dropRange = editor.createRange();
// Get actual selection with bookmarks if drop's bookmark are not in editable any longer.
// This might happen after extracting content from range (#2292).
if ( !dropBookmark.startNode.getCommonAncestor( editable ) ) {
dropBookmark = editor.getSelection().createBookmarks()[ 0 ];
}
dropRange.moveToBookmark( dropBookmark );
// We do not select drop range, because of may be in the place we can not set the selection
// (e.g. between blocks, in case of block widget D&D). We put range to the paste event instead.
firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop', range: dropRange }, 1 );
editor.fire( 'unlockSnapshot' );
},
/**
* Gets the range from the `drop` event.
*
* @since 4.5.0
* @param {Object} domEvent A native DOM drop event object.
* @param {CKEDITOR.editor} editor The source editor instance.
* @returns {CKEDITOR.dom.range} range at drop position.
*/
getRangeAtDropPosition: function( dropEvt, editor ) {
var $evt = dropEvt.data.$,
x = $evt.clientX,
y = $evt.clientY,
$range,
defaultRange = editor.getSelection( true ).getRanges()[ 0 ],
range = editor.createRange();
// Make testing possible.
if ( dropEvt.data.testRange )
return dropEvt.data.testRange;
// Webkits.
if ( document.caretRangeFromPoint && editor.document.$.caretRangeFromPoint( x, y ) ) {
$range = editor.document.$.caretRangeFromPoint( x, y );
range.setStart( CKEDITOR.dom.node( $range.startContainer ), $range.startOffset );
range.collapse( true );
}
// FF.
else if ( $evt.rangeParent ) {
range.setStart( CKEDITOR.dom.node( $evt.rangeParent ), $evt.rangeOffset );
range.collapse( true );
}
// IEs 9+.
// We check if editable is focused to make sure that it's an internal DnD. External DnD must use the second
// mechanism because of https://dev.ckeditor.com/ticket/13472#comment:6.
else if ( CKEDITOR.env.ie && CKEDITOR.env.version > 8 && defaultRange && editor.editable().hasFocus ) {
// On IE 9+ range by default is where we expected it.
// defaultRange may be undefined if dragover was canceled (file drop).
return defaultRange;
}
// IE 8 and all IEs if !defaultRange or external DnD.
else if ( document.body.createTextRange ) {
// To use this method we need a focus (which may be somewhere else in case of external drop).
editor.focus();
$range = editor.document.getBody().$.createTextRange();
try {
var sucess = false;
// If user drop between text line IEs moveToPoint throws exception:
//
// Lorem ipsum pulvinar purus et euismod
//
// dolor sit amet,| consectetur adipiscing
// *
// vestibulum tincidunt augue eget tempus.
//
// * - drop position
// | - expected cursor position
//
// So we try to call moveToPoint with +-1px up to +-20px above or
// below original drop position to find nearest good drop position.
for ( var i = 0; i < 20 && !sucess; i++ ) {
if ( !sucess ) {
try {
$range.moveToPoint( x, y - i );
sucess = true;
} catch ( err ) {
}
}
if ( !sucess ) {
try {
$range.moveToPoint( x, y + i );
sucess = true;
} catch ( err ) {
}
}
}
if ( sucess ) {
var id = 'cke-temp-' + ( new Date() ).getTime();
$range.pasteHTML( '\u200b ' );
var span = editor.document.getById( id );
range.moveToPosition( span, CKEDITOR.POSITION_BEFORE_START );
span.remove();
} else {
// If the fist method does not succeed we might be next to
// the short element (like header):
//
// Lorem ipsum pulvinar purus et euismod.
//
//
// SOME HEADER| *
//
//
// vestibulum tincidunt augue eget tempus.
//
// * - drop position
// | - expected cursor position
//
// In such situation elementFromPoint returns proper element. Using getClientRect
// it is possible to check if the cursor should be at the beginning or at the end
// of paragraph.
var $element = editor.document.$.elementFromPoint( x, y ),
element = new CKEDITOR.dom.element( $element ),
rect;
if ( !element.equals( editor.editable() ) && element.getName() != 'html' ) {
rect = element.getClientRect();
if ( x < rect.left ) {
range.setStartAt( element, CKEDITOR.POSITION_AFTER_START );
range.collapse( true );
} else {
range.setStartAt( element, CKEDITOR.POSITION_BEFORE_END );
range.collapse( true );
}
}
// If drop happens on no element elementFromPoint returns html or body.
//
// * |Lorem ipsum pulvinar purus et euismod.
//
// vestibulum tincidunt augue eget tempus.
//
// * - drop position
// | - expected cursor position
//
// In such case we can try to use default selection. If startContainer is not
// 'editable' element it is probably proper selection.
else if ( defaultRange && defaultRange.startContainer &&
!defaultRange.startContainer.equals( editor.editable() ) ) {
return defaultRange;
// Otherwise we can not find any drop position and we have to return null
// and cancel drop event.
} else {
return null;
}
}
} catch ( err ) {
return null;
}
} else {
return null;
}
return range;
},
/**
* This function tries to link the `evt.data.dataTransfer` property of the {@link CKEDITOR.editor#dragstart},
* {@link CKEDITOR.editor#dragend} and {@link CKEDITOR.editor#drop} events to a single
* {@link CKEDITOR.plugins.clipboard.dataTransfer} object.
*
* This method is automatically used by the core of the drag and drop functionality and
* usually does not have to be called manually when using the drag and drop events.
*
* This method behaves differently depending on whether the drag and drop events were fired
* artificially (to represent a non-native drag and drop) or whether they were caused by the native drag and drop.
*
* If the native event is not available, then it will create a new {@link CKEDITOR.plugins.clipboard.dataTransfer}
* instance (if it does not exist already) and will link it to this and all following event objects until
* the {@link #resetDragDataTransfer} method is called. It means that all three drag and drop events must be fired
* in order to ensure that the data transfer is bound correctly.
*
* If the native event is available, then the {@link CKEDITOR.plugins.clipboard.dataTransfer} is identified
* by its ID and a new instance is assigned to the `evt.data.dataTransfer` only if the ID changed or
* the {@link #resetDragDataTransfer} method was called.
*
* @since 4.5.0
* @param {CKEDITOR.dom.event} [evt] A drop event object.
* @param {CKEDITOR.editor} [sourceEditor] The source editor instance.
*/
initDragDataTransfer: function( evt, sourceEditor ) {
// Create a new dataTransfer object based on the drop event.
// If this event was used on dragstart to create dataTransfer
// both dataTransfer objects will have the same id.
var nativeDataTransfer = evt.data.$ ? evt.data.$.dataTransfer : null,
dataTransfer = new this.dataTransfer( nativeDataTransfer, sourceEditor );
// Set dataTransfer.id only for 'dragstart' event (so for events initializing dataTransfer inside editor) (#962).
if ( evt.name === 'dragstart' ) {
dataTransfer.storeId();
}
if ( !nativeDataTransfer ) {
// No native event.
if ( this.dragData ) {
dataTransfer = this.dragData;
} else {
this.dragData = dataTransfer;
}
} else {
// Native event. If there is the same id we will replace dataTransfer with the one
// created on drag, because it contains drag editor, drag content and so on.
// Otherwise (in case of drag from external source) we save new object to
// the global clipboard.dragData.
if ( this.dragData && dataTransfer.id == this.dragData.id ) {
dataTransfer = this.dragData;
} else {
this.dragData = dataTransfer;
}
}
evt.data.dataTransfer = dataTransfer;
},
/**
* Removes the global {@link #dragData} so the next call to {@link #initDragDataTransfer}
* always creates a new instance of {@link CKEDITOR.plugins.clipboard.dataTransfer}.
*
* @since 4.5.0
*/
resetDragDataTransfer: function() {
this.dragData = null;
},
/**
* Global object storing the data transfer of the current drag and drop operation.
* Do not use it directly, use {@link #initDragDataTransfer} and {@link #resetDragDataTransfer}.
*
* Note: This object is global (meaning that it is not related to a single editor instance)
* in order to handle drag and drop from one editor into another.
*
* @since 4.5.0
* @private
* @property {CKEDITOR.plugins.clipboard.dataTransfer} dragData
*/
/**
* Range object to save the drag range and remove its content after the drop.
*
* @since 4.5.0
* @private
* @property {CKEDITOR.dom.range} dragRange
*/
/**
* Initializes and links data transfer objects based on the paste event. If the data
* transfer object was already initialized on this event, the function will
* return that object. In IE it is not possible to link copy/cut and paste events
* so the method always returns a new object. The same happens if there is no paste event
* passed to the method.
*
* @since 4.5.0
* @param {CKEDITOR.dom.event} [evt] A paste event object.
* @param {CKEDITOR.editor} [sourceEditor] The source editor instance.
* @returns {CKEDITOR.plugins.clipboard.dataTransfer} The data transfer object.
*/
initPasteDataTransfer: function( evt, sourceEditor ) {
if ( !this.isCustomCopyCutSupported ) {
// Edge < 16 does not support custom copy/cut, but it has some useful data in the clipboardData (https://dev.ckeditor.com/ticket/13755).
return new this.dataTransfer( ( CKEDITOR.env.edge && evt && evt.data.$ && evt.data.$.clipboardData ) || null, sourceEditor );
} else if ( evt && evt.data && evt.data.$ ) {
var clipboardData = evt.data.$.clipboardData,
dataTransfer = new this.dataTransfer( clipboardData, sourceEditor );
// Set dataTransfer.id only for 'copy'/'cut' events (so for events initializing dataTransfer inside editor) (#962).
if ( evt.name === 'copy' || evt.name === 'cut' ) {
dataTransfer.storeId();
}
if ( this.copyCutData && dataTransfer.id == this.copyCutData.id ) {
dataTransfer = this.copyCutData;
dataTransfer.$ = clipboardData;
} else {
this.copyCutData = dataTransfer;
}
return dataTransfer;
} else {
return new this.dataTransfer( null, sourceEditor );
}
},
/**
* Prevents dropping on the specified element.
*
* @since 4.5.0
* @param {CKEDITOR.dom.element} element The element on which dropping should be disabled.
*/
preventDefaultDropOnElement: function( element ) {
element && element.on( 'dragover', preventDefaultSetDropEffectToNone );
}
};
// Data type used to link drag and drop events.
//
// In IE URL data type is buggie and there is no way to mark drag & drop without
// modifying text data (which would be displayed if user drop content to the textarea)
// so we just read dragged text.
//
// In Chrome and Firefox we can use custom data types.
clipboardIdDataType = CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ? 'cke/id' : 'Text';
/**
* Facade for the native `dataTransfer`/`clipboadData` object to hide all differences
* between browsers.
*
* @since 4.5.0
* @class CKEDITOR.plugins.clipboard.dataTransfer
* @constructor Creates a class instance.
* @param {Object} [nativeDataTransfer] A native data transfer object.
* @param {CKEDITOR.editor} [editor] The source editor instance. If the editor is defined, dataValue will
* be created based on the editor content and the type will be 'html'.
*/
CKEDITOR.plugins.clipboard.dataTransfer = function( nativeDataTransfer, editor ) {
if ( nativeDataTransfer ) {
this.$ = nativeDataTransfer;
}
this._ = {
metaRegExp: /^/i,
bodyRegExp: /([\s\S]*)<\/body>/i,
fragmentRegExp: //g,
data: {},
files: [],
// Stores full HTML so it can be accessed asynchronously with `getData( 'text/html', true )`.
nativeHtmlCache: '',
normalizeType: function( type ) {
type = type.toLowerCase();
if ( type == 'text' || type == 'text/plain' ) {
return 'Text'; // IE support only Text and URL;
} else if ( type == 'url' ) {
return 'URL'; // IE support only Text and URL;
} else {
return type;
}
}
};
this._.fallbackDataTransfer = new CKEDITOR.plugins.clipboard.fallbackDataTransfer( this );
// Check if ID is already created.
this.id = this.getData( clipboardIdDataType );
// If there is no ID we need to create it. Different browsers needs different ID.
if ( !this.id ) {
if ( clipboardIdDataType == 'Text' ) {
// For IE10+ only Text data type is supported and we have to compare dragged
// and dropped text. If the ID is not set it means that empty string was dragged
// (ex. image with no alt). We change null to empty string.
this.id = '';
} else {
// String for custom data type.
this.id = 'cke-' + CKEDITOR.tools.getUniqueId();
}
}
if ( editor ) {
this.sourceEditor = editor;
this.setData( 'text/html', editor.getSelectedHtml( 1 ) );
// Without setData( 'text', ... ) on dragstart there is no drop event in Safari.
// Also 'text' data is empty as drop to the textarea does not work if we do not put there text.
if ( clipboardIdDataType != 'Text' && !this.getData( 'text/plain' ) ) {
this.setData( 'text/plain', editor.getSelection().getSelectedText() );
}
}
/**
* Data transfer ID used to bind all dataTransfer
* objects based on the same event (e.g. in drag and drop events).
*
* @readonly
* @property {String} id
*/
/**
* A native DOM event object.
*
* @readonly
* @property {Object} $
*/
/**
* Source editor — the editor where the drag starts.
* Might be undefined if the drag starts outside the editor (e.g. when dropping files to the editor).
*
* @readonly
* @property {CKEDITOR.editor} sourceEditor
*/
/**
* Private properties and methods.
*
* @private
* @property {Object} _
*/
};
/**
* Data transfer operation (drag and drop or copy and paste) started and ended in the same
* editor instance.
*
* @since 4.5.0
* @readonly
* @property {Number} [=1]
* @member CKEDITOR
*/
CKEDITOR.DATA_TRANSFER_INTERNAL = 1;
/**
* Data transfer operation (drag and drop or copy and paste) started in one editor
* instance and ended in another.
*
* @since 4.5.0
* @readonly
* @property {Number} [=2]
* @member CKEDITOR
*/
CKEDITOR.DATA_TRANSFER_CROSS_EDITORS = 2;
/**
* Data transfer operation (drag and drop or copy and paste) started outside of the editor.
* The source of the data may be a textarea, HTML, another application, etc.
*
* @since 4.5.0
* @readonly
* @property {Number} [=3]
* @member CKEDITOR
*/
CKEDITOR.DATA_TRANSFER_EXTERNAL = 3;
CKEDITOR.plugins.clipboard.dataTransfer.prototype = {
/**
* Facade for the native `getData` method.
*
* @param {String} type The type of data to retrieve.
* @param {Boolean} [getNative=false] Indicates if the whole, original content of the dataTransfer should be returned.
* Introduced in CKEditor 4.7.0.
* @returns {String} type Stored data for the given type or an empty string if the data for that type does not exist.
*/
getData: function( type, getNative ) {
function isEmpty( data ) {
return data === undefined || data === null || data === '';
}
function filterUnwantedCharacters( data ) {
if ( typeof data !== 'string' ) {
return data;
}
var htmlEnd = data.indexOf( '' );
if ( htmlEnd !== -1 ) {
// Just cut everything after ``, so everything after htmlEnd index + length of ``.
// Required to workaround bug: https://bugs.chromium.org/p/chromium/issues/detail?id=696978
return data.substring( 0, htmlEnd + 7 );
}
return data;
}
type = this._.normalizeType( type );
var data = type == 'text/html' && getNative ? this._.nativeHtmlCache : this._.data[ type ];
if ( isEmpty( data ) ) {
if ( this._.fallbackDataTransfer.isRequired() ) {
data = this._.fallbackDataTransfer.getData( type, getNative );
} else {
try {
data = this.$.getData( type ) || '';
} catch ( e ) {
data = '';
}
}
if ( type == 'text/html' && !getNative ) {
data = this._stripHtml( data );
}
}
// Firefox on Linux put files paths as a text/plain data if there are files
// in the dataTransfer object. We need to hide it, because files should be
// handled on paste only if dataValue is empty.
if ( type == 'Text' && CKEDITOR.env.gecko && this.getFilesCount() &&
data.substring( 0, 7 ) == 'file://' ) {
data = '';
}
return filterUnwantedCharacters( data );
},
/**
* Facade for the native `setData` method.
*
* @param {String} type The type of data to retrieve.
* @param {String} value The data to add.
*/
setData: function( type, value ) {
type = this._.normalizeType( type );
if ( type == 'text/html' ) {
this._.data[ type ] = this._stripHtml( value );
// If 'text/html' is set manually we also store it in `nativeHtmlCache` without modifications.
this._.nativeHtmlCache = value;
} else {
this._.data[ type ] = value;
}
// There is "Unexpected call to method or property access." error if you try
// to set data of unsupported type on IE.
if ( !CKEDITOR.plugins.clipboard.isCustomDataTypesSupported && type != 'URL' && type != 'Text' ) {
return;
}
// If we use the text type to bind the ID, then if someone tries to set the text, we must also
// update ID accordingly. https://dev.ckeditor.com/ticket/13468.
if ( clipboardIdDataType == 'Text' && type == 'Text' ) {
this.id = value;
}
if ( this._.fallbackDataTransfer.isRequired() ) {
this._.fallbackDataTransfer.setData( type, value );
} else {
try {
this.$.setData( type, value );
} catch ( e ) {}
}
},
/**
* Stores dataTransfer id in native data transfer object
* so it can be retrieved by other events.
*
* @since 4.8.0
*/
storeId: function() {
if ( clipboardIdDataType !== 'Text' ) {
this.setData( clipboardIdDataType, this.id );
}
},
/**
* Gets the data transfer type.
*
* @param {CKEDITOR.editor} targetEditor The drop/paste target editor instance.
* @returns {Number} Possible values: {@link CKEDITOR#DATA_TRANSFER_INTERNAL},
* {@link CKEDITOR#DATA_TRANSFER_CROSS_EDITORS}, {@link CKEDITOR#DATA_TRANSFER_EXTERNAL}.
*/
getTransferType: function( targetEditor ) {
if ( !this.sourceEditor ) {
return CKEDITOR.DATA_TRANSFER_EXTERNAL;
} else if ( this.sourceEditor == targetEditor ) {
return CKEDITOR.DATA_TRANSFER_INTERNAL;
} else {
return CKEDITOR.DATA_TRANSFER_CROSS_EDITORS;
}
},
/**
* Copies the data from the native data transfer to a private cache.
* This function is needed because the data from the native data transfer
* is available only synchronously to the event listener. It is not possible
* to get the data asynchronously, after a timeout, and the {@link CKEDITOR.editor#paste}
* event is fired asynchronously — hence the need for caching the data.
*/
cacheData: function() {
if ( !this.$ ) {
return;
}
var that = this,
i, file;
function getAndSetData( type ) {
type = that._.normalizeType( type );
var data = that.getData( type );
// Cache full html.
if ( type == 'text/html' ) {
that._.nativeHtmlCache = that.getData( type, true );
data = that._stripHtml( data );
}
if ( data ) {
that._.data[ type ] = data;
}
}
// Copy data.
if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ) {
if ( this.$.types ) {
for ( i = 0; i < this.$.types.length; i++ ) {
getAndSetData( this.$.types[ i ] );
}
}
} else {
getAndSetData( 'Text' );
getAndSetData( 'URL' );
}
// Copy files references.
file = this._getImageFromClipboard();
if ( ( this.$ && this.$.files ) || file ) {
this._.files = [];
// Edge have empty files property with no length (https://dev.ckeditor.com/ticket/13755).
if ( this.$.files && this.$.files.length ) {
for ( i = 0; i < this.$.files.length; i++ ) {
this._.files.push( this.$.files[ i ] );
}
}
// Don't include $.items if both $.files and $.items contains files, because,
// according to spec and browsers behavior, they contain the same files.
if ( this._.files.length === 0 && file ) {
this._.files.push( file );
}
}
},
/**
* Gets the number of files in the dataTransfer object.
*
* @returns {Number} The number of files.
*/
getFilesCount: function() {
if ( this._.files.length ) {
return this._.files.length;
}
if ( this.$ && this.$.files && this.$.files.length ) {
return this.$.files.length;
}
return this._getImageFromClipboard() ? 1 : 0;
},
/**
* Gets the file at the index given.
*
* @param {Number} i Index.
* @returns {File} File instance.
*/
getFile: function( i ) {
if ( this._.files.length ) {
return this._.files[ i ];
}
if ( this.$ && this.$.files && this.$.files.length ) {
return this.$.files[ i ];
}
// File or null if the file was not found.
return i === 0 ? this._getImageFromClipboard() : undefined;
},
/**
* Checks if the data transfer contains any data.
*
* @returns {Boolean} `true` if the object contains no data.
*/
isEmpty: function() {
var typesToCheck = {},
type;
// If dataTransfer contains files it is not empty.
if ( this.getFilesCount() ) {
return false;
}
CKEDITOR.tools.array.forEach( CKEDITOR.tools.object.keys( this._.data ), function( type ) {
typesToCheck[ type ] = 1;
} );
// Add native types.
if ( this.$ ) {
if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ) {
if ( this.$.types ) {
for ( var i = 0; i < this.$.types.length; i++ ) {
typesToCheck[ this.$.types[ i ] ] = 1;
}
}
} else {
typesToCheck.Text = 1;
typesToCheck.URL = 1;
}
}
// Remove ID.
if ( clipboardIdDataType != 'Text' ) {
typesToCheck[ clipboardIdDataType ] = 0;
}
for ( type in typesToCheck ) {
if ( typesToCheck[ type ] && this.getData( type ) !== '' ) {
return false;
}
}
return true;
},
/**
* When the content of the clipboard is pasted in Chrome, the clipboard data object has an empty `files` property,
* but it is possible to get the file as `items[0].getAsFile();` (https://dev.ckeditor.com/ticket/12961).
*
* @private
* @returns {File} File instance or `null` if not found.
*/
_getImageFromClipboard: function() {
var file;
try {
if ( this.$ && this.$.items && this.$.items[ 0 ] ) {
file = this.$.items[ 0 ].getAsFile();
// Duck typing
if ( file && file.type ) {
return file;
}
}
} catch ( err ) {
// noop
}
return undefined;
},
/**
* This function removes this meta information and returns only the contents of the `` element if found.
*
* Various environments use miscellaneous meta tags in HTML clipboard, e.g.
*
* * ` ` at the begging of the HTML data.
* * Surrounding HTML with `` and `` nested within `` elements.
*
* @private
* @param {String} html
* @returns {String}
*/
_stripHtml: function( html ) {
var result = html;
// Passed HTML may be empty or null. There is no need to strip such values (#1299).
if ( result && result.length ) {
// See https://dev.ckeditor.com/ticket/13583 for more details.
// Additionally https://dev.ckeditor.com/ticket/16847 adds a flag allowing to get the whole, original content.
result = result.replace( this._.metaRegExp, '' );
// Keep only contents of the element
var match = this._.bodyRegExp.exec( result );
if ( match && match.length ) {
result = match[ 1 ];
// Remove also comments.
result = result.replace( this._.fragmentRegExp, '' );
}
}
return result;
}
};
/**
* Fallback dataTransfer object which is used together with {@link CKEDITOR.plugins.clipboard.dataTransfer}
* for browsers supporting Clipboard API, but not supporting custom
* MIME types (Edge 16+, see [ckeditor-dev/issues/#962](https://github.com/ckeditor/ckeditor-dev/issues/962)).
*
* @since 4.8.0
* @class CKEDITOR.plugins.clipboard.fallbackDataTransfer
* @constructor
* @param {CKEDITOR.plugins.clipboard.dataTransfer} dataTransfer DataTransfer
* object which internal cache and
* {@link CKEDITOR.plugins.clipboard.dataTransfer#$ data transfer} objects will be reused.
*/
CKEDITOR.plugins.clipboard.fallbackDataTransfer = function( dataTransfer ) {
/**
* DataTransfer object which internal cache and
* {@link CKEDITOR.plugins.clipboard.dataTransfer#$ data transfer} objects will be modified if needed.
*
* @private
* @property {CKEDITOR.plugins.clipboard.dataTransfer} _dataTransfer
*/
this._dataTransfer = dataTransfer;
/**
* A MIME type used for storing custom MIME types.
*
* @private
* @property {String} [_customDataFallbackType='text/html']
*/
this._customDataFallbackType = 'text/html';
};
/**
* True if the environment supports custom MIME types in {@link CKEDITOR.plugins.clipboard.dataTransfer#getData}
* and {@link CKEDITOR.plugins.clipboard.dataTransfer#setData} methods.
*
* Introduced to distinguish between browsers which support only some whitelisted types (like `text/html`, `application/xml`),
* but do not support custom MIME types (like `cke/id`). When the value of this property equals `null`
* it means it was not yet initialized.
*
* This property should not be accessed directly, use {@link #isRequired} method instead.
*
* @private
* @static
* @property {Boolean}
*/
CKEDITOR.plugins.clipboard.fallbackDataTransfer._isCustomMimeTypeSupported = null;
/**
* Array containing MIME types which are not supported by native `setData`. Those types are
* recognized by error which is thrown when using native `setData` with a given type
* (see {@link CKEDITOR.plugins.clipboard.fallbackDataTransfer#_isUnsupportedMimeTypeError}).
*
* @private
* @static
* @property {String[]}
*/
CKEDITOR.plugins.clipboard.fallbackDataTransfer._customTypes = [];
CKEDITOR.plugins.clipboard.fallbackDataTransfer.prototype = {
/**
* Whether {@link CKEDITOR.plugins.clipboard.fallbackDataTransfer fallbackDataTransfer object} should
* be used when operating on native `dataTransfer`. If `true` is returned, it means custom MIME types
* are not supported in the current browser (see {@link #_isCustomMimeTypeSupported}).
*
* @returns {Boolean}
*/
isRequired: function() {
var fallbackDataTransfer = CKEDITOR.plugins.clipboard.fallbackDataTransfer,
nativeDataTransfer = this._dataTransfer.$;
if ( fallbackDataTransfer._isCustomMimeTypeSupported === null ) {
// If there is no `dataTransfer` we cannot detect if fallback is needed.
// Method returns `false` so regular flow will be applied.
if ( !nativeDataTransfer ) {
return false;
} else {
var testValue = 'cke test value',
testType = 'cke/mimetypetest';
fallbackDataTransfer._isCustomMimeTypeSupported = false;
// It looks like after our custom MIME type test Edge 17 is denying access on nativeDataTransfer (#2169).
// Upstream issue: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/18089287/
if ( CKEDITOR.env.edge && CKEDITOR.env.version >= 17 ) {
return true;
}
try {
nativeDataTransfer.setData( testType, testValue );
fallbackDataTransfer._isCustomMimeTypeSupported = nativeDataTransfer.getData( testType ) === testValue;
nativeDataTransfer.clearData( testType );
} catch ( e ) {}
}
}
return !fallbackDataTransfer._isCustomMimeTypeSupported;
},
/**
* Returns the data of the given MIME type if stored in a regular way or in a special comment. If given type
* is the same as {@link #_customDataFallbackType} the whole data without special comment is returned.
*
* @param {String} type
* @param {Boolean} [getNative=false] Indicates if the whole, original content of the dataTransfer should be returned.
* @returns {String}
*/
getData: function( type, getNative ) {
// As cache is already checked in CKEDITOR.plugins.clipboard.dataTransfer#getData it is skipped
// here. So the assumption is the given type is not in cache.
var nativeData = this._getData( this._customDataFallbackType, true );
if ( getNative ) {
return nativeData;
}
var dataComment = this._extractDataComment( nativeData ),
value = null;
// If we are getting the same type which may store custom data we need to extract content only.
if ( type === this._customDataFallbackType ) {
value = dataComment.content;
} else {
// If we are getting different type we need to check inside data comment if it is stored there.
if ( dataComment.data && dataComment.data[ type ] ) {
value = dataComment.data[ type ];
} else {
// And then fallback to regular `getData`.
value = this._getData( type, true );
}
}
return value !== null ? value : '';
},
/**
* Sets given data in native `dataTransfer` object. If given MIME type is not supported it uses
* {@link #_customDataFallbackType} MIME type to save data using special comment format:
*
*
*
* It is important to keep in mind that `{ type: value }` object is stringified (using `JSON.stringify`)
* and encoded (using `encodeURIComponent`).
*
* @param {String} type
* @param {String} value
* @returns {String} The value which was set.
*/
setData: function( type, value ) {
// In case of fallbackDataTransfer, cache does not reflect native data one-to-one. For example, having
// types like text/plain, text/html, cke/id will result in cache storing:
//
// {
// text/plain: value1,
// text/html: value2,
// cke/id: value3
// }
//
// and native dataTransfer storing:
//
// {
// text/plain: value1,
// text/html: value2
// }
//
// This way, accessing cache will always return proper value for a given type without a need for further processing.
// Cache is already set in CKEDITOR.plugins.clipboard.dataTransfer#setData so it is skipped here.
var isFallbackDataType = type === this._customDataFallbackType;
if ( isFallbackDataType ) {
value = this._applyDataComment( value, this._getFallbackTypeData() );
}
var data = value,
nativeDataTransfer = this._dataTransfer.$;
try {
nativeDataTransfer.setData( type, data );
if ( isFallbackDataType ) {
// If fallback type used, the native data is different so we overwrite `nativeHtmlCache` here.
this._dataTransfer._.nativeHtmlCache = data;
}
} catch ( e ) {
if ( this._isUnsupportedMimeTypeError( e ) ) {
var fallbackDataTransfer = CKEDITOR.plugins.clipboard.fallbackDataTransfer;
if ( CKEDITOR.tools.indexOf( fallbackDataTransfer._customTypes, type ) === -1 ) {
fallbackDataTransfer._customTypes.push( type );
}
var fallbackTypeContent = this._getFallbackTypeContent(),
fallbackTypeData = this._getFallbackTypeData();
fallbackTypeData[ type ] = data;
try {
data = this._applyDataComment( fallbackTypeContent, fallbackTypeData );
nativeDataTransfer.setData( this._customDataFallbackType, data );
// Again, fallback type was changed, so we need to refresh the cache.
this._dataTransfer._.nativeHtmlCache = data;
} catch ( e ) {
data = '';
// Some dev logger should be added here.
}
}
}
return data;
},
/**
* Native getData wrapper.
*
* @private
* @param {String} type
* @param {Boolean} [skipCache=false]
* @returns {String|null}
*/
_getData: function( type, skipCache ) {
var cache = this._dataTransfer._.data;
if ( !skipCache && cache[ type ] ) {
return cache[ type ];
} else {
try {
return this._dataTransfer.$.getData( type );
} catch ( e ) {
return null;
}
}
},
/**
* Returns content stored in {@link #\_customDataFallbackType}. Content is always first retrieved
* from {@link #_dataTransfer} cache and then from native `dataTransfer` object.
*
* @private
* @returns {String}
*/
_getFallbackTypeContent: function() {
var fallbackTypeContent = this._dataTransfer._.data[ this._customDataFallbackType ];
if ( !fallbackTypeContent ) {
fallbackTypeContent = this._extractDataComment( this._getData( this._customDataFallbackType, true ) ).content;
}
return fallbackTypeContent;
},
/**
* Returns custom data stored in {@link #\_customDataFallbackType}. Custom data is always first retrieved
* from {@link #_dataTransfer} cache and then from native `dataTransfer` object.
*
* @private
* @returns {Object}
*/
_getFallbackTypeData: function() {
var fallbackTypes = CKEDITOR.plugins.clipboard.fallbackDataTransfer._customTypes,
fallbackTypeData = this._extractDataComment( this._getData( this._customDataFallbackType, true ) ).data || {},
cache = this._dataTransfer._.data;
CKEDITOR.tools.array.forEach( fallbackTypes, function( type ) {
if ( cache[ type ] !== undefined ) {
fallbackTypeData[ type ] = cache[ type ];
} else if ( fallbackTypeData[ type ] !== undefined ) {
fallbackTypeData[ type ] = fallbackTypeData[ type ];
}
}, this );
return fallbackTypeData;
},
/**
* Whether provided error means that unsupported MIME type was used when calling native `dataTransfer.setData` method.
*
* @private
* @param {Error} error
* @returns {Boolean}
*/
_isUnsupportedMimeTypeError: function( error ) {
return error.message && error.message.search( /element not found/gi ) !== -1;
},
/**
* Extracts `cke-data` comment from the given content.
*
* @private
* @param {String} content
* @returns {Object} Returns an object containing extracted data as `data`
* and content (without `cke-data` comment) as `content`.
* @returns {Object|null} return.data Object containing `MIME type : value` pairs
* or null if `cke-data` comment is not present.
* @returns {String} return.content Regular content without `cke-data` comment.
*/
_extractDataComment: function( content ) {
var result = {
data: null,
content: content || ''
};
// At least 17 characters length: .
if ( content && content.length > 16 ) {
var matcher = //g,
matches;
matches = matcher.exec( content );
if ( matches && matches[ 1 ] ) {
result.data = JSON.parse( decodeURIComponent( matches[ 1 ] ) );
result.content = content.replace( matches[ 0 ], '' );
}
}
return result;
},
/**
* Creates `cke-data` comment containing stringified and encoded data object which is prepended to a given content.
*
* @private
* @param {String} content
* @param {Object} data
* @returns {String}
*/
_applyDataComment: function( content, data ) {
var customData = '';
if ( data && CKEDITOR.tools.object.keys( data ).length ) {
customData = '';
}
return customData + ( content && content.length ? content : '' );
}
};
} )();
/**
* The default content type that is used when pasted data cannot be clearly recognized as HTML or text.
*
* For example: `'foo'` may come from a plain text editor or a website. It is not possible to recognize the content
* type in this case, so the default type will be used. At the same time it is clear that `'example text'` is
* HTML and its origin is a web page, email or another rich text editor.
*
* **Note:** If content type is text, then styles of the paste context are preserved.
*
* CKEDITOR.config.clipboard_defaultContentType = 'text';
*
* See also the {@link CKEDITOR.editor#paste} event and read more about the integration with clipboard
* in the {@glink guide/dev_clipboard Clipboard Deep Dive guide}.
*
* @since 4.0.0
* @cfg {'html'/'text'} [clipboard_defaultContentType='html']
* @member CKEDITOR.config
*/
/**
* Fired after the user initiated a paste action, but before the data is inserted into the editor.
* The listeners to this event are able to process the content before its insertion into the document.
*
* Read more about the integration with clipboard in the {@glink guide/dev_clipboard Clipboard Deep Dive guide}.
*
* See also:
*
* * the {@link CKEDITOR.config#pasteFilter} option,
* * the {@link CKEDITOR.editor#drop} event,
* * the {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
*
* @since 3.1.0
* @event paste
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {String} data.type The type of data in `data.dataValue`. Usually `'html'` or `'text'`, but for listeners
* with a priority smaller than `6` it may also be `'auto'` which means that the content type has not been recognised yet
* (this will be done by the content type sniffer that listens with priority `6`).
* @param {String} data.dataValue HTML to be pasted.
* @param {String} data.method Indicates the data transfer method. It could be drag and drop or copy and paste.
* Possible values: `'drop'`, `'paste'`. Introduced in CKEditor 4.5.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer Facade for the native dataTransfer object
* which provides access to various data types and files, and passes some data between linked events
* (like drag and drop). Introduced in CKEditor 4.5.
* @param {Boolean} [data.dontFilter=false] Whether the {@link CKEDITOR.editor#pasteFilter paste filter} should not
* be applied to data. This option has no effect when `data.type` equals `'text'` which means that for instance
* {@link CKEDITOR.config#forcePasteAsPlainText} has a higher priority. Introduced in CKEditor 4.5.
*/
/**
* Fired before the {@link #paste} event. Allows to preset data type.
*
* **Note:** This event is deprecated. Add a `0` priority listener for the
* {@link #paste} event instead.
*
* @deprecated
* @event beforePaste
* @member CKEDITOR.editor
*/
/**
* Fired after the {@link #paste} event if content was modified. Note that if the paste
* event does not insert any data, the `afterPaste` event will not be fired.
*
* @event afterPaste
* @member CKEDITOR.editor
*/
/**
* Internal event to open the Paste dialog window.
*
*
* This event was not available in 4.7.0-4.8.0 versions.
*
* @private
* @event pasteDialog
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param {Function} [data] Callback that will be passed to {@link CKEDITOR.editor#openDialog}.
*/
/**
* Facade for the native `drop` event. Fired when the native `drop` event occurs.
*
* **Note:** To manipulate dropped data, use the {@link CKEDITOR.editor#paste} event.
* Use the `drop` event only to control drag and drop operations (e.g. to prevent the ability to drop some content).
*
* Read more about integration with drag and drop in the {@glink guide/dev_clipboard Clipboard Deep Dive guide}.
*
* See also:
*
* * The {@link CKEDITOR.editor#paste} event,
* * The {@link CKEDITOR.editor#dragstart} and {@link CKEDITOR.editor#dragend} events,
* * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
*
* @since 4.5.0
* @event drop
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {Object} data.$ Native drop event.
* @param {CKEDITOR.dom.node} data.target Drop target.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
* @param {CKEDITOR.dom.range} data.dragRange Drag range, lets you manipulate the drag range.
* Note that dragged HTML is saved as `text/html` data on `dragstart` so if you change the drag range
* on drop, dropped HTML will not change. You need to change it manually using
* {@link CKEDITOR.plugins.clipboard.dataTransfer#setData dataTransfer.setData}.
* @param {CKEDITOR.dom.range} data.dropRange Drop range, lets you manipulate the drop range.
*/
/**
* Facade for the native `dragstart` event. Fired when the native `dragstart` event occurs.
*
* This event can be canceled in order to block the drag start operation. It can also be fired to mimic the start of the drag and drop
* operation. For instance, the `widget` plugin uses this option to integrate its custom block widget drag and drop with
* the entire system.
*
* Read more about integration with drag and drop in the {@glink guide/dev_clipboard Clipboard Deep Dive guide}.
*
* See also:
*
* * The {@link CKEDITOR.editor#paste} event,
* * The {@link CKEDITOR.editor#drop} and {@link CKEDITOR.editor#dragend} events,
* * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
*
* @since 4.5.0
* @event dragstart
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {Object} data.$ Native dragstart event.
* @param {CKEDITOR.dom.node} data.target Drag target.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
*/
/**
* Facade for the native `dragend` event. Fired when the native `dragend` event occurs.
*
* Read more about integration with drag and drop in the {@glink guide/dev_clipboard Clipboard Deep Dive guide}.
*
* See also:
*
* * The {@link CKEDITOR.editor#paste} event,
* * The {@link CKEDITOR.editor#drop} and {@link CKEDITOR.editor#dragend} events,
* * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
*
* @since 4.5.0
* @event dragend
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {Object} data.$ Native dragend event.
* @param {CKEDITOR.dom.node} data.target Drag target.
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
*/
/**
* Defines a filter which is applied to external data pasted or dropped into the editor. Possible values are:
*
* * `'plain-text'` – Content will be pasted as a plain text.
* * `'semantic-content'` – Known tags (except `div`, `span`) with all attributes (except
* `style` and `class`) will be kept.
* * `'h1 h2 p div'` – Custom rules compatible with {@link CKEDITOR.filter}.
* * `null` – Content will not be filtered by the paste filter (but it still may be filtered
* by {@glink guide/dev_advanced_content_filter Advanced Content Filter}). This value can be used to
* disable the paste filter in Chrome and Safari, where this option defaults to `'semantic-content'`.
*
* Example:
*
* config.pasteFilter = 'plain-text';
*
* Custom setting:
*
* config.pasteFilter = 'h1 h2 p ul ol li; img[!src, alt]; a[!href]';
*
* Based on this configuration option, a proper {@link CKEDITOR.filter} instance will be defined and assigned to the editor
* as a {@link CKEDITOR.editor#pasteFilter}. You can tweak the paste filter settings on the fly on this object
* as well as delete or replace it.
*
* var editor = CKEDITOR.replace( 'editor', {
* pasteFilter: 'semantic-content'
* } );
*
* editor.on( 'instanceReady', function() {
* // The result of this will be that all semantic content will be preserved
* // except tables.
* editor.pasteFilter.disallow( 'table' );
* } );
*
* Note that the paste filter is applied only to **external** data. There are three data sources:
*
* * copied and pasted in the same editor (internal),
* * copied from one editor and pasted into another (cross-editor),
* * coming from all other sources like websites, MS Word, etc. (external).
*
* If {@link CKEDITOR.config#allowedContent Advanced Content Filter} is not disabled, then
* it will also be applied to pasted and dropped data. The paste filter job is to "normalize"
* external data which often needs to be handled differently than content produced by the editor.
*
* This setting defaults to `'semantic-content'` in Chrome, Opera and Safari (all Blink and Webkit based browsers)
* due to messy HTML which these browsers keep in the clipboard. In other browsers it defaults to `null`.
*
* @since 4.5.0
* @cfg {String} [pasteFilter='semantic-content' in Chrome and Safari and `null` in other browsers]
* @member CKEDITOR.config
*/
/**
* {@link CKEDITOR.filter Content filter} which is used when external data is pasted or dropped into the editor
* or a forced paste as plain text occurs.
*
* This object might be used on the fly to define rules for pasted external content.
* This object is available and used if the {@link CKEDITOR.plugins.clipboard clipboard} plugin is enabled and
* {@link CKEDITOR.config#pasteFilter} or {@link CKEDITOR.config#forcePasteAsPlainText} was defined.
*
* To enable the filter:
*
* var editor = CKEDITOR.replace( 'editor', {
* pasteFilter: 'plain-text'
* } );
*
* You can also modify the filter on the fly later on:
*
* editor.pasteFilter = new CKEDITOR.filter( 'p h1 h2; a[!href]' );
*
* Note that the paste filter is only applied to **external** data. There are three data sources:
*
* * copied and pasted in the same editor (internal),
* * copied from one editor and pasted into another (cross-editor),
* * coming from all other sources like websites, MS Word, etc. (external).
*
* If {@link CKEDITOR.config#allowedContent Advanced Content Filter} is not disabled, then
* it will also be applied to pasted and dropped data. The paste filter job is to "normalize"
* external data which often needs to be handled differently than content produced by the editor.
*
* @since 4.5.0
* @readonly
* @property {CKEDITOR.filter} [pasteFilter]
* @member CKEDITOR.editor
*/
/**
* Duration of the notification displayed after pasting was blocked by the browser.
*
* @since 4.7.0
* @cfg {Number} [clipboard_notificationDuration=10000]
* @member CKEDITOR.config
*/
CKEDITOR.config.clipboard_notificationDuration = 10000;
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
CKEDITOR.plugins.add( 'panelbutton', {
requires: 'button',
onLoad: function() {
function clickFn( editor ) {
var _ = this._;
if ( _.state == CKEDITOR.TRISTATE_DISABLED )
return;
this.createPanel( editor );
if ( _.on ) {
_.panel.hide();
return;
}
_.panel.showBlock( this._.id, this.document.getById( this._.id ), 4 );
}
/**
* @class
* @extends CKEDITOR.ui.button
* @todo class and methods
*/
CKEDITOR.ui.panelButton = CKEDITOR.tools.createClass( {
base: CKEDITOR.ui.button,
/**
* Creates a panelButton class instance.
*
* @constructor
*/
$: function( definition ) {
// We don't want the panel definition in this object.
var panelDefinition = definition.panel || {};
delete definition.panel;
this.base( definition );
this.document = ( panelDefinition.parent && panelDefinition.parent.getDocument() ) || CKEDITOR.document;
panelDefinition.block = {
attributes: panelDefinition.attributes
};
panelDefinition.toolbarRelated = true;
this.hasArrow = 'listbox';
this.click = clickFn;
this._ = {
panelDefinition: panelDefinition
};
},
statics: {
handler: {
create: function( definition ) {
return new CKEDITOR.ui.panelButton( definition );
}
}
},
proto: {
createPanel: function( editor ) {
var _ = this._;
if ( _.panel )
return;
var panelDefinition = this._.panelDefinition,
panelBlockDefinition = this._.panelDefinition.block,
panelParentElement = panelDefinition.parent || CKEDITOR.document.getBody(),
panel = this._.panel = new CKEDITOR.ui.floatPanel( editor, panelParentElement, panelDefinition ),
block = panel.addBlock( _.id, panelBlockDefinition ),
me = this;
panel.onShow = function() {
if ( me.className )
this.element.addClass( me.className + '_panel' );
me.setState( CKEDITOR.TRISTATE_ON );
_.on = 1;
me.editorFocus && editor.focus();
if ( me.onOpen )
me.onOpen();
};
panel.onHide = function( preventOnClose ) {
if ( me.className )
this.element.getFirst().removeClass( me.className + '_panel' );
me.setState( me.modes && me.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
_.on = 0;
if ( !preventOnClose && me.onClose )
me.onClose();
};
panel.onEscape = function() {
panel.hide( 1 );
me.document.getById( _.id ).focus();
};
if ( this.onBlock )
this.onBlock( panel, block );
block.onHide = function() {
_.on = 0;
me.setState( CKEDITOR.TRISTATE_OFF );
};
}
}
} );
},
beforeInit: function( editor ) {
editor.ui.addHandler( CKEDITOR.UI_PANELBUTTON, CKEDITOR.ui.panelButton.handler );
}
} );
/**
* Button UI element.
*
* @readonly
* @property {String} [='panelbutton']
* @member CKEDITOR
*/
CKEDITOR.UI_PANELBUTTON = 'panelbutton';
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
( function() {
CKEDITOR.plugins.add( 'panel', {
beforeInit: function( editor ) {
editor.ui.addHandler( CKEDITOR.UI_PANEL, CKEDITOR.ui.panel.handler );
}
} );
/**
* Panel UI element.
*
* @readonly
* @property {String} [='panel']
* @member CKEDITOR
*/
CKEDITOR.UI_PANEL = 'panel';
/**
* @class
* @constructor Creates a panel class instance.
* @param {CKEDITOR.dom.document} document
* @param {Object} definition
*/
CKEDITOR.ui.panel = function( document, definition ) {
// Copy all definition properties to this object.
if ( definition )
CKEDITOR.tools.extend( this, definition );
// Set defaults.
CKEDITOR.tools.extend( this, {
className: '',
css: []
} );
this.id = CKEDITOR.tools.getNextId();
this.document = document;
this.isFramed = this.forceIFrame || this.css.length;
this._ = {
blocks: {}
};
};
/**
* Represents panel handler object.
*
* @class
* @singleton
* @extends CKEDITOR.ui.handlerDefinition
*/
CKEDITOR.ui.panel.handler = {
/**
* Transforms a panel definition in a {@link CKEDITOR.ui.panel} instance.
*
* @param {Object} definition
* @returns {CKEDITOR.ui.panel}
*/
create: function( definition ) {
return new CKEDITOR.ui.panel( definition );
}
};
var panelTpl = CKEDITOR.addTemplate( 'panel', '' +
'{frame}' +
'
' );
var frameTpl = CKEDITOR.addTemplate( 'panel-frame', ' ' );
var frameDocTpl = CKEDITOR.addTemplate( 'panel-frame-inner', '' +
'' +
'{css}' +
'' +
'<\/html>' );
/** @class CKEDITOR.ui.panel */
CKEDITOR.ui.panel.prototype = {
/**
* Renders the combo.
*
* @param {CKEDITOR.editor} editor The editor instance which this button is
* to be used by.
* @param {Array} [output] The output array to which append the HTML relative
* to this button.
*/
render: function( editor, output ) {
var data = {
editorId: editor.id,
id: this.id,
langCode: editor.langCode,
dir: editor.lang.dir,
cls: this.className,
frame: '',
env: CKEDITOR.env.cssClass,
'z-index': editor.config.baseFloatZIndex + 1
};
this.getHolderElement = function() {
var holder = this._.holder;
if ( !holder ) {
if ( this.isFramed ) {
var iframe = this.document.getById( this.id + '_frame' ),
parentDiv = iframe.getParent(),
doc = iframe.getFrameDocument();
// Make it scrollable on iOS. (https://dev.ckeditor.com/ticket/8308)
CKEDITOR.env.iOS && parentDiv.setStyles( {
'overflow': 'scroll',
'-webkit-overflow-scrolling': 'touch'
} );
var onLoad = CKEDITOR.tools.addFunction( CKEDITOR.tools.bind( function() {
this.isLoaded = true;
if ( this.onLoad )
this.onLoad();
}, this ) );
doc.write( frameDocTpl.output( CKEDITOR.tools.extend( {
css: CKEDITOR.tools.buildStyleHtml( this.css ),
onload: 'window.parent.CKEDITOR.tools.callFunction(' + onLoad + ');'
}, data ) ) );
var win = doc.getWindow();
// Register the CKEDITOR global.
win.$.CKEDITOR = CKEDITOR;
// Arrow keys for scrolling is only preventable with 'keypress' event in Opera (https://dev.ckeditor.com/ticket/4534).
doc.on( 'keydown', function( evt ) {
var keystroke = evt.data.getKeystroke(),
dir = this.document.getById( this.id ).getAttribute( 'dir' );
// Arrow left and right should use native behaviour inside input element
if ( evt.data.getTarget().getName() === 'input' && ( keystroke === 37 || keystroke === 39 ) ) {
return;
}
// Delegate key processing to block.
if ( this._.onKeyDown && this._.onKeyDown( keystroke ) === false ) {
if ( !( evt.data.getTarget().getName() === 'input' && keystroke === 32 ) ) {
// Don't prevent space when is pressed on a input filed.
evt.data.preventDefault();
}
return;
}
// ESC/ARROW-LEFT(ltr) OR ARROW-RIGHT(rtl)
if ( keystroke == 27 || keystroke == ( dir == 'rtl' ? 39 : 37 ) ) {
if ( this.onEscape && this.onEscape( keystroke ) === false )
evt.data.preventDefault();
}
}, this );
holder = doc.getBody();
holder.unselectable();
CKEDITOR.env.air && CKEDITOR.tools.callFunction( onLoad );
} else {
holder = this.document.getById( this.id );
}
this._.holder = holder;
}
return holder;
};
if ( this.isFramed ) {
// With IE, the custom domain has to be taken care at first,
// for other browers, the 'src' attribute should be left empty to
// trigger iframe's 'load' event.
var src =
CKEDITOR.env.air ? 'javascript:void(0)' : // jshint ignore:line
( CKEDITOR.env.ie && !CKEDITOR.env.edge ) ? 'javascript:void(function(){' + encodeURIComponent( // jshint ignore:line
'document.open();' +
// In IE, the document domain must be set any time we call document.open().
'(' + CKEDITOR.tools.fixDomain + ')();' +
'document.close();'
) + '}())' :
'';
data.frame = frameTpl.output( {
id: this.id + '_frame',
src: src
} );
}
var html = panelTpl.output( data );
if ( output )
output.push( html );
return html;
},
/**
* @todo
*/
addBlock: function( name, block ) {
block = this._.blocks[ name ] = block instanceof CKEDITOR.ui.panel.block ? block : new CKEDITOR.ui.panel.block( this.getHolderElement(), block );
if ( !this._.currentBlock )
this.showBlock( name );
return block;
},
/**
* @todo
*/
getBlock: function( name ) {
return this._.blocks[ name ];
},
/**
* @todo
*/
showBlock: function( name ) {
var blocks = this._.blocks,
block = blocks[ name ],
current = this._.currentBlock;
// ARIA role works better in IE on the body element, while on the iframe
// for FF. (https://dev.ckeditor.com/ticket/8864)
var holder = !this.forceIFrame || CKEDITOR.env.ie ? this._.holder : this.document.getById( this.id + '_frame' );
if ( current )
current.hide();
this._.currentBlock = block;
CKEDITOR.fire( 'ariaWidget', holder );
// Reset the focus index, so it will always go into the first one.
block._.focusIndex = -1;
this._.onKeyDown = block.onKeyDown && CKEDITOR.tools.bind( block.onKeyDown, block );
block.show();
return block;
},
/**
* @todo
*/
destroy: function() {
this.element && this.element.remove();
}
};
/**
* @class
*
* @todo class and all methods
*/
CKEDITOR.ui.panel.block = CKEDITOR.tools.createClass( {
/**
* Creates a block class instances.
*
* @constructor
* @todo
*/
$: function( blockHolder, blockDefinition ) {
this.element = blockHolder.append( blockHolder.getDocument().createElement( 'div', {
attributes: {
'tabindex': -1,
'class': 'cke_panel_block'
},
styles: {
display: 'none'
}
} ) );
// Copy all definition properties to this object.
if ( blockDefinition )
CKEDITOR.tools.extend( this, blockDefinition );
// Set the a11y attributes of this element ...
this.element.setAttributes( {
'role': this.attributes.role || 'presentation',
'aria-label': this.attributes[ 'aria-label' ],
'title': this.attributes.title || this.attributes[ 'aria-label' ]
} );
this.keys = {};
this._.focusIndex = -1;
// Disable context menu for panels.
this.element.disableContextMenu();
},
_: {
/**
* Mark the item specified by the index as current activated.
*/
markItem: function( index ) {
if ( index == -1 )
return;
var focusables = this._.getItems();
var item = focusables.getItem( this._.focusIndex = index );
// Safari need focus on the iframe window first(https://dev.ckeditor.com/ticket/3389), but we need
// lock the blur to avoid hiding the panel.
if ( CKEDITOR.env.webkit )
item.getDocument().getWindow().focus();
item.focus();
this.onMark && this.onMark( item );
},
/**
* Marks the first visible item or the one whose `aria-selected` attribute is set to `true`.
* The latter has priority over the former.
*
* @private
* @param beforeMark function to be executed just before marking.
* Used in cases when any preparatory cleanup (like unmarking all items) would simultaneously
* destroy the information that is needed to determine the focused item.
*/
markFirstDisplayed: function( beforeMark ) {
var notDisplayed = function( element ) {
return element.type == CKEDITOR.NODE_ELEMENT && element.getStyle( 'display' ) == 'none';
},
focusables = this._.getItems(),
item, focused;
for ( var i = focusables.count() - 1; i >= 0; i-- ) {
item = focusables.getItem( i );
if ( !item.getAscendant( notDisplayed ) ) {
focused = item;
this._.focusIndex = i;
}
if ( item.getAttribute( 'aria-selected' ) == 'true' ) {
focused = item;
this._.focusIndex = i;
break;
}
}
if ( !focused ) {
return;
}
if ( beforeMark ) {
beforeMark();
}
if ( CKEDITOR.env.webkit )
focused.getDocument().getWindow().focus();
focused.focus();
this.onMark && this.onMark( focused );
},
/**
* Returns a `CKEDITOR.dom.nodeList` of block items.
*
* @returns {CKEDITOR.dom.nodeList}
*/
getItems: function() {
return this.element.find( 'a,input' );
}
},
proto: {
show: function() {
this.element.setStyle( 'display', '' );
},
hide: function() {
if ( !this.onHide || this.onHide.call( this ) !== true )
this.element.setStyle( 'display', 'none' );
},
onKeyDown: function( keystroke, noCycle ) {
var keyAction = this.keys[ keystroke ];
switch ( keyAction ) {
// Move forward.
case 'next':
var index = this._.focusIndex,
focusables = this._.getItems(),
focusable;
while ( ( focusable = focusables.getItem( ++index ) ) ) {
// Move the focus only if the element is marked with
// the _cke_focus and it it's visible (check if it has
// width).
if ( focusable.getAttribute( '_cke_focus' ) && focusable.$.offsetWidth ) {
this._.focusIndex = index;
focusable.focus( true );
break;
}
}
// If no focusable was found, cycle and restart from the top. (https://dev.ckeditor.com/ticket/11125)
if ( !focusable && !noCycle ) {
this._.focusIndex = -1;
return this.onKeyDown( keystroke, 1 );
}
return false;
// Move backward.
case 'prev':
index = this._.focusIndex;
focusables = this._.getItems();
while ( index > 0 && ( focusable = focusables.getItem( --index ) ) ) {
// Move the focus only if the element is marked with
// the _cke_focus and it it's visible (check if it has
// width).
if ( focusable.getAttribute( '_cke_focus' ) && focusable.$.offsetWidth ) {
this._.focusIndex = index;
focusable.focus( true );
break;
}
// Make sure focusable is null when the loop ends and nothing was
// found (https://dev.ckeditor.com/ticket/11125).
focusable = null;
}
// If no focusable was found, cycle and restart from the bottom. (https://dev.ckeditor.com/ticket/11125)
if ( !focusable && !noCycle ) {
this._.focusIndex = focusables.count();
return this.onKeyDown( keystroke, 1 );
}
return false;
case 'click':
case 'mouseup':
index = this._.focusIndex;
focusable = index >= 0 && this._.getItems().getItem( index );
if ( focusable )
focusable.$[ keyAction ] ? focusable.$[ keyAction ]() : focusable.$[ 'on' + keyAction ]();
return false;
}
return true;
}
}
} );
} )();
/**
* Fired when a panel is added to the document.
*
* @event ariaWidget
* @member CKEDITOR
* @param {Object} data The element wrapping the panel.
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
CKEDITOR.plugins.add( 'floatpanel', {
requires: 'panel'
} );
( function() {
var panels = {};
function getPanel( editor, doc, parentElement, definition, level ) {
// Generates the panel key: docId-eleId-skinName-langDir[-uiColor][-CSSs][-level]
var key = CKEDITOR.tools.genKey( doc.getUniqueId(), parentElement.getUniqueId(), editor.lang.dir, editor.uiColor || '', definition.css || '', level || '' ),
panel = panels[ key ];
if ( !panel ) {
panel = panels[ key ] = new CKEDITOR.ui.panel( doc, definition );
panel.element = parentElement.append( CKEDITOR.dom.element.createFromHtml( panel.render( editor ), doc ) );
panel.element.setStyles( {
display: 'none',
position: 'absolute'
} );
}
return panel;
}
/**
* Represents a floating panel UI element.
*
* It is reused by rich combos, color combos, menus, etc.
* and it renders its content using {@link CKEDITOR.ui.panel}.
*
* @class
* @todo
*/
CKEDITOR.ui.floatPanel = CKEDITOR.tools.createClass( {
/**
* Creates a floatPanel class instance.
*
* @constructor
* @param {CKEDITOR.editor} editor
* @param {CKEDITOR.dom.element} parentElement
* @param {Object} definition Definition of the panel that will be floating.
* @param {Number} level
*/
$: function( editor, parentElement, definition, level ) {
definition.forceIFrame = 1;
// In case of editor with floating toolbar append panels that should float
// to the main UI element.
if ( definition.toolbarRelated && editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE )
parentElement = CKEDITOR.document.getById( 'cke_' + editor.name );
var doc = parentElement.getDocument(),
panel = getPanel( editor, doc, parentElement, definition, level || 0 ),
element = panel.element,
iframe = element.getFirst(),
that = this;
// Disable native browser menu. (https://dev.ckeditor.com/ticket/4825)
element.disableContextMenu();
this.element = element;
this._ = {
editor: editor,
// The panel that will be floating.
panel: panel,
parentElement: parentElement,
definition: definition,
document: doc,
iframe: iframe,
children: [],
dir: editor.lang.dir,
showBlockParams: null,
markFirst: definition.markFirst !== undefined ? definition.markFirst : true
};
editor.on( 'mode', hide );
editor.on( 'resize', hide );
// When resize of the window is triggered floatpanel should be repositioned according to new dimensions.
// https://dev.ckeditor.com/ticket/11724. Fixes issue with undesired panel hiding on Android and iOS.
doc.getWindow().on( 'resize', function() {
this.reposition();
}, this );
// We need a wrapper because events implementation doesn't allow to attach
// one listener more than once for the same event on the same object.
// Remember that floatPanel#hide is shared between all instances.
function hide() {
that.hide();
}
},
proto: {
/**
* @todo
*/
addBlock: function( name, block ) {
return this._.panel.addBlock( name, block );
},
/**
* @todo
*/
addListBlock: function( name, multiSelect ) {
return this._.panel.addListBlock( name, multiSelect );
},
/**
* @todo
*/
getBlock: function( name ) {
return this._.panel.getBlock( name );
},
/**
* Shows the panel block.
*
* @param {String} name
* @param {CKEDITOR.dom.element} offsetParent Positioned parent.
* @param {Number} corner
*
* * For LTR (left to right) oriented editor:
* * `1` = top-left
* * `2` = top-right
* * `3` = bottom-right
* * `4` = bottom-left
* * For RTL (right to left):
* * `1` = top-right
* * `2` = top-left
* * `3` = bottom-left
* * `4` = bottom-right
*
* @param {Number} [offsetX=0]
* @param {Number} [offsetY=0]
* @param {Function} [callback] A callback function executed when block positioning is done.
* @todo what do exactly these params mean (especially corner)?
*/
showBlock: function( name, offsetParent, corner, offsetX, offsetY, callback ) {
var panel = this._.panel,
block = panel.showBlock( name );
this._.showBlockParams = [].slice.call( arguments );
this.allowBlur( false );
// Record from where the focus is when open panel.
var editable = this._.editor.editable();
this._.returnFocus = editable.hasFocus ? editable : new CKEDITOR.dom.element( CKEDITOR.document.$.activeElement );
this._.hideTimeout = 0;
var element = this.element,
iframe = this._.iframe,
// Edge prefers iframe's window to the iframe, just like the rest of the browsers (https://dev.ckeditor.com/ticket/13143).
focused = CKEDITOR.env.ie && !CKEDITOR.env.edge ? iframe : new CKEDITOR.dom.window( iframe.$.contentWindow ),
doc = element.getDocument(),
positionedAncestor = this._.parentElement.getPositionedAncestor(),
position = offsetParent.getDocumentPosition( doc ),
positionedAncestorPosition = positionedAncestor ? positionedAncestor.getDocumentPosition( doc ) : { x: 0, y: 0 },
rtl = this._.dir == 'rtl',
left = position.x + ( offsetX || 0 ) - positionedAncestorPosition.x,
top = position.y + ( offsetY || 0 ) - positionedAncestorPosition.y;
// Floating panels are off by (-1px, 0px) in RTL mode. (https://dev.ckeditor.com/ticket/3438)
if ( rtl && ( corner == 1 || corner == 4 ) )
left += offsetParent.$.offsetWidth;
else if ( !rtl && ( corner == 2 || corner == 3 ) )
left += offsetParent.$.offsetWidth - 1;
if ( corner == 3 || corner == 4 )
top += offsetParent.$.offsetHeight - 1;
// Memorize offsetParent by it's ID.
this._.panel._.offsetParentId = offsetParent.getId();
element.setStyles( {
top: top + 'px',
left: 0,
display: ''
} );
// Don't use display or visibility style because we need to
// calculate the rendering layout later and focus the element.
element.setOpacity( 0 );
// To allow the context menu to decrease back their width
element.getFirst().removeStyle( 'width' );
// Report to focus manager.
this._.editor.focusManager.add( focused );
// Configure the IFrame blur event. Do that only once.
if ( !this._.blurSet ) {
// With addEventListener compatible browsers, we must
// useCapture when registering the focus/blur events to
// guarantee they will be firing in all situations. (https://dev.ckeditor.com/ticket/3068, https://dev.ckeditor.com/ticket/3222 )
CKEDITOR.event.useCapture = true;
focused.on( 'blur', function( ev ) {
// As we are using capture to register the listener,
// the blur event may get fired even when focusing
// inside the window itself, so we must ensure the
// target is out of it.
if ( !this.allowBlur() || ev.data.getPhase() != CKEDITOR.EVENT_PHASE_AT_TARGET )
return;
if ( this.visible && !this._.activeChild ) {
// [iOS] Allow hide to be prevented if touch is bound
// to any parent of the iframe blur happens before touch (https://dev.ckeditor.com/ticket/10714).
if ( CKEDITOR.env.iOS ) {
if ( !this._.hideTimeout )
this._.hideTimeout = CKEDITOR.tools.setTimeout( doHide, 0, this );
} else {
doHide.call( this );
}
}
function doHide() {
// Panel close is caused by user's navigating away the focus, e.g. click outside the panel.
// DO NOT restore focus in this case.
delete this._.returnFocus;
this.hide();
}
}, this );
focused.on( 'focus', function() {
this._.focused = true;
this.hideChild();
this.allowBlur( true );
}, this );
// [iOS] if touch is bound to any parent of the iframe blur
// happens twice before touchstart and before touchend (https://dev.ckeditor.com/ticket/10714).
if ( CKEDITOR.env.iOS ) {
// Prevent false hiding on blur.
// We don't need to return focus here because touchend will fire anyway.
// If user scrolls and pointer gets out of the panel area touchend will also fire.
focused.on( 'touchstart', function() {
clearTimeout( this._.hideTimeout );
}, this );
// Set focus back to handle blur and hide panel when needed.
focused.on( 'touchend', function() {
this._.hideTimeout = 0;
this.focus();
}, this );
}
CKEDITOR.event.useCapture = false;
this._.blurSet = 1;
}
panel.onEscape = CKEDITOR.tools.bind( function( keystroke ) {
if ( this.onEscape && this.onEscape( keystroke ) === false )
return false;
}, this );
CKEDITOR.tools.setTimeout( function() {
var panelLoad = CKEDITOR.tools.bind( function() {
var target = element;
// Reset panel width as the new content can be narrower
// than the old one. (https://dev.ckeditor.com/ticket/9355)
target.removeStyle( 'width' );
if ( block.autoSize ) {
var panelDoc = block.element.getDocument(),
width = ( ( CKEDITOR.env.webkit || CKEDITOR.env.edge ) ? block.element : panelDoc.getBody() ).$.scrollWidth;
// Account for extra height needed due to IE quirks box model bug:
// http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
// (https://dev.ckeditor.com/ticket/3426)
if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && width > 0 )
width += ( target.$.offsetWidth || 0 ) - ( target.$.clientWidth || 0 ) + 3;
// Add some extra pixels to improve the appearance.
width += 10;
target.setStyle( 'width', width + 'px' );
var height = block.element.$.scrollHeight;
// Account for extra height needed due to IE quirks box model bug:
// http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
// (https://dev.ckeditor.com/ticket/3426)
if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && height > 0 )
height += ( target.$.offsetHeight || 0 ) - ( target.$.clientHeight || 0 ) + 3;
target.setStyle( 'height', height + 'px' );
// Fix IE < 8 visibility.
panel._.currentBlock.element.setStyle( 'display', 'none' ).removeStyle( 'display' );
} else {
target.removeStyle( 'height' );
}
// Flip panel layout horizontally in RTL with known width.
if ( rtl )
left -= element.$.offsetWidth;
// Pop the style now for measurement.
element.setStyle( 'left', left + 'px' );
/* panel layout smartly fit the viewport size. */
var panelElement = panel.element,
panelWindow = panelElement.getWindow(),
rect = element.$.getBoundingClientRect(),
viewportSize = panelWindow.getViewPaneSize();
// Compensation for browsers that dont support "width" and "height".
var rectWidth = rect.width || rect.right - rect.left,
rectHeight = rect.height || rect.bottom - rect.top;
// Check if default horizontal layout is impossible.
var spaceAfter = rtl ? rect.right : viewportSize.width - rect.left,
spaceBefore = rtl ? viewportSize.width - rect.right : rect.left;
if ( rtl ) {
if ( spaceAfter < rectWidth ) {
// Flip to show on right.
if ( spaceBefore > rectWidth )
left += rectWidth;
// Align to window left.
else if ( viewportSize.width > rectWidth )
left = left - rect.left;
// Align to window right, never cutting the panel at right.
else
left = left - rect.right + viewportSize.width;
}
} else if ( spaceAfter < rectWidth ) {
// Flip to show on left.
if ( spaceBefore > rectWidth )
left -= rectWidth;
// Align to window right.
else if ( viewportSize.width > rectWidth )
left = left - rect.right + viewportSize.width;
// Align to window left, never cutting the panel at left.
else
left = left - rect.left;
}
// Check if the default vertical layout is possible.
var spaceBelow = viewportSize.height - rect.top,
spaceAbove = rect.top;
if ( spaceBelow < rectHeight ) {
// Flip to show above.
if ( spaceAbove > rectHeight )
top -= rectHeight;
// Align to window bottom.
else if ( viewportSize.height > rectHeight )
top = top - rect.bottom + viewportSize.height;
// Align to top, never cutting the panel at top.
else
top = top - rect.top;
}
// If IE is in RTL, we have troubles with absolute
// position and horizontal scrolls. Here we have a
// series of hacks to workaround it. (https://dev.ckeditor.com/ticket/6146)
if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) {
var offsetParent = new CKEDITOR.dom.element( element.$.offsetParent ),
scrollParent = offsetParent;
// Quirks returns , but standards returns .
if ( scrollParent.getName() == 'html' ) {
scrollParent = scrollParent.getDocument().getBody();
}
if ( scrollParent.getComputedStyle( 'direction' ) == 'rtl' ) {
// For IE8, there is not much logic on this, but it works.
if ( CKEDITOR.env.ie8Compat ) {
left -= element.getDocument().getDocumentElement().$.scrollLeft * 2;
} else {
left -= ( offsetParent.$.scrollWidth - offsetParent.$.clientWidth );
}
}
}
// Trigger the onHide event of the previously active panel to prevent
// incorrect styles from being applied (https://dev.ckeditor.com/ticket/6170)
var innerElement = element.getFirst(),
activePanel;
if ( ( activePanel = innerElement.getCustomData( 'activePanel' ) ) )
activePanel.onHide && activePanel.onHide.call( this, 1 );
innerElement.setCustomData( 'activePanel', this );
element.setStyles( {
top: top + 'px',
left: left + 'px'
} );
element.setOpacity( 1 );
callback && callback();
}, this );
panel.isLoaded ? panelLoad() : panel.onLoad = panelLoad;
CKEDITOR.tools.setTimeout( function() {
var scrollTop = CKEDITOR.env.webkit && CKEDITOR.document.getWindow().getScrollPosition().y;
// Focus the panel frame first, so blur gets fired.
this.focus();
// Focus the block now.
block.element.focus();
// https://dev.ckeditor.com/ticket/10623, https://dev.ckeditor.com/ticket/10951 - restore the viewport's scroll position after focusing list element.
if ( CKEDITOR.env.webkit )
CKEDITOR.document.getBody().$.scrollTop = scrollTop;
// We need this get fired manually because of unfired focus() function.
this.allowBlur( true );
// Ensure that the first item is focused (https://dev.ckeditor.com/ticket/16804).
if ( this._.markFirst ) {
if ( CKEDITOR.env.ie ) {
CKEDITOR.tools.setTimeout( function() {
block.markFirstDisplayed ? block.markFirstDisplayed() : block._.markFirstDisplayed();
}, 0 );
} else {
block.markFirstDisplayed ? block.markFirstDisplayed() : block._.markFirstDisplayed();
}
}
this._.editor.fire( 'panelShow', this );
}, 0, this );
}, CKEDITOR.env.air ? 200 : 0, this );
this.visible = 1;
if ( this.onShow )
this.onShow.call( this );
},
/**
* Repositions the panel with the same parameters that were used in the last {@link #showBlock} call.
*
* @since 4.5.4
*/
reposition: function() {
var blockParams = this._.showBlockParams;
if ( this.visible && this._.showBlockParams ) {
this.hide();
this.showBlock.apply( this, blockParams );
}
},
/**
* Restores the last focused element or simply focuses the panel window.
*/
focus: function() {
// Webkit requires to blur any previous focused page element, in
// order to properly fire the "focus" event.
if ( CKEDITOR.env.webkit ) {
var active = CKEDITOR.document.getActive();
active && !active.equals( this._.iframe ) && active.$.blur();
}
// Restore last focused element or simply focus panel window.
var focus = this._.lastFocused || this._.iframe.getFrameDocument().getWindow();
focus.focus();
},
/**
* @todo
*/
blur: function() {
var doc = this._.iframe.getFrameDocument(),
active = doc.getActive();
active && active.is( 'a' ) && ( this._.lastFocused = active );
},
/**
* Hides the panel.
*
* @todo
*/
hide: function( returnFocus ) {
if ( this.visible && ( !this.onHide || this.onHide.call( this ) !== true ) ) {
this.hideChild();
// Blur previously focused element. (https://dev.ckeditor.com/ticket/6671)
CKEDITOR.env.gecko && this._.iframe.getFrameDocument().$.activeElement.blur();
this.element.setStyle( 'display', 'none' );
this.visible = 0;
this.element.getFirst().removeCustomData( 'activePanel' );
// Return focus properly. (https://dev.ckeditor.com/ticket/6247)
var focusReturn = returnFocus && this._.returnFocus;
if ( focusReturn ) {
// Webkit requires focus moved out panel iframe first.
if ( CKEDITOR.env.webkit && focusReturn.type )
focusReturn.getWindow().$.focus();
focusReturn.focus();
}
delete this._.lastFocused;
this._.showBlockParams = null;
this._.editor.fire( 'panelHide', this );
}
},
/**
* @todo
*/
allowBlur: function( allow ) {
// Prevent editor from hiding the panel. (https://dev.ckeditor.com/ticket/3222)
var panel = this._.panel;
if ( allow !== undefined )
panel.allowBlur = allow;
return panel.allowBlur;
},
/**
* Shows the specified panel as a child of one block of this one.
*
* @param {CKEDITOR.ui.floatPanel} panel
* @param {String} blockName
* @param {CKEDITOR.dom.element} offsetParent Positioned parent.
* @param {Number} corner
*
* * For LTR (left to right) oriented editor:
* * `1` = top-left
* * `2` = top-right
* * `3` = bottom-right
* * `4` = bottom-left
* * For RTL (right to left):
* * `1` = top-right
* * `2` = top-left
* * `3` = bottom-left
* * `4` = bottom-right
*
* @param {Number} [offsetX=0]
* @param {Number} [offsetY=0]
* @todo
*/
showAsChild: function( panel, blockName, offsetParent, corner, offsetX, offsetY ) {
// Skip reshowing of child which is already visible.
if ( this._.activeChild == panel && panel._.panel._.offsetParentId == offsetParent.getId() )
return;
this.hideChild();
panel.onHide = CKEDITOR.tools.bind( function() {
// Use a timeout, so we give time for this menu to get
// potentially focused.
CKEDITOR.tools.setTimeout( function() {
if ( !this._.focused )
this.hide();
}, 0, this );
}, this );
this._.activeChild = panel;
this._.focused = false;
panel.showBlock( blockName, offsetParent, corner, offsetX, offsetY );
this.blur();
/* https://dev.ckeditor.com/ticket/3767 IE: Second level menu may not have borders */
if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
setTimeout( function() {
panel.element.getChild( 0 ).$.style.cssText += '';
}, 100 );
}
},
/**
* @todo
*/
hideChild: function( restoreFocus ) {
var activeChild = this._.activeChild;
if ( activeChild ) {
delete activeChild.onHide;
delete this._.activeChild;
activeChild.hide();
// At this point focus should be moved back to parent panel.
restoreFocus && this.focus();
}
}
}
} );
CKEDITOR.on( 'instanceDestroyed', function() {
var isLastInstance = CKEDITOR.tools.isEmpty( CKEDITOR.instances );
for ( var i in panels ) {
var panel = panels[ i ];
// Safe to destroy it since there're no more instances.(https://dev.ckeditor.com/ticket/4241)
if ( isLastInstance )
panel.destroy();
// Panel might be used by other instances, just hide them.(https://dev.ckeditor.com/ticket/4552)
else
panel.element.hide();
}
// Remove the registration.
isLastInstance && ( panels = {} );
} );
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview The "colorbutton" plugin that makes it possible to assign
* text and background colors to editor contents.
*
*/
CKEDITOR.plugins.add( 'colorbutton', {
requires: 'panelbutton,floatpanel',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
var config = editor.config,
lang = editor.lang.colorbutton;
if ( !CKEDITOR.env.hc ) {
addButton( 'TextColor', 'fore', lang.textColorTitle, 10, {
contentTransformations: [
[
{
element: 'font',
check: 'span{color}',
left: function( element ) {
return !!element.attributes.color;
},
right: function( element ) {
element.name = 'span';
element.attributes.color && ( element.styles.color = element.attributes.color );
delete element.attributes.color;
}
}
]
]
} );
var bgOptions = {},
normalizeBackground = editor.config.colorButton_normalizeBackground;
if ( normalizeBackground === undefined || normalizeBackground ) {
// If background contains only color, then we want to convert it into background-color so that it's
// correctly picked by colorbutton plugin.
bgOptions.contentTransformations = [
[
{
// Transform span that specify background with color only to background-color.
element: 'span',
left: function( element ) {
var tools = CKEDITOR.tools;
if ( element.name != 'span' || !element.styles || !element.styles.background ) {
return false;
}
var background = tools.style.parse.background( element.styles.background );
// We return true only if background specifies **only** color property, and there's only one background directive.
return background.color && tools.object.keys( background ).length === 1;
},
right: function( element ) {
var style = new CKEDITOR.style( editor.config.colorButton_backStyle, {
color: element.styles.background
} ),
definition = style.getDefinition();
// Align the output object with the template used in config.
element.name = definition.element;
element.styles = definition.styles;
element.attributes = definition.attributes || {};
return element;
}
}
]
];
}
addButton( 'BGColor', 'back', lang.bgColorTitle, 20, bgOptions );
}
function addButton( name, type, title, order, options ) {
var style = new CKEDITOR.style( config[ 'colorButton_' + type + 'Style' ] ),
colorBoxId = CKEDITOR.tools.getNextId() + '_colorBox',
colorData = { type: type },
panelBlock;
options = options || {};
editor.ui.add( name, CKEDITOR.UI_PANELBUTTON, {
label: title,
title: title,
modes: { wysiwyg: 1 },
editorFocus: 0,
toolbar: 'colors,' + order,
allowedContent: style,
requiredContent: style,
contentTransformations: options.contentTransformations,
panel: {
css: CKEDITOR.skin.getPath( 'editor' ),
attributes: { role: 'listbox', 'aria-label': lang.panelTitle }
},
onBlock: function( panel, block ) {
panelBlock = block;
block.autoSize = true;
block.element.addClass( 'cke_colorblock' );
block.element.setHtml( renderColors( panel, type, colorBoxId, colorData ) );
// The block should not have scrollbars (https://dev.ckeditor.com/ticket/5933, https://dev.ckeditor.com/ticket/6056)
block.element.getDocument().getBody().setStyle( 'overflow', 'hidden' );
CKEDITOR.ui.fire( 'ready', this );
var keys = block.keys;
var rtl = editor.lang.dir == 'rtl';
keys[ rtl ? 37 : 39 ] = 'next'; // ARROW-RIGHT
keys[ 40 ] = 'next'; // ARROW-DOWN
keys[ 9 ] = 'next'; // TAB
keys[ rtl ? 39 : 37 ] = 'prev'; // ARROW-LEFT
keys[ 38 ] = 'prev'; // ARROW-UP
keys[ CKEDITOR.SHIFT + 9 ] = 'prev'; // SHIFT + TAB
keys[ 32 ] = 'click'; // SPACE
},
refresh: function() {
if ( !editor.activeFilter.check( style ) ) {
this.setState( CKEDITOR.TRISTATE_DISABLED );
}
},
// The automatic colorbox should represent the real color (https://dev.ckeditor.com/ticket/6010)
onOpen: function() {
var selection = editor.getSelection(),
block = selection && selection.getStartElement(),
path = editor.elementPath( block ),
automaticColor;
if ( !path ) {
return;
}
// Find the closest block element.
block = path.block || path.blockLimit || editor.document.getBody();
// The background color might be transparent. In that case, look up the color in the DOM tree.
do {
automaticColor = block && block.getComputedStyle( type == 'back' ? 'background-color' : 'color' ) || 'transparent';
}
while ( type == 'back' && automaticColor == 'transparent' && block && ( block = block.getParent() ) );
// The box should never be transparent.
if ( !automaticColor || automaticColor == 'transparent' ) {
automaticColor = '#ffffff';
}
if ( config.colorButton_enableAutomatic !== false ) {
this._.panel._.iframe.getFrameDocument().getById( colorBoxId ).setStyle( 'background-color', automaticColor );
}
var range = selection && selection.getRanges()[ 0 ];
if ( range ) {
var walker = new CKEDITOR.dom.walker( range ),
element = range.collapsed ? range.startContainer : walker.next(),
finalColor = '',
currentColor;
while ( element ) {
// (#2296)
if ( element.type !== CKEDITOR.NODE_ELEMENT ) {
element = element.getParent();
}
currentColor = normalizeColor( element.getComputedStyle( type == 'back' ? 'background-color' : 'color' ) );
finalColor = finalColor || currentColor;
if ( finalColor !== currentColor ) {
finalColor = '';
break;
}
element = walker.next();
}
if ( finalColor == 'transparent' ) {
finalColor = '';
}
if ( type == 'fore' ) {
colorData.automaticTextColor = '#' + normalizeColor( automaticColor );
}
colorData.selectionColor = finalColor ? '#' + finalColor : '';
selectColor( panelBlock, finalColor );
}
return automaticColor;
}
} );
}
function renderColors( panel, type, colorBoxId, colorData ) {
var output = [],
colors = config.colorButton_colors.split( ',' ),
colorsPerRow = config.colorButton_colorsPerRow || 6,
// Tells if we should include "More Colors..." button.
moreColorsEnabled = editor.plugins.colordialog && config.colorButton_enableMore !== false,
// aria-setsize and aria-posinset attributes are used to indicate size of options, because
// screen readers doesn't play nice with table, based layouts (https://dev.ckeditor.com/ticket/12097).
total = colors.length + ( moreColorsEnabled ? 2 : 1 );
var clickFn = CKEDITOR.tools.addFunction( function applyColorStyle( color, type ) {
editor.focus();
editor.fire( 'saveSnapshot' );
if ( color == '?' ) {
editor.getColorFromDialog( function( color ) {
if ( color ) {
return setColor( color );
}
}, null, colorData );
} else {
return setColor( color && '#' + color );
}
function setColor( color ) {
var colorStyle = config[ 'colorButton_' + type + 'Style' ];
// Clean up any conflicting style within the range.
editor.removeStyle( new CKEDITOR.style( colorStyle, { color: 'inherit' } ) );
colorStyle.childRule = type == 'back' ?
function( element ) {
// It's better to apply background color as the innermost style. (https://dev.ckeditor.com/ticket/3599)
// Except for "unstylable elements". (https://dev.ckeditor.com/ticket/6103)
return isUnstylable( element );
} : function( element ) {
// Fore color style must be applied inside links instead of around it. (https://dev.ckeditor.com/ticket/4772,https://dev.ckeditor.com/ticket/6908)
return !( element.is( 'a' ) || element.getElementsByTag( 'a' ).count() ) || isUnstylable( element );
};
editor.focus();
if ( color ) {
editor.applyStyle( new CKEDITOR.style( colorStyle, { color: color } ) );
}
editor.fire( 'saveSnapshot' );
}
} );
if ( config.colorButton_enableAutomatic !== false ) {
// Render the "Automatic" button.
output.push( '' +
' ' +
'' +
' ', lang.auto, ' ' +
' ' +
'
' +
'' );
}
output.push( '' );
// Render the color boxes.
for ( var i = 0; i < colors.length; i++ ) {
if ( ( i % colorsPerRow ) === 0 )
output.push( '' );
var parts = colors[ i ].split( '/' ),
colorName = parts[ 0 ],
colorCode = parts[ 1 ] || colorName,
colorLabel;
// The data can be only a color code (without #) or colorName + color code
// If only a color code is provided, then the colorName is the color with the hash
// Convert the color from RGB to RRGGBB for better compatibility with IE and . See https://dev.ckeditor.com/ticket/5676
// Additionally, if the data is a single color code then let's try to translate it or fallback on the
// color code. If the data is a color name/code, then use directly the color name provided.
if ( !parts[ 1 ] ) {
colorLabel = editor.lang.colorbutton.colors[ colorCode ] || colorCode;
} else {
colorLabel = colorName;
}
output.push( ' ' +
'' +
' ' +
' ' +
' ' );
}
// Render the "More Colors" button.
if ( moreColorsEnabled ) {
output.push( ' ' +
'' +
'' +
'', lang.more, ' ' +
' ' ); // tr is later in the code.
}
output.push( '
' );
return output.join( '' );
}
function isUnstylable( ele ) {
return ( ele.getAttribute( 'contentEditable' ) == 'false' ) || ele.getAttribute( 'data-nostyle' );
}
/*
* Selects the specified color in the specified panel block.
*
* @private
* @member CKEDITOR.plugins.colorbutton
* @param {CKEDITOR.ui.panel.block} block
* @param {String} color
*/
function selectColor( block, color ) {
var items = block._.getItems();
for ( var i = 0; i < items.count(); i++ ) {
var item = items.getItem( i );
item.removeAttribute( 'aria-selected' );
if ( color && color == normalizeColor( item.getAttribute( 'data-value' ) ) ) {
item.setAttribute( 'aria-selected', true );
}
}
}
/*
* Converts a CSS color value to an easily comparable form.
*
* @private
* @member CKEDITOR.plugins.colorbutton
* @param {String} color
* @returns {String}
*/
function normalizeColor( color ) {
// Replace 3-character hexadecimal notation with a 6-character hexadecimal notation (#1008).
return CKEDITOR.tools.normalizeHex( '#' + CKEDITOR.tools.convertRgbToHex( color || '' ) ).replace( /#/g, '' );
}
}
} );
/**
* Whether to enable the **More Colors** button in the color selectors.
*
* Read more in the {@glink features/colorbutton documentation}
* and see the {@glink examples/colorbutton example}.
*
* config.colorButton_enableMore = false;
*
* @cfg {Boolean} [colorButton_enableMore=true]
* @member CKEDITOR.config
*/
/**
* Defines the colors to be displayed in the color selectors. This is a string
* containing hexadecimal notation for HTML colors, without the `'#'` prefix.
*
* **Since 3.3:** A color name may optionally be defined by prefixing the entries with
* a name and the slash character. For example, `'FontColor1/FF9900'` will be
* displayed as the color `#FF9900` in the selector, but will be output as `'FontColor1'`.
* **This behaviour was altered in version 4.12.0.**
*
* **Since 4.6.2:** The default color palette has changed. It contains fewer colors in more
* pastel shades than the previous one.
*
* **Since 4.12.0:** Defining colors with names works in a different way. Colors names can be defined
* by `colorName/colorCode`. The color name is only used in the tooltip. The output will now use the color code.
* For example, `FontColor/FF9900` will be displayed as the color `#FF9900` in the selector, and will
* be output as `#FF9900`.
*
* Read more in the {@glink features/colorbutton documentation}
* and see the {@glink examples/colorbutton example}.
*
* // Brazil colors only.
* config.colorButton_colors = '00923E,F8C100,28166F';
*
* config.colorButton_colors = 'FontColor1/FF9900,FontColor2/0066CC,FontColor3/F00';
*
* // CKEditor color palette available before version 4.6.2.
* config.colorButton_colors =
* '000,800000,8B4513,2F4F4F,008080,000080,4B0082,696969,' +
* 'B22222,A52A2A,DAA520,006400,40E0D0,0000CD,800080,808080,' +
* 'F00,FF8C00,FFD700,008000,0FF,00F,EE82EE,A9A9A9,' +
* 'FFA07A,FFA500,FFFF00,00FF00,AFEEEE,ADD8E6,DDA0DD,D3D3D3,' +
* 'FFF0F5,FAEBD7,FFFFE0,F0FFF0,F0FFFF,F0F8FF,E6E6FA,FFF';
*
* @cfg {String} [colorButton_colors=see source]
* @member CKEDITOR.config
*/
CKEDITOR.config.colorButton_colors = '1ABC9C,2ECC71,3498DB,9B59B6,4E5F70,F1C40F,' +
'16A085,27AE60,2980B9,8E44AD,2C3E50,F39C12,' +
'E67E22,E74C3C,ECF0F1,95A5A6,DDD,FFF,' +
'D35400,C0392B,BDC3C7,7F8C8D,999,000';
/**
* Stores the style definition that applies the text foreground color.
*
* Read more in the {@glink features/colorbutton documentation}
* and see the {@glink examples/colorbutton example}.
*
* // This is actually the default value.
* config.colorButton_foreStyle = {
* element: 'span',
* styles: { color: '#(color)' }
* };
*
* @cfg [colorButton_foreStyle=see source]
* @member CKEDITOR.config
*/
CKEDITOR.config.colorButton_foreStyle = {
element: 'span',
styles: { 'color': '#(color)' },
overrides: [ {
element: 'font', attributes: { 'color': null }
} ]
};
/**
* Stores the style definition that applies the text background color.
*
* Read more in the {@glink features/colorbutton documentation}
* and see the {@glink examples/colorbutton example}.
*
* // This is actually the default value.
* config.colorButton_backStyle = {
* element: 'span',
* styles: { 'background-color': '#(color)' }
* };
*
* @cfg [colorButton_backStyle=see source]
* @member CKEDITOR.config
*/
CKEDITOR.config.colorButton_backStyle = {
element: 'span',
styles: { 'background-color': '#(color)' }
};
/**
* Whether to enable the **Automatic** button in the color selectors.
*
* Read more in the {@glink features/colorbutton documentation}
* and see the {@glink examples/colorbutton example}.
*
* config.colorButton_enableAutomatic = false;
*
* @cfg {Boolean} [colorButton_enableAutomatic=true]
* @member CKEDITOR.config
*/
/**
* Defines how many colors will be shown per row in the color selectors.
*
* Read more in the {@glink features/colorbutton documentation}
* and see the {@glink examples/colorbutton example}.
*
* config.colorButton_colorsPerRow = 8;
*
* @since 4.6.2
* @cfg {Number} [colorButton_colorsPerRow=6]
* @member CKEDITOR.config
*/
/**
* Whether the plugin should convert `background` CSS properties with color only, to a `background-color` property,
* allowing the [Color Button](https://ckeditor.com/cke4/addon/colorbutton) plugin to edit these styles.
*
* config.colorButton_normalizeBackground = false;
*
* @since 4.6.1
* @cfg {Boolean} [colorButton_normalizeBackground=true]
* @member CKEDITOR.config
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
( function() {
function setupAdvParams( element ) {
var attrName = this.att;
var value = element && element.hasAttribute( attrName ) && element.getAttribute( attrName ) || '';
if ( value !== undefined )
this.setValue( value );
}
function commitAdvParams() {
// Dialogs may use different parameters in the commit list, so, by
// definition, we take the first CKEDITOR.dom.element available.
var element;
for ( var i = 0; i < arguments.length; i++ ) {
if ( arguments[ i ] instanceof CKEDITOR.dom.element ) {
element = arguments[ i ];
break;
}
}
if ( element ) {
var attrName = this.att,
value = this.getValue();
if ( value )
element.setAttribute( attrName, value );
else
element.removeAttribute( attrName, value );
}
}
var defaultTabConfig = { id: 1, dir: 1, classes: 1, styles: 1 };
CKEDITOR.plugins.add( 'dialogadvtab', {
requires: 'dialog',
// Returns allowed content rule for the content created by this plugin.
allowedContent: function( tabConfig ) {
if ( !tabConfig )
tabConfig = defaultTabConfig;
var allowedAttrs = [];
if ( tabConfig.id )
allowedAttrs.push( 'id' );
if ( tabConfig.dir )
allowedAttrs.push( 'dir' );
var allowed = '';
if ( allowedAttrs.length )
allowed += '[' + allowedAttrs.join( ',' ) + ']';
if ( tabConfig.classes )
allowed += '(*)';
if ( tabConfig.styles )
allowed += '{*}';
return allowed;
},
// @param tabConfig
// id, dir, classes, styles
createAdvancedTab: function( editor, tabConfig, element ) {
if ( !tabConfig )
tabConfig = defaultTabConfig;
var lang = editor.lang.common;
var result = {
id: 'advanced',
label: lang.advancedTab,
title: lang.advancedTab,
elements: [ {
type: 'vbox',
padding: 1,
children: []
} ]
};
var contents = [];
if ( tabConfig.id || tabConfig.dir ) {
if ( tabConfig.id ) {
contents.push( {
id: 'advId',
att: 'id',
type: 'text',
requiredContent: element ? element + '[id]' : null,
label: lang.id,
setup: setupAdvParams,
commit: commitAdvParams
} );
}
if ( tabConfig.dir ) {
contents.push( {
id: 'advLangDir',
att: 'dir',
type: 'select',
requiredContent: element ? element + '[dir]' : null,
label: lang.langDir,
'default': '',
style: 'width:100%',
items: [
[ lang.notSet, '' ],
[ lang.langDirLTR, 'ltr' ],
[ lang.langDirRTL, 'rtl' ]
],
setup: setupAdvParams,
commit: commitAdvParams
} );
}
result.elements[ 0 ].children.push( {
type: 'hbox',
widths: [ '50%', '50%' ],
children: [].concat( contents )
} );
}
if ( tabConfig.styles || tabConfig.classes ) {
contents = [];
if ( tabConfig.styles ) {
contents.push( {
id: 'advStyles',
att: 'style',
type: 'text',
requiredContent: element ? element + '{cke-xyz}' : null,
label: lang.styles,
'default': '',
validate: CKEDITOR.dialog.validate.inlineStyle( lang.invalidInlineStyle ),
onChange: function() {},
getStyle: function( name, defaultValue ) {
var match = this.getValue().match( new RegExp( '(?:^|;)\\s*' + name + '\\s*:\\s*([^;]*)', 'i' ) );
return match ? match[ 1 ] : defaultValue;
},
updateStyle: function( name, value ) {
var styles = this.getValue();
var tmp = editor.document.createElement( 'span' );
tmp.setAttribute( 'style', styles );
tmp.setStyle( name, value );
styles = CKEDITOR.tools.normalizeCssText( tmp.getAttribute( 'style' ) );
this.setValue( styles, 1 );
},
setup: setupAdvParams,
commit: commitAdvParams
} );
}
if ( tabConfig.classes ) {
contents.push( {
type: 'hbox',
widths: [ '45%', '55%' ],
children: [ {
id: 'advCSSClasses',
att: 'class',
type: 'text',
requiredContent: element ? element + '(cke-xyz)' : null,
label: lang.cssClasses,
'default': '',
setup: setupAdvParams,
commit: commitAdvParams
} ]
} );
}
result.elements[ 0 ].children.push( {
type: 'hbox',
widths: [ '50%', '50%' ],
children: [].concat( contents )
} );
}
return result;
}
} );
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview The "div" plugin. It wraps the selected block level elements with a 'div' element with specified styles and attributes.
*
*/
( function() {
CKEDITOR.plugins.add( 'div', {
requires: 'dialog',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
if ( editor.blockless )
return;
var lang = editor.lang.div,
allowed = 'div(*)';
if ( CKEDITOR.dialog.isTabEnabled( editor, 'editdiv', 'advanced' ) )
allowed += ';div[dir,id,lang,title]{*}';
editor.addCommand( 'creatediv', new CKEDITOR.dialogCommand( 'creatediv', {
allowedContent: allowed,
requiredContent: 'div',
contextSensitive: true,
contentTransformations: [
[ 'div: alignmentToStyle' ]
],
refresh: function( editor, path ) {
var context = editor.config.div_wrapTable ? path.root : path.blockLimit;
this.setState( 'div' in context.getDtd() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
}
} ) );
editor.addCommand( 'editdiv', new CKEDITOR.dialogCommand( 'editdiv', { requiredContent: 'div' } ) );
editor.addCommand( 'removediv', {
requiredContent: 'div',
exec: function( editor ) {
var selection = editor.getSelection(),
ranges = selection && selection.getRanges(),
range,
bookmarks = selection.createBookmarks(),
walker,
toRemove = [];
function findDiv( node ) {
var div = CKEDITOR.plugins.div.getSurroundDiv( editor, node );
if ( div && !div.data( 'cke-div-added' ) ) {
toRemove.push( div );
div.data( 'cke-div-added' );
}
}
for ( var i = 0; i < ranges.length; i++ ) {
range = ranges[ i ];
if ( range.collapsed )
findDiv( selection.getStartElement() );
else {
walker = new CKEDITOR.dom.walker( range );
walker.evaluator = findDiv;
walker.lastForward();
}
}
for ( i = 0; i < toRemove.length; i++ )
toRemove[ i ].remove( true );
selection.selectBookmarks( bookmarks );
}
} );
editor.ui.addButton && editor.ui.addButton( 'CreateDiv', {
label: lang.toolbar,
command: 'creatediv',
toolbar: 'blocks,50'
} );
if ( editor.addMenuItems ) {
editor.addMenuItems( {
editdiv: {
label: lang.edit,
command: 'editdiv',
group: 'div',
order: 1
},
removediv: {
label: lang.remove,
command: 'removediv',
group: 'div',
order: 5
}
} );
if ( editor.contextMenu ) {
editor.contextMenu.addListener( function( element ) {
if ( !element || element.isReadOnly() )
return null;
if ( CKEDITOR.plugins.div.getSurroundDiv( editor ) ) {
return {
editdiv: CKEDITOR.TRISTATE_OFF,
removediv: CKEDITOR.TRISTATE_OFF
};
}
return null;
} );
}
}
CKEDITOR.dialog.add( 'creatediv', this.path + 'dialogs/div.js' );
CKEDITOR.dialog.add( 'editdiv', this.path + 'dialogs/div.js' );
}
} );
CKEDITOR.plugins.div = {
getSurroundDiv: function( editor, start ) {
var path = editor.elementPath( start );
return editor.elementPath( path.blockLimit ).contains( function( node ) {
// Avoid read-only (i.e. contenteditable="false") divs (https://dev.ckeditor.com/ticket/11083).
return node.is( 'div' ) && !node.isReadOnly();
}, 1 );
}
};
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview The "elementspath" plugin. It shows all elements in the DOM
* parent tree relative to the current selection in the editing area.
*/
( function() {
var commands = {
toolbarFocus: {
editorFocus: false,
readOnly: 1,
exec: function( editor ) {
var idBase = editor._.elementsPath.idBase;
var element = CKEDITOR.document.getById( idBase + '0' );
// Make the first button focus accessible for IE. (https://dev.ckeditor.com/ticket/3417)
// Adobe AIR instead need while of delay.
element && element.focus( CKEDITOR.env.ie || CKEDITOR.env.air );
}
}
};
var emptyHtml = ' ';
var extra = '';
// Some browsers don't cancel key events in the keydown but in the
// keypress.
// TODO: Check if really needed.
if ( CKEDITOR.env.gecko && CKEDITOR.env.mac )
extra += ' onkeypress="return false;"';
// With Firefox, we need to force the button to redraw, otherwise it
// will remain in the focus state.
if ( CKEDITOR.env.gecko )
extra += ' onblur="this.style.cssText = this.style.cssText;"';
var pathItemTpl = CKEDITOR.addTemplate( 'pathItem', '' +
'{text}' +
' ' );
CKEDITOR.plugins.add( 'elementspath', {
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
editor._.elementsPath = {
idBase: 'cke_elementspath_' + CKEDITOR.tools.getNextNumber() + '_',
filters: []
};
editor.on( 'uiSpace', function( event ) {
if ( event.data.space == 'bottom' )
initElementsPath( editor, event.data );
} );
}
} );
function initElementsPath( editor, bottomSpaceData ) {
var spaceId = editor.ui.spaceId( 'path' ),
spaceElement,
getSpaceElement = function() {
if ( !spaceElement )
spaceElement = CKEDITOR.document.getById( spaceId );
return spaceElement;
},
elementsPath = editor._.elementsPath,
idBase = elementsPath.idBase;
bottomSpaceData.html += '' + editor.lang.elementspath.eleLabel + ' ' +
'' + emptyHtml + ' ';
// Register the ui element to the focus manager.
editor.on( 'uiReady', function() {
var element = editor.ui.space( 'path' );
element && editor.focusManager.add( element, 1 );
} );
function onClick( elementIndex ) {
var element = elementsPath.list[ elementIndex ],
selection;
if ( element.equals( editor.editable() ) || element.getAttribute( 'contenteditable' ) == 'true' ) {
var range = editor.createRange();
range.selectNodeContents( element );
selection = range.select();
} else {
selection = editor.getSelection();
selection.selectElement( element );
}
// Explicitly fire selectionChange when clicking on an element path button. (https://dev.ckeditor.com/ticket/13548)
if ( CKEDITOR.env.ie ) {
editor.fire( 'selectionChange', { selection: selection, path: new CKEDITOR.dom.elementPath( element ) } );
}
// It is important to focus() *after* the above selection
// manipulation, otherwise Firefox will have troubles. https://dev.ckeditor.com/ticket/10119
editor.focus();
}
elementsPath.onClick = onClick;
var onClickHanlder = CKEDITOR.tools.addFunction( onClick ),
onKeyDownHandler = CKEDITOR.tools.addFunction( function( elementIndex, ev ) {
var idBase = elementsPath.idBase,
element;
ev = new CKEDITOR.dom.event( ev );
var rtl = editor.lang.dir == 'rtl';
switch ( ev.getKeystroke() ) {
case rtl ? 39 : 37: // LEFT-ARROW
case 9: // TAB
element = CKEDITOR.document.getById( idBase + ( elementIndex + 1 ) );
if ( !element )
element = CKEDITOR.document.getById( idBase + '0' );
element.focus();
return false;
case rtl ? 37 : 39: // RIGHT-ARROW
case CKEDITOR.SHIFT + 9: // SHIFT + TAB
element = CKEDITOR.document.getById( idBase + ( elementIndex - 1 ) );
if ( !element )
element = CKEDITOR.document.getById( idBase + ( elementsPath.list.length - 1 ) );
element.focus();
return false;
case 27: // ESC
editor.focus();
return false;
case 13: // ENTER // Opera
case 32: // SPACE
onClick( elementIndex );
return false;
}
return true;
} );
editor.on( 'selectionChange', function( evt ) {
var html = [],
elementsList = elementsPath.list = [],
namesList = [],
filters = elementsPath.filters,
isContentEditable = true,
// Use elementPath to consider children of editable only (https://dev.ckeditor.com/ticket/11124).
// Use elementPath from event (instead of editor.elementPath()), which is accurate in all cases (#801).
elementsChain = evt.data.path.elements,
name;
// Starts iteration from body element, skipping html.
for ( var j = elementsChain.length; j--; ) {
var element = elementsChain[ j ],
ignore = 0;
if ( element.data( 'cke-display-name' ) )
name = element.data( 'cke-display-name' );
else if ( element.data( 'cke-real-element-type' ) )
name = element.data( 'cke-real-element-type' );
else
name = element.getName();
isContentEditable = element.hasAttribute( 'contenteditable' ) ?
element.getAttribute( 'contenteditable' ) == 'true' : isContentEditable;
// If elem is non-contenteditable, and it's not specifying contenteditable
// attribute - then elem should be ignored.
if ( !isContentEditable && !element.hasAttribute( 'contenteditable' ) )
ignore = 1;
for ( var i = 0; i < filters.length; i++ ) {
var ret = filters[ i ]( element, name );
if ( ret === false ) {
ignore = 1;
break;
}
name = ret || name;
}
if ( !ignore ) {
elementsList.unshift( element );
namesList.unshift( name );
}
}
for ( var iterationLimit = elementsList.length, index = 0; index < iterationLimit; index++ ) {
name = namesList[ index ];
var label = editor.lang.elementspath.eleTitle.replace( /%1/, name ),
item = pathItemTpl.output( {
id: idBase + index,
label: label,
text: name,
jsTitle: 'javascript:void(\'' + name + '\')', // jshint ignore:line
index: index,
keyDownFn: onKeyDownHandler,
clickFn: onClickHanlder
} );
html.unshift( item );
}
var space = getSpaceElement();
space.setHtml( html.join( '' ) + emptyHtml );
editor.fire( 'elementsPathUpdate', { space: space } );
} );
function empty() {
spaceElement && spaceElement.setHtml( emptyHtml );
delete elementsPath.list;
}
editor.on( 'readOnly', empty );
editor.on( 'contentDomUnload', empty );
editor.addCommand( 'elementsPathFocus', commands.toolbarFocus );
editor.setKeystroke( CKEDITOR.ALT + 122 /*F11*/, 'elementsPathFocus' );
}
} )();
/**
* Fired when the contents of the elementsPath are changed.
*
* @event elementsPathUpdate
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {CKEDITOR.dom.element} data.space The elementsPath container.
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
( function() {
CKEDITOR.plugins.add( 'enterkey', {
init: function( editor ) {
editor.addCommand( 'enter', {
modes: { wysiwyg: 1 },
editorFocus: false,
exec: function( editor ) {
enter( editor );
}
} );
editor.addCommand( 'shiftEnter', {
modes: { wysiwyg: 1 },
editorFocus: false,
exec: function( editor ) {
shiftEnter( editor );
}
} );
editor.setKeystroke( [
[ 13, 'enter' ],
[ CKEDITOR.SHIFT + 13, 'shiftEnter' ]
] );
}
} );
var whitespaces = CKEDITOR.dom.walker.whitespaces(),
bookmark = CKEDITOR.dom.walker.bookmark(),
plugin,
enterBr,
enterBlock,
headerTagRegex;
CKEDITOR.plugins.enterkey = {
enterBlock: function( editor, mode, range, forceMode ) {
// Get the range for the current selection.
range = range || getRange( editor );
// We may not have valid ranges to work on, like when inside a
// contenteditable=false element.
if ( !range )
return;
// When range is in nested editable, we have to replace range with this one,
// which have root property set to closest editable, to make auto paragraphing work. (https://dev.ckeditor.com/ticket/12162)
range = replaceRangeWithClosestEditableRoot( range );
var doc = range.document;
var atBlockStart = range.checkStartOfBlock(),
atBlockEnd = range.checkEndOfBlock(),
path = editor.elementPath( range.startContainer ),
block = path.block,
// Determine the block element to be used.
blockTag = ( mode == CKEDITOR.ENTER_DIV ? 'div' : 'p' ),
newBlock;
if ( block && atBlockStart && atBlockEnd ) {
var blockParent = block.getParent();
// Block placeholder breaks list structure (#2205).
if ( blockParent.is( 'li' ) && blockParent.getChildCount() > 1 ) {
var placeholder = new CKEDITOR.dom.element( 'li' ),
newRange = editor.createRange();
placeholder.insertAfter( blockParent );
block.remove();
newRange.setStart( placeholder, 0 );
editor.getSelection().selectRanges( [ newRange ] );
return;
}
// Exit the list when we're inside an empty list item block. (https://dev.ckeditor.com/ticket/5376).
else if ( block.is( 'li' ) || block.getParent().is( 'li' ) ) {
// Make sure to point to the li when dealing with empty list item.
if ( !block.is( 'li' ) ) {
block = block.getParent();
blockParent = block.getParent();
}
var blockGrandParent = blockParent.getParent(),
firstChild = !block.hasPrevious(),
lastChild = !block.hasNext(),
selection = editor.getSelection(),
bookmarks = selection.createBookmarks(),
orgDir = block.getDirection( 1 ),
className = block.getAttribute( 'class' ),
style = block.getAttribute( 'style' ),
dirLoose = blockGrandParent.getDirection( 1 ) != orgDir,
enterMode = editor.enterMode,
needsBlock = enterMode != CKEDITOR.ENTER_BR || dirLoose || style || className,
child;
if ( blockGrandParent.is( 'li' ) ) {
// If block is the first or the last child of the parent
// list, degrade it and move to the outer list:
// before the parent list if block is first child and after
// the parent list if block is the last child, respectively.
//
//
//
// AND
//
//
if ( firstChild || lastChild ) {
// If it's only child, we don't want to keep perent ul anymore.
if ( firstChild && lastChild ) {
blockParent.remove();
}
block[lastChild ? 'insertAfter' : 'insertBefore']( blockGrandParent );
// If the empty block is neither first nor last child
// then split the list and the block as an element
// of outer list.
//
// =>
// =>
//
// ^ => ^
// y =>
// => =>
// =>
// =>
} else {
block.breakParent( blockGrandParent );
}
}
else if ( !needsBlock ) {
block.appendBogus( true );
// If block is the first or last child of the parent
// list, move all block's children out of the list:
// before the list if block is first child and after the list
// if block is the last child, respectively.
//
// => ^
//
// AND
//
//
if ( firstChild || lastChild ) {
while ( ( child = block[ firstChild ? 'getFirst' : 'getLast' ]() ) )
child[ firstChild ? 'insertBefore' : 'insertAfter' ]( blockParent );
}
// If the empty block is neither first nor last child
// then split the list and put all the block contents
// between two lists.
//
// =>
else {
block.breakParent( blockParent );
while ( ( child = block.getLast() ) )
child.insertAfter( blockParent );
}
block.remove();
} else {
// Original path block is the list item, create new block for the list item content.
if ( path.block.is( 'li' ) ) {
// Use block for ENTER_BR and ENTER_DIV.
newBlock = doc.createElement( mode == CKEDITOR.ENTER_P ? 'p' : 'div' );
if ( dirLoose )
newBlock.setAttribute( 'dir', orgDir );
style && newBlock.setAttribute( 'style', style );
className && newBlock.setAttribute( 'class', className );
// Move all the child nodes to the new block.
block.moveChildren( newBlock );
}
// The original path block is not a list item, just copy the block to out side of the list.
else {
newBlock = path.block;
}
// If block is the first or last child of the parent
// list, move it out of the list:
// before the list if block is first child and after the list
// if block is the last child, respectively.
//
//
=>
^
//
// AND
//
//
if ( firstChild || lastChild )
newBlock[ firstChild ? 'insertBefore' : 'insertAfter' ]( blockParent );
// If the empty block is neither first nor last child
// then split the list and put the new block between
// two lists.
//
// =>
//
// ^ => ^
// y => => y
// =>
else {
block.breakParent( blockParent );
newBlock.insertAfter( blockParent );
}
block.remove();
}
selection.selectBookmarks( bookmarks );
return;
}
if ( block && block.getParent().is( 'blockquote' ) ) {
block.breakParent( block.getParent() );
// If we were at the start of
, there will be an empty element before it now.
if ( !block.getPrevious().getFirst( CKEDITOR.dom.walker.invisible( 1 ) ) )
block.getPrevious().remove();
// If we were at the end of , there will be an empty element after it now.
if ( !block.getNext().getFirst( CKEDITOR.dom.walker.invisible( 1 ) ) )
block.getNext().remove();
range.moveToElementEditStart( block );
range.select();
return;
}
}
// Don't split if we're in the middle of it, act as shift enter key.
else if ( block && block.is( 'pre' ) ) {
if ( !atBlockEnd ) {
enterBr( editor, mode, range, forceMode );
return;
}
}
// Split the range.
var splitInfo = range.splitBlock( blockTag );
if ( !splitInfo )
return;
// Get the current blocks.
var previousBlock = splitInfo.previousBlock,
nextBlock = splitInfo.nextBlock;
var isStartOfBlock = splitInfo.wasStartOfBlock,
isEndOfBlock = splitInfo.wasEndOfBlock;
var node;
// If this is a block under a list item, split it as well. (https://dev.ckeditor.com/ticket/1647)
if ( nextBlock ) {
node = nextBlock.getParent();
if ( node.is( 'li' ) ) {
nextBlock.breakParent( node );
nextBlock.move( nextBlock.getNext(), 1 );
}
} else if ( previousBlock && ( node = previousBlock.getParent() ) && node.is( 'li' ) ) {
previousBlock.breakParent( node );
node = previousBlock.getNext();
range.moveToElementEditStart( node );
previousBlock.move( previousBlock.getPrevious() );
}
// If we have both the previous and next blocks, it means that the
// boundaries were on separated blocks, or none of them where on the
// block limits (start/end).
if ( !isStartOfBlock && !isEndOfBlock ) {
// If the next block is an with another list tree as the first
// child, we'll need to append a filler ( /NBSP) or the list item
// wouldn't be editable. (https://dev.ckeditor.com/ticket/1420)
if ( nextBlock.is( 'li' ) ) {
var walkerRange = range.clone();
walkerRange.selectNodeContents( nextBlock );
var walker = new CKEDITOR.dom.walker( walkerRange );
walker.evaluator = function( node ) {
return !( bookmark( node ) || whitespaces( node ) || node.type == CKEDITOR.NODE_ELEMENT && node.getName() in CKEDITOR.dtd.$inline && !( node.getName() in CKEDITOR.dtd.$empty ) );
};
node = walker.next();
if ( node && node.type == CKEDITOR.NODE_ELEMENT && node.is( 'ul', 'ol' ) )
( CKEDITOR.env.needsBrFiller ? doc.createElement( 'br' ) : doc.createText( '\xa0' ) ).insertBefore( node );
}
// Move the selection to the end block.
if ( nextBlock )
range.moveToElementEditStart( nextBlock );
}
// Handle differently when content of table cell was erased by pressing enter (#1816).
// We don't want to add new block, because it was created with range.splitBlock().
else if ( preventExtraLineInsideTable( mode ) ) {
range.moveToElementEditStart( range.getTouchedStartNode() );
} else {
var newBlockDir;
if ( previousBlock ) {
// Do not enter this block if it's a header tag, or we are in
// a Shift+Enter (https://dev.ckeditor.com/ticket/77). Create a new block element instead
// (later in the code).
if ( previousBlock.is( 'li' ) || !( headerTagRegex.test( previousBlock.getName() ) || previousBlock.is( 'pre' ) ) ) {
// Otherwise, duplicate the previous block.
newBlock = previousBlock.clone();
}
} else if ( nextBlock ) {
newBlock = nextBlock.clone();
}
if ( !newBlock ) {
// We have already created a new list item. (https://dev.ckeditor.com/ticket/6849)
if ( node && node.is( 'li' ) )
newBlock = node;
else {
newBlock = doc.createElement( blockTag );
if ( previousBlock && ( newBlockDir = previousBlock.getDirection() ) )
newBlock.setAttribute( 'dir', newBlockDir );
}
}
// Force the enter block unless we're talking of a list item.
else if ( forceMode && !newBlock.is( 'li' ) ) {
newBlock.renameNode( blockTag );
}
// Recreate the inline elements tree, which was available
// before hitting enter, so the same styles will be available in
// the new block.
var elementPath = splitInfo.elementPath;
if ( elementPath ) {
for ( var i = 0, len = elementPath.elements.length; i < len; i++ ) {
var element = elementPath.elements[ i ];
if ( element.equals( elementPath.block ) || element.equals( elementPath.blockLimit ) )
break;
if ( CKEDITOR.dtd.$removeEmpty[ element.getName() ] ) {
element = element.clone();
newBlock.moveChildren( element );
newBlock.append( element );
}
}
}
newBlock.appendBogus();
if ( !newBlock.getParent() )
range.insertNode( newBlock );
// list item start number should not be duplicated (https://dev.ckeditor.com/ticket/7330), but we need
// to remove the attribute after it's onto the DOM tree because of old IEs (https://dev.ckeditor.com/ticket/7581).
newBlock.is( 'li' ) && newBlock.removeAttribute( 'value' );
// This is tricky, but to make the new block visible correctly
// we must select it.
// The previousBlock check has been included because it may be
// empty if we have fixed a block-less space (like ENTER into an
// empty table cell).
if ( CKEDITOR.env.ie && isStartOfBlock && ( !isEndOfBlock || !previousBlock.getChildCount() ) ) {
// Move the selection to the new block.
range.moveToElementEditStart( isEndOfBlock ? previousBlock : newBlock );
range.select();
}
// Move the selection to the new block.
range.moveToElementEditStart( isStartOfBlock && !isEndOfBlock ? nextBlock : newBlock );
}
range.select();
range.scrollIntoView();
// ===== HELPERS =====
function preventExtraLineInsideTable( mode ) {
// #1816
// We want to have behaviour after pressing enter like this:
// 1. ^ ->
// 2. Foo^ -> Foo
// 3. Foo^Bar -> Foo
Bar
// We need to separate 1. case to not add extra line. Like it happen for 2nd or 3rd option.
var innerElement,
bogus;
if ( mode === CKEDITOR.ENTER_BR ) {
return false;
}
if ( CKEDITOR.tools.indexOf( [ 'td', 'th' ], path.lastElement.getName() ) === -1 ) {
return false;
}
if ( path.lastElement.getChildCount() !== 1 ) {
return false;
}
innerElement = path.lastElement.getChild( 0 ).clone( true );
bogus = innerElement.getBogus();
if ( bogus ) {
bogus.remove();
}
if ( innerElement.getText().length ) {
return false;
}
return true;
}
},
enterBr: function( editor, mode, range, forceMode ) {
// Get the range for the current selection.
range = range || getRange( editor );
// We may not have valid ranges to work on, like when inside a
// contenteditable=false element.
if ( !range )
return;
var doc = range.document;
var isEndOfBlock = range.checkEndOfBlock();
var elementPath = new CKEDITOR.dom.elementPath( editor.getSelection().getStartElement() );
var startBlock = elementPath.block,
startBlockTag = startBlock && elementPath.block.getName();
if ( !forceMode && startBlockTag == 'li' ) {
enterBlock( editor, mode, range, forceMode );
return;
}
// If we are at the end of a header block.
if ( !forceMode && isEndOfBlock && headerTagRegex.test( startBlockTag ) ) {
var newBlock, newBlockDir;
if ( ( newBlockDir = startBlock.getDirection() ) ) {
newBlock = doc.createElement( 'div' );
newBlock.setAttribute( 'dir', newBlockDir );
newBlock.insertAfter( startBlock );
range.setStart( newBlock, 0 );
} else {
// Insert a after the current paragraph.
doc.createElement( 'br' ).insertAfter( startBlock );
// A text node is required by Gecko only to make the cursor blink.
if ( CKEDITOR.env.gecko )
doc.createText( '' ).insertAfter( startBlock );
// IE has different behaviors regarding position.
range.setStartAt( startBlock.getNext(), CKEDITOR.env.ie ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_START );
}
} else {
var lineBreak;
// IE<8 prefers text node as line-break inside of (https://dev.ckeditor.com/ticket/4711).
if ( startBlockTag == 'pre' && CKEDITOR.env.ie && CKEDITOR.env.version < 8 )
lineBreak = doc.createText( '\r' );
else
lineBreak = doc.createElement( 'br' );
range.deleteContents();
range.insertNode( lineBreak );
// Old IEs have different behavior regarding position.
if ( !CKEDITOR.env.needsBrFiller )
range.setStartAt( lineBreak, CKEDITOR.POSITION_AFTER_END );
else {
// A text node is required by Gecko only to make the cursor blink.
// We need some text inside of it, so the bogus is properly
// created.
doc.createText( '\ufeff' ).insertAfter( lineBreak );
// If we are at the end of a block, we must be sure the bogus node is available in that block.
if ( isEndOfBlock ) {
// In most situations we've got an elementPath.block (e.g. ), but in a
// blockless editor or when autoP is false that needs to be a block limit.
( startBlock || elementPath.blockLimit ).appendBogus();
}
// Now we can remove the text node contents, so the caret doesn't
// stop on it.
lineBreak.getNext().$.nodeValue = '';
range.setStartAt( lineBreak.getNext(), CKEDITOR.POSITION_AFTER_START );
}
}
// This collapse guarantees the cursor will be blinking.
range.collapse( true );
range.select();
range.scrollIntoView();
}
};
plugin = CKEDITOR.plugins.enterkey;
enterBr = plugin.enterBr;
enterBlock = plugin.enterBlock;
headerTagRegex = /^h[1-6]$/;
function shiftEnter( editor ) {
// On SHIFT+ENTER:
// 1. We want to enforce the mode to be respected, instead
// of cloning the current block. (https://dev.ckeditor.com/ticket/77)
return enter( editor, editor.activeShiftEnterMode, 1 );
}
function enter( editor, mode, forceMode ) {
forceMode = editor.config.forceEnterMode || forceMode;
// Only effective within document.
if ( editor.mode != 'wysiwyg' )
return;
if ( !mode )
mode = editor.activeEnterMode;
// TODO this should be handled by setting editor.activeEnterMode on selection change.
// Check path block specialities:
// 1. Cannot be a un-splittable element, e.g. table caption;
var path = editor.elementPath();
if ( path && !path.isContextFor( 'p' ) ) {
mode = CKEDITOR.ENTER_BR;
forceMode = 1;
}
editor.fire( 'saveSnapshot' ); // Save undo step.
if ( mode == CKEDITOR.ENTER_BR )
enterBr( editor, mode, null, forceMode );
else
enterBlock( editor, mode, null, forceMode );
editor.fire( 'saveSnapshot' );
}
function getRange( editor ) {
// Get the selection ranges.
var ranges = editor.getSelection().getRanges( true );
// Delete the contents of all ranges except the first one.
for ( var i = ranges.length - 1; i > 0; i-- ) {
ranges[ i ].deleteContents();
}
// Return the first range.
return ranges[ 0 ];
}
function replaceRangeWithClosestEditableRoot( range ) {
var closestEditable = range.startContainer.getAscendant( function( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'true';
}, true );
if ( range.root.equals( closestEditable ) ) {
return range;
} else {
var newRange = new CKEDITOR.dom.range( closestEditable );
newRange.moveToRange( range );
return newRange;
}
}
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
( function() {
var win = CKEDITOR.document.getWindow(),
pixelate = CKEDITOR.tools.cssLength;
CKEDITOR.plugins.add( 'floatingspace', {
init: function( editor ) {
// Add listener with lower priority than that in themedui creator.
// Thereby floatingspace will be created only if themedui wasn't used.
editor.on( 'loaded', function() {
attach( this );
}, null, null, 20 );
}
} );
function scrollOffset( side ) {
var pageOffset = side == 'left' ? 'pageXOffset' : 'pageYOffset',
docScrollOffset = side == 'left' ? 'scrollLeft' : 'scrollTop';
return ( pageOffset in win.$ ) ? win.$[ pageOffset ] : CKEDITOR.document.$.documentElement[ docScrollOffset ];
}
function attach( editor ) {
var config = editor.config,
// Get the HTML for the predefined spaces.
topHtml = editor.fire( 'uiSpace', { space: 'top', html: '' } ).html,
// Re-positioning of the space.
layout = ( function() {
// Mode indicates the vertical aligning mode.
var mode, editable,
spaceRect, editorRect, viewRect, spaceHeight, pageScrollX,
// Allow minor adjustments of the float space from custom configs.
dockedOffsetX = config.floatSpaceDockedOffsetX || 0,
dockedOffsetY = config.floatSpaceDockedOffsetY || 0,
pinnedOffsetX = config.floatSpacePinnedOffsetX || 0,
pinnedOffsetY = config.floatSpacePinnedOffsetY || 0;
// Update the float space position.
function updatePos( pos, prop, val ) {
floatSpace.setStyle( prop, pixelate( val ) );
floatSpace.setStyle( 'position', pos );
}
// Change the current mode and update float space position accordingly.
function changeMode( newMode ) {
var editorPos = editable.getDocumentPosition();
switch ( newMode ) {
case 'top':
updatePos( 'absolute', 'top', editorPos.y - spaceHeight - dockedOffsetY );
break;
case 'pin':
updatePos( 'fixed', 'top', pinnedOffsetY );
break;
case 'bottom':
updatePos( 'absolute', 'top', editorPos.y + ( editorRect.height || editorRect.bottom - editorRect.top ) + dockedOffsetY );
break;
}
mode = newMode;
}
return function( evt ) {
// https://dev.ckeditor.com/ticket/10112 Do not fail on editable-less editor.
if ( !( editable = editor.editable() ) )
return;
var show = ( evt && evt.name == 'focus' );
// Show up the space on focus gain.
if ( show ) {
floatSpace.show();
}
editor.fire( 'floatingSpaceLayout', { show: show } );
// Reset the horizontal position for below measurement.
floatSpace.removeStyle( 'left' );
floatSpace.removeStyle( 'right' );
// Compute the screen position from the TextRectangle object would
// be very simple, even though the "width"/"height" property is not
// available for all, it's safe to figure that out from the rest.
// http://help.dottoro.com/ljgupwlp.php
spaceRect = floatSpace.getClientRect();
editorRect = editable.getClientRect();
viewRect = win.getViewPaneSize();
spaceHeight = spaceRect.height;
pageScrollX = scrollOffset( 'left' );
// We initialize it as pin mode.
if ( !mode ) {
mode = 'pin';
changeMode( 'pin' );
// Call for a refresh to the actual layout.
layout( evt );
return;
}
// +------------------------ Viewport -+ \
// | | |-> floatSpaceDockedOffsetY
// | ................................. | /
// | |
// | +------ Space -+ |
// | | | |
// | +--------------+ |
// | +------------------ Editor -+ |
// | | | |
//
if ( spaceHeight + dockedOffsetY <= editorRect.top )
changeMode( 'top' );
// +- - - - - - - - - Editor -+
// | |
// +------------------------ Viewport -+ \
// | | | | |-> floatSpacePinnedOffsetY
// | ................................. | /
// | +------ Space -+ | |
// | | | | |
// | +--------------+ | |
// | | | |
// | +---------------------------+ |
// +-----------------------------------+
//
else if ( spaceHeight + dockedOffsetY > viewRect.height - editorRect.bottom )
changeMode( 'pin' );
// +- - - - - - - - - Editor -+
// | |
// +------------------------ Viewport -+ \
// | | | | |-> floatSpacePinnedOffsetY
// | ................................. | /
// | | | |
// | | | |
// | +---------------------------+ |
// | +------ Space -+ |
// | | | |
// | +--------------+ |
//
else
changeMode( 'bottom' );
var mid = viewRect.width / 2,
alignSide, offset;
if ( config.floatSpacePreferRight ) {
alignSide = 'right';
} else if ( editorRect.left > 0 && editorRect.right < viewRect.width && editorRect.width > spaceRect.width ) {
alignSide = config.contentsLangDirection == 'rtl' ? 'right' : 'left';
} else {
alignSide = mid - editorRect.left > editorRect.right - mid ? 'left' : 'right';
}
// (https://dev.ckeditor.com/ticket/9769) If viewport width is less than space width,
// make sure space never cross the left boundary of the viewport.
// In other words: top-left corner of the space is always visible.
if ( spaceRect.width > viewRect.width ) {
alignSide = 'left';
offset = 0;
}
else {
if ( alignSide == 'left' ) {
// If the space rect fits into viewport, align it
// to the left edge of editor:
//
// +------------------------ Viewport -+
// | |
// | +------------- Space -+ |
// | | | |
// | +---------------------+ |
// | +------------------ Editor -+ |
// | | | |
//
if ( editorRect.left > 0 )
offset = editorRect.left;
// If the left part of the editor is cut off by the left
// edge of the viewport, stick the space to the viewport:
//
// +------------------------ Viewport -+
// | |
// +---------------- Space -+ |
// | | |
// +------------------------+ |
// +----|------------- Editor -+ |
// | | | |
//
else
offset = 0;
}
else {
// If the space rect fits into viewport, align it
// to the right edge of editor:
//
// +------------------------ Viewport -+
// | |
// | +------------- Space -+ |
// | | | |
// | +---------------------+ |
// | +------------------ Editor -+ |
// | | | |
//
if ( editorRect.right < viewRect.width )
offset = viewRect.width - editorRect.right;
// If the right part of the editor is cut off by the right
// edge of the viewport, stick the space to the viewport:
//
// +------------------------ Viewport -+
// | |
// | +------------- Space -+
// | | |
// | +---------------------+
// | +-----------------|- Editor -+
// | | | |
//
else
offset = 0;
}
// (https://dev.ckeditor.com/ticket/9769) Finally, stick the space to the opposite side of
// the viewport when it's cut off horizontally on the left/right
// side like below.
//
// This trick reveals cut off space in some edge cases and
// hence it improves accessibility.
//
// +------------------------ Viewport -+
// | |
// | +--------------------|-- Space -+
// | | | |
// | +--------------------|----------+
// | +------- Editor -+ |
// | | | |
//
// becomes:
//
// +------------------------ Viewport -+
// | |
// | +----------------------- Space -+
// | | |
// | +-------------------------------+
// | +------- Editor -+ |
// | | | |
//
if ( offset + spaceRect.width > viewRect.width ) {
alignSide = alignSide == 'left' ? 'right' : 'left';
offset = 0;
}
}
// Pin mode is fixed, so don't include scroll-x.
// (https://dev.ckeditor.com/ticket/9903) For mode is "top" or "bottom", add opposite scroll-x for right-aligned space.
var scroll = mode == 'pin' ? 0 : alignSide == 'left' ? pageScrollX : -pageScrollX;
floatSpace.setStyle( alignSide, pixelate( ( mode == 'pin' ? pinnedOffsetX : dockedOffsetX ) + offset + scroll ) );
};
} )();
if ( topHtml ) {
var floatSpaceTpl = new CKEDITOR.template(
'
' +
( editor.title ? '
{voiceLabel} ' : ' ' ) +
'
' +
'
' ),
floatSpace = CKEDITOR.document.getBody().append( CKEDITOR.dom.element.createFromHtml( floatSpaceTpl.output( {
content: topHtml,
id: editor.id,
langDir: editor.lang.dir,
langCode: editor.langCode,
name: editor.name,
style: 'display:none;z-index:' + ( config.baseFloatZIndex - 1 ),
topId: editor.ui.spaceId( 'top' ),
voiceLabel: editor.title
} ) ) ),
// Use event buffers to reduce CPU load when tons of events are fired.
changeBuffer = CKEDITOR.tools.eventsBuffer( 500, layout ),
uiBuffer = CKEDITOR.tools.eventsBuffer( 100, layout );
// There's no need for the floatSpace to be selectable.
floatSpace.unselectable();
// Prevent clicking on non-buttons area of the space from blurring editor.
floatSpace.on( 'mousedown', function( evt ) {
evt = evt.data;
if ( !evt.getTarget().hasAscendant( 'a', 1 ) )
evt.preventDefault();
} );
editor.on( 'focus', function( evt ) {
layout( evt );
editor.on( 'change', changeBuffer.input );
win.on( 'scroll', uiBuffer.input );
win.on( 'resize', uiBuffer.input );
} );
editor.on( 'blur', function() {
floatSpace.hide();
editor.removeListener( 'change', changeBuffer.input );
win.removeListener( 'scroll', uiBuffer.input );
win.removeListener( 'resize', uiBuffer.input );
} );
editor.on( 'destroy', function() {
win.removeListener( 'scroll', uiBuffer.input );
win.removeListener( 'resize', uiBuffer.input );
floatSpace.clearCustomData();
floatSpace.remove();
} );
// Handle initial focus.
if ( editor.focusManager.hasFocus )
floatSpace.show();
// Register this UI space to the focus manager.
editor.focusManager.add( floatSpace, 1 );
}
}
} )();
/**
* Along with {@link #floatSpaceDockedOffsetY} it defines the
* amount of offset (in pixels) between the float space and the editable left/right
* boundaries when the space element is docked on either side of the editable.
*
* config.floatSpaceDockedOffsetX = 10;
*
* @cfg {Number} [floatSpaceDockedOffsetX=0]
* @member CKEDITOR.config
*/
/**
* Along with {@link #floatSpaceDockedOffsetX} it defines the
* amount of offset (in pixels) between the float space and the editable top/bottom
* boundaries when the space element is docked on either side of the editable.
*
* config.floatSpaceDockedOffsetY = 10;
*
* @cfg {Number} [floatSpaceDockedOffsetY=0]
* @member CKEDITOR.config
*/
/**
* Along with {@link #floatSpacePinnedOffsetY} it defines the
* amount of offset (in pixels) between the float space and the viewport boundaries
* when the space element is pinned.
*
* config.floatSpacePinnedOffsetX = 20;
*
* @cfg {Number} [floatSpacePinnedOffsetX=0]
* @member CKEDITOR.config
*/
/**
* Along with {@link #floatSpacePinnedOffsetX} it defines the
* amount of offset (in pixels) between the float space and the viewport boundaries
* when the space element is pinned.
*
* config.floatSpacePinnedOffsetY = 20;
*
* @cfg {Number} [floatSpacePinnedOffsetY=0]
* @member CKEDITOR.config
*/
/**
* Indicates that the float space should be aligned to the right side
* of the editable area rather than to the left (if possible).
*
* config.floatSpacePreferRight = true;
*
* @since 4.5.0
* @cfg {Boolean} [floatSpacePreferRight=false]
* @member CKEDITOR.config
*/
/**
* Fired when the viewport or editor parameters change and the floating space needs to check and
* eventually update its position and dimensions.
*
* @since 4.5.0
* @event floatingSpaceLayout
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor The editor instance.
* @param data
* @param {Boolean} data.show True if the float space should show up as a result of this event.
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
CKEDITOR.plugins.add( 'listblock', {
requires: 'panel',
onLoad: function() {
var list = CKEDITOR.addTemplate( 'panel-list', ' ' ),
listItem = CKEDITOR.addTemplate( 'panel-list-item', ' ' +
'' +
'{text}' +
' ' +
' ' ),
listGroup = CKEDITOR.addTemplate( 'panel-list-group', '{label} ' ),
reSingleQuote = /\'/g,
escapeSingleQuotes = function( str ) {
return str.replace( reSingleQuote, '\\\'' );
};
CKEDITOR.ui.panel.prototype.addListBlock = function( name, definition ) {
return this.addBlock( name, new CKEDITOR.ui.listBlock( this.getHolderElement(), definition ) );
};
CKEDITOR.ui.listBlock = CKEDITOR.tools.createClass( {
base: CKEDITOR.ui.panel.block,
$: function( blockHolder, blockDefinition ) {
blockDefinition = blockDefinition || {};
var attribs = blockDefinition.attributes || ( blockDefinition.attributes = {} );
( this.multiSelect = !!blockDefinition.multiSelect ) && ( attribs[ 'aria-multiselectable' ] = true );
// Provide default role of 'listbox'.
!attribs.role && ( attribs.role = 'listbox' );
// Call the base contructor.
this.base.apply( this, arguments );
// Set the proper a11y attributes.
this.element.setAttribute( 'role', attribs.role );
var keys = this.keys;
keys[ 40 ] = 'next'; // ARROW-DOWN
keys[ 9 ] = 'next'; // TAB
keys[ 38 ] = 'prev'; // ARROW-UP
keys[ CKEDITOR.SHIFT + 9 ] = 'prev'; // SHIFT + TAB
keys[ 32 ] = CKEDITOR.env.ie ? 'mouseup' : 'click'; // SPACE
CKEDITOR.env.ie && ( keys[ 13 ] = 'mouseup' ); // Manage ENTER, since onclick is blocked in IE (https://dev.ckeditor.com/ticket/8041).
this._.pendingHtml = [];
this._.pendingList = [];
this._.items = {};
this._.groups = {};
},
_: {
close: function() {
if ( this._.started ) {
var output = list.output( { items: this._.pendingList.join( '' ) } );
this._.pendingList = [];
this._.pendingHtml.push( output );
delete this._.started;
}
},
getClick: function() {
if ( !this._.click ) {
this._.click = CKEDITOR.tools.addFunction( function( value ) {
var marked = this.toggle( value );
if ( this.onClick )
this.onClick( value, marked );
}, this );
}
return this._.click;
}
},
proto: {
add: function( value, html, title ) {
var id = CKEDITOR.tools.getNextId();
if ( !this._.started ) {
this._.started = 1;
this._.size = this._.size || 0;
}
this._.items[ value ] = id;
var data = {
id: id,
val: escapeSingleQuotes( CKEDITOR.tools.htmlEncodeAttr( value ) ),
onclick: CKEDITOR.env.ie ? 'onclick="return false;" onmouseup' : 'onclick',
clickFn: this._.getClick(),
title: CKEDITOR.tools.htmlEncodeAttr( title || value ),
text: html || value
};
this._.pendingList.push( listItem.output( data ) );
},
startGroup: function( title ) {
this._.close();
var id = CKEDITOR.tools.getNextId();
this._.groups[ title ] = id;
this._.pendingHtml.push( listGroup.output( { id: id, label: title } ) );
},
commit: function() {
this._.close();
this.element.appendHtml( this._.pendingHtml.join( '' ) );
delete this._.size;
this._.pendingHtml = [];
},
toggle: function( value ) {
var isMarked = this.isMarked( value );
if ( isMarked )
this.unmark( value );
else
this.mark( value );
return !isMarked;
},
hideGroup: function( groupTitle ) {
var group = this.element.getDocument().getById( this._.groups[ groupTitle ] ),
list = group && group.getNext();
if ( group ) {
group.setStyle( 'display', 'none' );
if ( list && list.getName() == 'ul' )
list.setStyle( 'display', 'none' );
}
},
hideItem: function( value ) {
this.element.getDocument().getById( this._.items[ value ] ).setStyle( 'display', 'none' );
},
showAll: function() {
var items = this._.items,
groups = this._.groups,
doc = this.element.getDocument();
for ( var value in items ) {
doc.getById( items[ value ] ).setStyle( 'display', '' );
}
for ( var title in groups ) {
var group = doc.getById( groups[ title ] ),
list = group.getNext();
group.setStyle( 'display', '' );
if ( list && list.getName() == 'ul' )
list.setStyle( 'display', '' );
}
},
mark: function( value ) {
if ( !this.multiSelect )
this.unmarkAll();
var itemId = this._.items[ value ],
item = this.element.getDocument().getById( itemId );
item.addClass( 'cke_selected' );
this.element.getDocument().getById( itemId + '_option' ).setAttribute( 'aria-selected', true );
this.onMark && this.onMark( item );
},
markFirstDisplayed: function() {
var context = this;
this._.markFirstDisplayed( function() {
if ( !context.multiSelect )
context.unmarkAll();
} );
},
unmark: function( value ) {
var doc = this.element.getDocument(),
itemId = this._.items[ value ],
item = doc.getById( itemId );
item.removeClass( 'cke_selected' );
doc.getById( itemId + '_option' ).removeAttribute( 'aria-selected' );
this.onUnmark && this.onUnmark( item );
},
unmarkAll: function() {
var items = this._.items,
doc = this.element.getDocument();
for ( var value in items ) {
var itemId = items[ value ];
doc.getById( itemId ).removeClass( 'cke_selected' );
doc.getById( itemId + '_option' ).removeAttribute( 'aria-selected' );
}
this.onUnmark && this.onUnmark();
},
isMarked: function( value ) {
return this.element.getDocument().getById( this._.items[ value ] ).hasClass( 'cke_selected' );
},
focus: function( value ) {
this._.focusIndex = -1;
var links = this.element.getElementsByTag( 'a' ),
link,
selected,
i = -1;
if ( value ) {
selected = this.element.getDocument().getById( this._.items[ value ] ).getFirst();
while ( ( link = links.getItem( ++i ) ) ) {
if ( link.equals( selected ) ) {
this._.focusIndex = i;
break;
}
}
}
else {
this.element.focus();
}
selected && setTimeout( function() {
selected.focus();
}, 0 );
}
}
} );
}
} );
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
CKEDITOR.plugins.add( 'richcombo', {
requires: 'floatpanel,listblock,button',
beforeInit: function( editor ) {
editor.ui.addHandler( CKEDITOR.UI_RICHCOMBO, CKEDITOR.ui.richCombo.handler );
}
} );
( function() {
var template = '' +
'{label} ' +
'' +
'{label} ' +
'' +
'' +
// BLACK DOWN-POINTING TRIANGLE
( CKEDITOR.env.hc ? '▼' : CKEDITOR.env.air ? ' ' : '' ) +
' ' +
' ' +
' ' +
' ';
var rcomboTpl = CKEDITOR.addTemplate( 'combo', template );
/**
* Button UI element.
*
* @readonly
* @property {String} [='richcombo']
* @member CKEDITOR
*/
CKEDITOR.UI_RICHCOMBO = 'richcombo';
/**
* @class
* @todo
*/
CKEDITOR.ui.richCombo = CKEDITOR.tools.createClass( {
$: function( definition ) {
// Copy all definition properties to this object.
CKEDITOR.tools.extend( this, definition,
// Set defaults.
{
// The combo won't participate in toolbar grouping.
canGroup: false,
title: definition.label,
modes: { wysiwyg: 1 },
editorFocus: 1
} );
// We don't want the panel definition in this object.
var panelDefinition = this.panel || {};
delete this.panel;
this.id = CKEDITOR.tools.getNextNumber();
this.document = ( panelDefinition.parent && panelDefinition.parent.getDocument() ) || CKEDITOR.document;
panelDefinition.className = 'cke_combopanel';
panelDefinition.block = {
multiSelect: panelDefinition.multiSelect,
attributes: panelDefinition.attributes
};
panelDefinition.toolbarRelated = true;
this._ = {
panelDefinition: panelDefinition,
items: {},
listeners: []
};
},
proto: {
renderHtml: function( editor ) {
var output = [];
this.render( editor, output );
return output.join( '' );
},
/**
* Renders the rich combo.
*
* @param {CKEDITOR.editor} editor The editor instance which this button is
* to be used by.
* @param {Array} output The output array that the HTML relative
* to this button will be appended to.
*/
render: function( editor, output ) {
var env = CKEDITOR.env;
var id = 'cke_' + this.id;
var clickFn = CKEDITOR.tools.addFunction( function( el ) {
// Restore locked selection in Opera.
if ( selLocked ) {
editor.unlockSelection( 1 );
selLocked = 0;
}
instance.execute( el );
}, this );
var combo = this;
var instance = {
id: id,
combo: this,
focus: function() {
var element = CKEDITOR.document.getById( id ).getChild( 1 );
element.focus();
},
execute: function( el ) {
var _ = combo._;
if ( _.state == CKEDITOR.TRISTATE_DISABLED )
return;
combo.createPanel( editor );
if ( _.on ) {
_.panel.hide();
return;
}
combo.commit();
var value = combo.getValue();
if ( value )
_.list.mark( value );
else
_.list.unmarkAll();
_.panel.showBlock( combo.id, new CKEDITOR.dom.element( el ), 4 );
},
clickFn: clickFn
};
function updateState() {
// Don't change state while richcombo is active (https://dev.ckeditor.com/ticket/11793).
if ( this.getState() == CKEDITOR.TRISTATE_ON )
return;
var state = this.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED;
if ( editor.readOnly && !this.readOnly )
state = CKEDITOR.TRISTATE_DISABLED;
this.setState( state );
this.setValue( '' );
// Let plugin to disable button.
if ( state != CKEDITOR.TRISTATE_DISABLED && this.refresh )
this.refresh();
}
// Update status when activeFilter, mode, selection or readOnly changes.
this._.listeners.push( editor.on( 'activeFilterChange', updateState, this ) );
this._.listeners.push( editor.on( 'mode', updateState, this ) );
this._.listeners.push( editor.on( 'selectionChange', updateState, this ) );
// If this combo is sensitive to readOnly state, update it accordingly.
!this.readOnly && this._.listeners.push( editor.on( 'readOnly', updateState, this ) );
var keyDownFn = CKEDITOR.tools.addFunction( function( ev, element ) {
ev = new CKEDITOR.dom.event( ev );
var keystroke = ev.getKeystroke();
switch ( keystroke ) {
case 13: // ENTER
case 32: // SPACE
case 40: // ARROW-DOWN
// Show panel
CKEDITOR.tools.callFunction( clickFn, element );
break;
default:
// Delegate the default behavior to toolbar button key handling.
instance.onkey( instance, keystroke );
}
// Avoid subsequent focus grab on editor document.
ev.preventDefault();
} );
var focusFn = CKEDITOR.tools.addFunction( function() {
instance.onfocus && instance.onfocus();
} );
var selLocked = 0;
// For clean up
instance.keyDownFn = keyDownFn;
var params = {
id: id,
name: this.name || this.command,
label: this.label,
title: this.title,
cls: this.className || '',
titleJs: env.gecko && !env.hc ? '' : ( this.title || '' ).replace( "'", '' ),
keydownFn: keyDownFn,
focusFn: focusFn,
clickFn: clickFn
};
rcomboTpl.output( params, output );
if ( this.onRender )
this.onRender();
return instance;
},
createPanel: function( editor ) {
if ( this._.panel )
return;
var panelDefinition = this._.panelDefinition,
panelBlockDefinition = this._.panelDefinition.block,
panelParentElement = panelDefinition.parent || CKEDITOR.document.getBody(),
namedPanelCls = 'cke_combopanel__' + this.name,
panel = new CKEDITOR.ui.floatPanel( editor, panelParentElement, panelDefinition ),
list = panel.addListBlock( this.id, panelBlockDefinition ),
me = this;
panel.onShow = function() {
this.element.addClass( namedPanelCls );
me.setState( CKEDITOR.TRISTATE_ON );
me._.on = 1;
me.editorFocus && !editor.focusManager.hasFocus && editor.focus();
if ( me.onOpen )
me.onOpen();
};
panel.onHide = function( preventOnClose ) {
this.element.removeClass( namedPanelCls );
me.setState( me.modes && me.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
me._.on = 0;
if ( !preventOnClose && me.onClose )
me.onClose();
};
panel.onEscape = function() {
// Hide drop-down with focus returned.
panel.hide( 1 );
};
list.onClick = function( value, marked ) {
if ( me.onClick )
me.onClick.call( me, value, marked );
panel.hide();
};
this._.panel = panel;
this._.list = list;
panel.getBlock( this.id ).onHide = function() {
me._.on = 0;
me.setState( CKEDITOR.TRISTATE_OFF );
};
if ( this.init )
this.init();
},
setValue: function( value, text ) {
this._.value = value;
var textElement = this.document.getById( 'cke_' + this.id + '_text' );
if ( textElement ) {
if ( !( value || text ) ) {
text = this.label;
textElement.addClass( 'cke_combo_inlinelabel' );
} else {
textElement.removeClass( 'cke_combo_inlinelabel' );
}
textElement.setText( typeof text != 'undefined' ? text : value );
}
},
getValue: function() {
return this._.value || '';
},
unmarkAll: function() {
this._.list.unmarkAll();
},
mark: function( value ) {
this._.list.mark( value );
},
hideItem: function( value ) {
this._.list.hideItem( value );
},
hideGroup: function( groupTitle ) {
this._.list.hideGroup( groupTitle );
},
showAll: function() {
this._.list.showAll();
},
add: function( value, html, text ) {
this._.items[ value ] = text || value;
this._.list.add( value, html, text );
},
startGroup: function( title ) {
this._.list.startGroup( title );
},
commit: function() {
if ( !this._.committed ) {
this._.list.commit();
this._.committed = 1;
CKEDITOR.ui.fire( 'ready', this );
}
this._.committed = 1;
},
setState: function( state ) {
if ( this._.state == state )
return;
var el = this.document.getById( 'cke_' + this.id );
el.setState( state, 'cke_combo' );
state == CKEDITOR.TRISTATE_DISABLED ?
el.setAttribute( 'aria-disabled', true ) :
el.removeAttribute( 'aria-disabled' );
this._.state = state;
},
getState: function() {
return this._.state;
},
enable: function() {
if ( this._.state == CKEDITOR.TRISTATE_DISABLED )
this.setState( this._.lastState );
},
disable: function() {
if ( this._.state != CKEDITOR.TRISTATE_DISABLED ) {
this._.lastState = this._.state;
this.setState( CKEDITOR.TRISTATE_DISABLED );
}
},
/**
* Removes all listeners from a rich combo element.
*
* @since 4.11.0
*/
destroy: function() {
CKEDITOR.tools.array.forEach( this._.listeners, function( listener ) {
listener.removeListener();
} );
this._.listeners = [];
}
},
/**
* Represents a rich combo handler object.
*
* @class CKEDITOR.ui.richCombo.handler
* @singleton
* @extends CKEDITOR.ui.handlerDefinition
*/
statics: {
handler: {
/**
* Transforms a rich combo definition into a {@link CKEDITOR.ui.richCombo} instance.
*
* @param {Object} definition
* @returns {CKEDITOR.ui.richCombo}
*/
create: function( definition ) {
return new CKEDITOR.ui.richCombo( definition );
}
}
}
} );
/**
* @param {String} name
* @param {Object} definition
* @member CKEDITOR.ui
* @todo
*/
CKEDITOR.ui.prototype.addRichCombo = function( name, definition ) {
this.add( name, CKEDITOR.UI_RICHCOMBO, definition );
};
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
CKEDITOR.plugins.add( 'format', {
requires: 'richcombo',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
if ( editor.blockless )
return;
var config = editor.config,
lang = editor.lang.format;
// Gets the list of tags from the settings.
var tags = config.format_tags.split( ';' );
// Create style objects for all defined styles.
var styles = {},
stylesCount = 0,
allowedContent = [];
for ( var i = 0; i < tags.length; i++ ) {
var tag = tags[ i ];
var style = new CKEDITOR.style( config[ 'format_' + tag ] );
if ( !editor.filter.customConfig || editor.filter.check( style ) ) {
stylesCount++;
styles[ tag ] = style;
styles[ tag ]._.enterMode = editor.config.enterMode;
allowedContent.push( style );
}
}
// Hide entire combo when all formats are rejected.
if ( stylesCount === 0 )
return;
editor.ui.addRichCombo( 'Format', {
label: lang.label,
title: lang.panelTitle,
toolbar: 'styles,20',
allowedContent: allowedContent,
panel: {
css: [ CKEDITOR.skin.getPath( 'editor' ) ].concat( config.contentsCss ),
multiSelect: false,
attributes: { 'aria-label': lang.panelTitle }
},
init: function() {
this.startGroup( lang.panelTitle );
for ( var tag in styles ) {
var label = lang[ 'tag_' + tag ];
// Add the tag entry to the panel list.
this.add( tag, styles[ tag ].buildPreview( label ), label );
}
},
onClick: function( value ) {
editor.focus();
editor.fire( 'saveSnapshot' );
var style = styles[ value ],
elementPath = editor.elementPath();
// Always apply style, do not allow to toggle it by clicking on corresponding list item (#584).
if ( !style.checkActive( elementPath, editor ) ) {
editor.applyStyle( style );
}
// Save the undo snapshot after all changes are affected. (https://dev.ckeditor.com/ticket/4899)
setTimeout( function() {
editor.fire( 'saveSnapshot' );
}, 0 );
},
onRender: function() {
editor.on( 'selectionChange', function( ev ) {
var currentTag = this.getValue(),
elementPath = ev.data.path;
this.refresh();
for ( var tag in styles ) {
if ( styles[ tag ].checkActive( elementPath, editor ) ) {
if ( tag != currentTag )
this.setValue( tag, editor.lang.format[ 'tag_' + tag ] );
return;
}
}
// If no styles match, just empty it.
this.setValue( '' );
}, this );
},
onOpen: function() {
this.showAll();
for ( var name in styles ) {
var style = styles[ name ];
// Check if that style is enabled in activeFilter.
if ( !editor.activeFilter.check( style ) )
this.hideItem( name );
}
},
refresh: function() {
var elementPath = editor.elementPath();
if ( !elementPath )
return;
// Check if element path contains 'p' element.
if ( !elementPath.isContextFor( 'p' ) ) {
this.setState( CKEDITOR.TRISTATE_DISABLED );
return;
}
// Check if there is any available style.
for ( var name in styles ) {
if ( editor.activeFilter.check( styles[ name ] ) )
return;
}
this.setState( CKEDITOR.TRISTATE_DISABLED );
}
} );
}
} );
/**
* A list of semicolon-separated style names (by default: tags) representing
* the style definition for each entry to be displayed in the Format drop-down list
* in the toolbar. Each entry must have a corresponding configuration in a
* setting named `'format_(tagName)'`. For example, the `'p'` entry has its
* definition taken from [config.format_p](#!/api/CKEDITOR.config-cfg-format_p).
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_tags = 'p;h2;h3;pre';
*
* @cfg {String} [format_tags='p;h1;h2;h3;h4;h5;h6;pre;address;div']
* @member CKEDITOR.config
*/
CKEDITOR.config.format_tags = 'p;h1;h2;h3;h4;h5;h6;pre;address;div';
/**
* The style definition to be used to apply the `Normal` format.
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_p = { element: 'p', attributes: { 'class': 'normalPara' } };
*
* @cfg {Object} [format_p={ element: 'p' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_p = { element: 'p' };
/**
* The style definition to be used to apply the `Normal (DIV)` format.
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_div = { element: 'div', attributes: { 'class': 'normalDiv' } };
*
* @cfg {Object} [format_div={ element: 'div' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_div = { element: 'div' };
/**
* The style definition to be used to apply the `Formatted` format.
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_pre = { element: 'pre', attributes: { 'class': 'code' } };
*
* @cfg {Object} [format_pre={ element: 'pre' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_pre = { element: 'pre' };
/**
* The style definition to be used to apply the `Address` format.
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_address = { element: 'address', attributes: { 'class': 'styledAddress' } };
*
* @cfg {Object} [format_address={ element: 'address' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_address = { element: 'address' };
/**
* The style definition to be used to apply the `Heading 1` format.
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_h1 = { element: 'h1', attributes: { 'class': 'contentTitle1' } };
*
* @cfg {Object} [format_h1={ element: 'h1' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h1 = { element: 'h1' };
/**
* The style definition to be used to apply the `Heading 2` format.
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_h2 = { element: 'h2', attributes: { 'class': 'contentTitle2' } };
*
* @cfg {Object} [format_h2={ element: 'h2' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h2 = { element: 'h2' };
/**
* The style definition to be used to apply the `Heading 3` format.
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_h3 = { element: 'h3', attributes: { 'class': 'contentTitle3' } };
*
* @cfg {Object} [format_h3={ element: 'h3' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h3 = { element: 'h3' };
/**
* The style definition to be used to apply the `Heading 4` format.
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_h4 = { element: 'h4', attributes: { 'class': 'contentTitle4' } };
*
* @cfg {Object} [format_h4={ element: 'h4' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h4 = { element: 'h4' };
/**
* The style definition to be used to apply the `Heading 5` format.
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_h5 = { element: 'h5', attributes: { 'class': 'contentTitle5' } };
*
* @cfg {Object} [format_h5={ element: 'h5' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h5 = { element: 'h5' };
/**
* The style definition to be used to apply the `Heading 6` format.
*
* Read more in the {@glink features/format documentation}
* and see the {@glink examples/format example}.
*
* config.format_h6 = { element: 'h6', attributes: { 'class': 'contentTitle6' } };
*
* @cfg {Object} [format_h6={ element: 'h6' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h6 = { element: 'h6' };
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Horizontal Rule plugin.
*/
( function() {
var horizontalruleCmd = {
canUndo: false, // The undo snapshot will be handled by 'insertElement'.
exec: function( editor ) {
var hr = editor.document.createElement( 'hr' );
editor.insertElement( hr );
},
allowedContent: 'hr',
requiredContent: 'hr'
};
var pluginName = 'horizontalrule';
// Register a plugin named "horizontalrule".
CKEDITOR.plugins.add( pluginName, {
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
if ( editor.blockless )
return;
editor.addCommand( pluginName, horizontalruleCmd );
editor.ui.addButton && editor.ui.addButton( 'HorizontalRule', {
label: editor.lang.horizontalrule.toolbar,
command: pluginName,
toolbar: 'insert,40'
} );
}
} );
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
CKEDITOR.plugins.add( 'htmlwriter', {
init: function( editor ) {
var writer = new CKEDITOR.htmlWriter();
writer.forceSimpleAmpersand = editor.config.forceSimpleAmpersand;
writer.indentationChars = editor.config.dataIndentationChars || '\t';
// Overwrite default basicWriter initialized in hmtlDataProcessor constructor.
editor.dataProcessor.writer = writer;
}
} );
/**
* The class used to write HTML data.
*
* var writer = new CKEDITOR.htmlWriter();
* writer.openTag( 'p' );
* writer.attribute( 'class', 'MyClass' );
* writer.openTagClose( 'p' );
* writer.text( 'Hello' );
* writer.closeTag( 'p' );
* alert( writer.getHtml() ); // 'Hello
'
*
* @class
* @extends CKEDITOR.htmlParser.basicWriter
*/
CKEDITOR.htmlWriter = CKEDITOR.tools.createClass( {
base: CKEDITOR.htmlParser.basicWriter,
/**
* Creates an `htmlWriter` class instance.
*
* @constructor
*/
$: function() {
// Call the base contructor.
this.base();
/**
* The characters to be used for each indentation step.
*
* // Use tab for indentation.
* editorInstance.dataProcessor.writer.indentationChars = '\t';
*/
this.indentationChars = '\t';
/**
* The characters to be used to close "self-closing" elements, like ` ` or ` `.
*
* // Use HTML4 notation for self-closing elements.
* editorInstance.dataProcessor.writer.selfClosingEnd = '>';
*/
this.selfClosingEnd = ' />';
/**
* The characters to be used for line breaks.
*
* // Use CRLF for line breaks.
* editorInstance.dataProcessor.writer.lineBreakChars = '\r\n';
*/
this.lineBreakChars = '\n';
this.sortAttributes = 1;
this._.indent = 0;
this._.indentation = '';
// Indicate preformatted block context status. (https://dev.ckeditor.com/ticket/5789)
this._.inPre = 0;
this._.rules = {};
var dtd = CKEDITOR.dtd;
for ( var e in CKEDITOR.tools.extend( {}, dtd.$nonBodyContent, dtd.$block, dtd.$listItem, dtd.$tableContent ) ) {
this.setRules( e, {
indent: !dtd[ e ][ '#' ],
breakBeforeOpen: 1,
breakBeforeClose: !dtd[ e ][ '#' ],
breakAfterClose: 1,
needsSpace: ( e in dtd.$block ) && !( e in { li: 1, dt: 1, dd: 1 } )
} );
}
this.setRules( 'br', { breakAfterOpen: 1 } );
this.setRules( 'title', {
indent: 0,
breakAfterOpen: 0
} );
this.setRules( 'style', {
indent: 0,
breakBeforeClose: 1
} );
this.setRules( 'pre', {
breakAfterOpen: 1, // Keep line break after the opening tag
indent: 0 // Disable indentation on .
} );
},
proto: {
/**
* Writes the tag opening part for an opener tag.
*
* // Writes ''.
* writer.openTagClose( 'p', false );
*
* // Writes ' />'.
* writer.openTagClose( 'br', true );
*
* @param {String} tagName The element name for this tag.
* @param {Boolean} isSelfClose Indicates that this is a self-closing tag,
* like ` ` or ` `.
*/
openTagClose: function( tagName, isSelfClose ) {
var rules = this._.rules[ tagName ];
if ( isSelfClose ) {
this._.output.push( this.selfClosingEnd );
if ( rules && rules.breakAfterClose )
this._.needsSpace = rules.needsSpace;
} else {
this._.output.push( '>' );
if ( rules && rules.indent )
this._.indentation += this.indentationChars;
}
if ( rules && rules.breakAfterOpen )
this.lineBreak();
tagName == 'pre' && ( this._.inPre = 1 );
},
/**
* Writes an attribute. This function should be called after opening the
* tag with {@link #openTagClose}.
*
* // Writes ' class="MyClass"'.
* writer.attribute( 'class', 'MyClass' );
*
* @param {String} attName The attribute name.
* @param {String} attValue The attribute value.
*/
attribute: function( attName, attValue ) {
if ( typeof attValue == 'string' ) {
// Browsers don't always escape special character in attribute values. (https://dev.ckeditor.com/ticket/4683, https://dev.ckeditor.com/ticket/4719).
attValue = CKEDITOR.tools.htmlEncodeAttr( attValue );
// Run ampersand replacement after the htmlEncodeAttr, otherwise the results are overwritten (#965).
if ( this.forceSimpleAmpersand ) {
attValue = attValue.replace( /&/g, '&' );
}
}
this._.output.push( ' ', attName, '="', attValue, '"' );
},
/**
* Writes a closer tag.
*
* // Writes '
'.
* writer.closeTag( 'p' );
*
* @param {String} tagName The element name for this tag.
*/
closeTag: function( tagName ) {
var rules = this._.rules[ tagName ];
if ( rules && rules.indent )
this._.indentation = this._.indentation.substr( this.indentationChars.length );
if ( this._.indent )
this.indentation();
// Do not break if indenting.
else if ( rules && rules.breakBeforeClose ) {
this.lineBreak();
this.indentation();
}
this._.output.push( '', tagName, '>' );
tagName == 'pre' && ( this._.inPre = 0 );
if ( rules && rules.breakAfterClose ) {
this.lineBreak();
this._.needsSpace = rules.needsSpace;
}
this._.afterCloser = 1;
},
/**
* Writes text.
*
* // Writes 'Hello Word'.
* writer.text( 'Hello Word' );
*
* @param {String} text The text value
*/
text: function( text ) {
if ( this._.indent ) {
this.indentation();
!this._.inPre && ( text = CKEDITOR.tools.ltrim( text ) );
}
this._.output.push( text );
},
/**
* Writes a comment.
*
* // Writes "".
* writer.comment( ' My comment ' );
*
* @param {String} comment The comment text.
*/
comment: function( comment ) {
if ( this._.indent )
this.indentation();
this._.output.push( '' );
},
/**
* Writes a line break. It uses the {@link #lineBreakChars} property for it.
*
* // Writes '\n' (e.g.).
* writer.lineBreak();
*/
lineBreak: function() {
if ( !this._.inPre && this._.output.length > 0 )
this._.output.push( this.lineBreakChars );
this._.indent = 1;
},
/**
* Writes the current indentation character. It uses the {@link #indentationChars}
* property, repeating it for the current indentation steps.
*
* // Writes '\t' (e.g.).
* writer.indentation();
*/
indentation: function() {
if ( !this._.inPre && this._.indentation )
this._.output.push( this._.indentation );
this._.indent = 0;
},
/**
* Empties the current output buffer. It also brings back the default
* values of the writer flags.
*
* writer.reset();
*/
reset: function() {
this._.output = [];
this._.indent = 0;
this._.indentation = '';
this._.afterCloser = 0;
this._.inPre = 0;
this._.needsSpace = 0;
},
/**
* Sets formatting rules for a given element. Possible rules are:
*
* * `indent` – indent the element content.
* * `breakBeforeOpen` – break line before the opener tag for this element.
* * `breakAfterOpen` – break line after the opener tag for this element.
* * `breakBeforeClose` – break line before the closer tag for this element.
* * `breakAfterClose` – break line after the closer tag for this element.
*
* All rules default to `false`. Each function call overrides rules that are
* already present, leaving the undefined ones untouched.
*
* By default, all elements available in the {@link CKEDITOR.dtd#$block},
* {@link CKEDITOR.dtd#$listItem}, and {@link CKEDITOR.dtd#$tableContent}
* lists have all the above rules set to `true`. Additionaly, the ` `
* element has the `breakAfterOpen` rule set to `true`.
*
* // Break line before and after "img" tags.
* writer.setRules( 'img', {
* breakBeforeOpen: true
* breakAfterOpen: true
* } );
*
* // Reset the rules for the "h1" tag.
* writer.setRules( 'h1', {} );
*
* @param {String} tagName The name of the element for which the rules are set.
* @param {Object} rules An object containing the element rules.
*/
setRules: function( tagName, rules ) {
var currentRules = this._.rules[ tagName ];
if ( currentRules )
CKEDITOR.tools.extend( currentRules, rules, true );
else
this._.rules[ tagName ] = rules;
}
}
} );
/**
* Whether to force using `'&'` instead of `'&'` in element attributes
* values. It is not recommended to change this setting for compliance with the
* W3C XHTML 1.0 standards ([C.12, XHTML 1.0](http://www.w3.org/TR/xhtml1/#C_12)).
*
* // Use `'&'` instead of `'&'`
* CKEDITOR.config.forceSimpleAmpersand = true;
*
* @cfg {Boolean} [forceSimpleAmpersand=false]
* @member CKEDITOR.config
*/
/**
* The characters to be used for indenting HTML output produced by the editor.
* Using characters different from `' '` (space) and `'\t'` (tab) is not recommended
* as it will mess the code.
*
* // No indentation.
* CKEDITOR.config.dataIndentationChars = '';
*
* // Use two spaces for indentation.
* CKEDITOR.config.dataIndentationChars = ' ';
*
* @cfg {String} [dataIndentationChars='\t']
* @member CKEDITOR.config
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview The Image plugin.
*/
( function() {
CKEDITOR.plugins.add( 'image', {
requires: 'dialog',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
var pluginName = 'image';
// Abort when Easyimage or Image2 are to be loaded since this plugins
// share the same functionality (#1791).
if ( editor.plugins.detectConflict( pluginName, [ 'easyimage', 'image2' ] ) ) {
return;
}
// Register the dialog.
CKEDITOR.dialog.add( pluginName, this.path + 'dialogs/image.js' );
var allowed = 'img[alt,!src]{border-style,border-width,float,height,margin,margin-bottom,margin-left,margin-right,margin-top,width}',
required = 'img[alt,src]';
if ( CKEDITOR.dialog.isTabEnabled( editor, pluginName, 'advanced' ) )
allowed = 'img[alt,dir,id,lang,longdesc,!src,title]{*}(*)';
// Register the command.
editor.addCommand( pluginName, new CKEDITOR.dialogCommand( pluginName, {
allowedContent: allowed,
requiredContent: required,
contentTransformations: [
[ 'img{width}: sizeToStyle', 'img[width]: sizeToAttribute' ],
[ 'img{float}: alignmentToStyle', 'img[align]: alignmentToAttribute' ]
]
} ) );
// Register the toolbar button.
editor.ui.addButton && editor.ui.addButton( 'Image', {
label: editor.lang.common.image,
command: pluginName,
toolbar: 'insert,10'
} );
editor.on( 'doubleclick', function( evt ) {
var element = evt.data.element;
if ( element.is( 'img' ) && !element.data( 'cke-realelement' ) && !element.isReadOnly() )
evt.data.dialog = 'image';
} );
// If the "menu" plugin is loaded, register the menu items.
if ( editor.addMenuItems ) {
editor.addMenuItems( {
image: {
label: editor.lang.image.menu,
command: 'image',
group: 'image'
}
} );
}
// If the "contextmenu" plugin is loaded, register the listeners.
if ( editor.contextMenu ) {
editor.contextMenu.addListener( function( element ) {
if ( getSelectedImage( editor, element ) )
return { image: CKEDITOR.TRISTATE_OFF };
} );
}
},
afterInit: function( editor ) {
// Abort when Image2 is to be loaded since both plugins
// share the same button, command, etc. names (https://dev.ckeditor.com/ticket/11222).
if ( editor.plugins.image2 )
return;
// Customize the behavior of the alignment commands. (https://dev.ckeditor.com/ticket/7430)
setupAlignCommand( 'left' );
setupAlignCommand( 'right' );
setupAlignCommand( 'center' );
setupAlignCommand( 'block' );
function setupAlignCommand( value ) {
var command = editor.getCommand( 'justify' + value );
if ( command ) {
if ( value == 'left' || value == 'right' ) {
command.on( 'exec', function( evt ) {
var img = getSelectedImage( editor ),
align;
if ( img ) {
align = getImageAlignment( img );
if ( align == value ) {
img.removeStyle( 'float' );
// Remove "align" attribute when necessary.
if ( value == getImageAlignment( img ) )
img.removeAttribute( 'align' );
} else {
img.setStyle( 'float', value );
}
evt.cancel();
}
} );
}
command.on( 'refresh', function( evt ) {
var img = getSelectedImage( editor ),
align;
if ( img ) {
align = getImageAlignment( img );
this.setState(
( align == value ) ? CKEDITOR.TRISTATE_ON : ( value == 'right' || value == 'left' ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
evt.cancel();
}
} );
}
}
}
} );
function getSelectedImage( editor, element ) {
if ( !element ) {
var sel = editor.getSelection();
element = sel.getSelectedElement();
}
if ( element && element.is( 'img' ) && !element.data( 'cke-realelement' ) && !element.isReadOnly() )
return element;
}
function getImageAlignment( element ) {
var align = element.getStyle( 'float' );
if ( align == 'inherit' || align == 'none' )
align = 0;
if ( !align )
align = element.getAttribute( 'align' );
return align;
}
} )();
/**
* Determines whether dimension inputs should be automatically filled when the image URL changes in the Image plugin dialog window.
*
* config.image_prefillDimensions = false;
*
* @since 4.5.0
* @cfg {Boolean} [image_prefillDimensions=true]
* @member CKEDITOR.config
*/
/**
* Whether to remove links when emptying the link URL field in the Image dialog window.
*
* config.image_removeLinkByEmptyURL = false;
*
* @cfg {Boolean} [image_removeLinkByEmptyURL=true]
* @member CKEDITOR.config
*/
CKEDITOR.config.image_removeLinkByEmptyURL = true;
/**
* Padding text to set off the image in the preview area.
*
* config.image_previewText = CKEDITOR.tools.repeat( '___ ', 100 );
*
* @cfg {String} [image_previewText='Lorem ipsum dolor...' (placeholder text)]
* @member CKEDITOR.config
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Increase and Decrease Indent commands.
*/
( function() {
'use strict';
var TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED,
TRISTATE_OFF = CKEDITOR.TRISTATE_OFF;
CKEDITOR.plugins.add( 'indent', {
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
var genericDefinition = CKEDITOR.plugins.indent.genericDefinition;
// Register generic commands.
setupGenericListeners( editor, editor.addCommand( 'indent', new genericDefinition( true ) ) );
setupGenericListeners( editor, editor.addCommand( 'outdent', new genericDefinition() ) );
// Create and register toolbar button if possible.
if ( editor.ui.addButton ) {
editor.ui.addButton( 'Indent', {
label: editor.lang.indent.indent,
command: 'indent',
directional: true,
toolbar: 'indent,20'
} );
editor.ui.addButton( 'Outdent', {
label: editor.lang.indent.outdent,
command: 'outdent',
directional: true,
toolbar: 'indent,10'
} );
}
// Register dirChanged listener.
editor.on( 'dirChanged', function( evt ) {
var range = editor.createRange(),
dataNode = evt.data.node;
range.setStartBefore( dataNode );
range.setEndAfter( dataNode );
var walker = new CKEDITOR.dom.walker( range ),
node;
while ( ( node = walker.next() ) ) {
if ( node.type == CKEDITOR.NODE_ELEMENT ) {
// A child with the defined dir is to be ignored.
if ( !node.equals( dataNode ) && node.getDirection() ) {
range.setStartAfter( node );
walker = new CKEDITOR.dom.walker( range );
continue;
}
// Switch alignment classes.
var classes = editor.config.indentClasses;
if ( classes ) {
var suffix = ( evt.data.dir == 'ltr' ) ? [ '_rtl', '' ] : [ '', '_rtl' ];
for ( var i = 0; i < classes.length; i++ ) {
if ( node.hasClass( classes[ i ] + suffix[ 0 ] ) ) {
node.removeClass( classes[ i ] + suffix[ 0 ] );
node.addClass( classes[ i ] + suffix[ 1 ] );
}
}
}
// Switch the margins.
var marginLeft = node.getStyle( 'margin-right' ),
marginRight = node.getStyle( 'margin-left' );
marginLeft ? node.setStyle( 'margin-left', marginLeft ) : node.removeStyle( 'margin-left' );
marginRight ? node.setStyle( 'margin-right', marginRight ) : node.removeStyle( 'margin-right' );
}
}
} );
}
} );
/**
* Global command class definitions and global helpers.
*
* @class
* @singleton
*/
CKEDITOR.plugins.indent = {
/**
* A base class for a generic command definition, responsible mainly for creating
* Increase Indent and Decrease Indent toolbar buttons as well as for refreshing
* UI states.
*
* Commands of this class do not perform any indentation by themselves. They
* delegate this job to content-specific indentation commands (i.e. indentlist).
*
* @class CKEDITOR.plugins.indent.genericDefinition
* @extends CKEDITOR.commandDefinition
* @param {CKEDITOR.editor} editor The editor instance this command will be
* applied to.
* @param {String} name The name of the command.
* @param {Boolean} [isIndent] Defines the command as indenting or outdenting.
*/
genericDefinition: function( isIndent ) {
/**
* Determines whether the command belongs to the indentation family.
* Otherwise it is assumed to be an outdenting command.
*
* @readonly
* @property {Boolean} [=false]
*/
this.isIndent = !!isIndent;
// Mimic naive startDisabled behavior for outdent.
this.startDisabled = !this.isIndent;
},
/**
* A base class for specific indentation command definitions responsible for
* handling a pre-defined set of elements i.e. indentlist for lists or
* indentblock for text block elements.
*
* Commands of this class perform indentation operations and modify the DOM structure.
* They listen for events fired by {@link CKEDITOR.plugins.indent.genericDefinition}
* and execute defined actions.
*
* **NOTE**: This is not an {@link CKEDITOR.command editor command}.
* Context-specific commands are internal, for indentation system only.
*
* @class CKEDITOR.plugins.indent.specificDefinition
* @param {CKEDITOR.editor} editor The editor instance this command will be
* applied to.
* @param {String} name The name of the command.
* @param {Boolean} [isIndent] Defines the command as indenting or outdenting.
*/
specificDefinition: function( editor, name, isIndent ) {
this.name = name;
this.editor = editor;
/**
* An object of jobs handled by the command. Each job consists
* of two functions: `refresh` and `exec` as well as the execution priority.
*
* * The `refresh` function determines whether a job is doable for
* a particular context. These functions are executed in the
* order of priorities, one by one, for all plugins that registered
* jobs. As jobs are related to generic commands, refreshing
* occurs when the global command is firing the `refresh` event.
*
* **Note**: This function must return either {@link CKEDITOR#TRISTATE_DISABLED}
* or {@link CKEDITOR#TRISTATE_OFF}.
*
* * The `exec` function modifies the DOM if possible. Just like
* `refresh`, `exec` functions are executed in the order of priorities
* while the generic command is executed. This function is not executed
* if `refresh` for this job returned {@link CKEDITOR#TRISTATE_DISABLED}.
*
* **Note**: This function must return a Boolean value, indicating whether it
* was successful. If a job was successful, then no other jobs are being executed.
*
* Sample definition:
*
* command.jobs = {
* // Priority = 20.
* '20': {
* refresh( editor, path ) {
* if ( condition )
* return CKEDITOR.TRISTATE_OFF;
* else
* return CKEDITOR.TRISTATE_DISABLED;
* },
* exec( editor ) {
* // DOM modified! This was OK.
* return true;
* }
* },
* // Priority = 60. This job is done later.
* '60': {
* // Another job.
* }
* };
*
* For additional information, please check comments for
* the `setupGenericListeners` function.
*
* @readonly
* @property {Object} [={}]
*/
this.jobs = {};
/**
* Determines whether the editor that the command belongs to has
* {@link CKEDITOR.config#enterMode config.enterMode} set to {@link CKEDITOR#ENTER_BR}.
*
* @readonly
* @see CKEDITOR.config#enterMode
* @property {Boolean} [=false]
*/
this.enterBr = editor.config.enterMode == CKEDITOR.ENTER_BR;
/**
* Determines whether the command belongs to the indentation family.
* Otherwise it is assumed to be an outdenting command.
*
* @readonly
* @property {Boolean} [=false]
*/
this.isIndent = !!isIndent;
/**
* The name of the global command related to this one.
*
* @readonly
*/
this.relatedGlobal = isIndent ? 'indent' : 'outdent';
/**
* A keystroke associated with this command (*Tab* or *Shift+Tab*).
*
* @readonly
*/
this.indentKey = isIndent ? 9 : CKEDITOR.SHIFT + 9;
/**
* Stores created markers for the command so they can eventually be
* purged after the `exec` function is run.
*/
this.database = {};
},
/**
* Registers content-specific commands as a part of the indentation system
* directed by generic commands. Once a command is registered,
* it listens for events of a related generic command.
*
* CKEDITOR.plugins.indent.registerCommands( editor, {
* 'indentlist': new indentListCommand( editor, 'indentlist' ),
* 'outdentlist': new indentListCommand( editor, 'outdentlist' )
* } );
*
* Content-specific commands listen for the generic command's `exec` and
* try to execute their own jobs, one after another. If some execution is
* successful, `evt.data.done` is set so no more jobs (commands) are involved.
*
* Content-specific commands also listen for the generic command's `refresh`
* and fill the `evt.data.states` object with states of jobs. A generic command
* uses this data to determine its own state and to update the UI.
*
* @member CKEDITOR.plugins.indent
* @param {CKEDITOR.editor} editor The editor instance this command is
* applied to.
* @param {Object} commands An object of {@link CKEDITOR.command}.
*/
registerCommands: function( editor, commands ) {
editor.on( 'pluginsLoaded', function() {
for ( var name in commands ) {
( function( editor, command ) {
var relatedGlobal = editor.getCommand( command.relatedGlobal );
for ( var priority in command.jobs ) {
// Observe generic exec event and execute command when necessary.
// If the command was successfully handled by the command and
// DOM has been modified, stop event propagation so no other plugin
// will bother. Job is done.
relatedGlobal.on( 'exec', function( evt ) {
if ( evt.data.done )
return;
// Make sure that anything this command will do is invisible
// for undoManager. What undoManager only can see and
// remember is the execution of the global command (relatedGlobal).
editor.fire( 'lockSnapshot' );
if ( command.execJob( editor, priority ) )
evt.data.done = true;
editor.fire( 'unlockSnapshot' );
// Clean up the markers.
CKEDITOR.dom.element.clearAllMarkers( command.database );
}, this, null, priority );
// Observe generic refresh event and force command refresh.
// Once refreshed, save command state in event data
// so generic command plugin can update its own state and UI.
relatedGlobal.on( 'refresh', function( evt ) {
if ( !evt.data.states )
evt.data.states = {};
evt.data.states[ command.name + '@' + priority ] =
command.refreshJob( editor, priority, evt.data.path );
}, this, null, priority );
}
// Since specific indent commands have no UI elements,
// they need to be manually registered as a editor feature.
editor.addFeature( command );
} )( this, commands[ name ] );
}
} );
}
};
CKEDITOR.plugins.indent.genericDefinition.prototype = {
context: 'p',
exec: function() {}
};
CKEDITOR.plugins.indent.specificDefinition.prototype = {
/**
* Executes the content-specific procedure if the context is correct.
* It calls the `exec` function of a job of the given `priority`
* that modifies the DOM.
*
* @param {CKEDITOR.editor} editor The editor instance this command
* will be applied to.
* @param {Number} priority The priority of the job to be executed.
* @returns {Boolean} Indicates whether the job was successful.
*/
execJob: function( editor, priority ) {
var job = this.jobs[ priority ];
if ( job.state != TRISTATE_DISABLED )
return job.exec.call( this, editor );
},
/**
* Calls the `refresh` function of a job of the given `priority`.
* The function returns the state of the job which can be either
* {@link CKEDITOR#TRISTATE_DISABLED} or {@link CKEDITOR#TRISTATE_OFF}.
*
* @param {CKEDITOR.editor} editor The editor instance this command
* will be applied to.
* @param {Number} priority The priority of the job to be executed.
* @returns {Number} The state of the job.
*/
refreshJob: function( editor, priority, path ) {
var job = this.jobs[ priority ];
if ( !editor.activeFilter.checkFeature( this ) )
job.state = TRISTATE_DISABLED;
else
job.state = job.refresh.call( this, editor, path );
return job.state;
},
/**
* Checks if the element path contains the element handled
* by this indentation command.
*
* @param {CKEDITOR.dom.elementPath} node A path to be checked.
* @returns {CKEDITOR.dom.element}
*/
getContext: function( path ) {
return path.contains( this.context );
}
};
/**
* Attaches event listeners for this generic command. Since the indentation
* system is event-oriented, generic commands communicate with
* content-specific commands using the `exec` and `refresh` events.
*
* Listener priorities are crucial. Different indentation phases
* are executed with different priorities.
*
* For the `exec` event:
*
* * 0: Selection and bookmarks are saved by the generic command.
* * 1-99: Content-specific commands try to indent the code by executing
* their own jobs ({@link CKEDITOR.plugins.indent.specificDefinition#jobs}).
* * 100: Bookmarks are re-selected by the generic command.
*
* The visual interpretation looks as follows:
*
* +------------------+
* | Exec event fired |
* +------ + ---------+
* |
* 0 -<----------+ Selection and bookmarks saved.
* |
* |
* 25 -<---+ Exec 1st job of plugin#1 (return false, continuing...).
* |
* |
* 50 -<---+ Exec 1st job of plugin#2 (return false, continuing...).
* |
* |
* 75 -<---+ Exec 2nd job of plugin#1 (only if plugin#2 failed).
* |
* |
* 100 -<-----------+ Re-select bookmarks, clean-up.
* |
* +-------- v ----------+
* | Exec event finished |
* +---------------------+
*
* For the `refresh` event:
*
* * <100: Content-specific commands refresh their job states according
* to the given path. Jobs save their states in the `evt.data.states` object
* passed along with the event. This can be either {@link CKEDITOR#TRISTATE_DISABLED}
* or {@link CKEDITOR#TRISTATE_OFF}.
* * 100: Command state is determined according to what states
* have been returned by content-specific jobs (`evt.data.states`).
* UI elements are updated at this stage.
*
* **Note**: If there is at least one job with the {@link CKEDITOR#TRISTATE_OFF} state,
* then the generic command state is also {@link CKEDITOR#TRISTATE_OFF}. Otherwise,
* the command state is {@link CKEDITOR#TRISTATE_DISABLED}.
*
* @param {CKEDITOR.command} command The command to be set up.
* @private
*/
function setupGenericListeners( editor, command ) {
var selection, bookmarks;
// Set the command state according to content-specific
// command states.
command.on( 'refresh', function( evt ) {
// If no state comes with event data, disable command.
var states = [ TRISTATE_DISABLED ];
for ( var s in evt.data.states )
states.push( evt.data.states[ s ] );
this.setState( CKEDITOR.tools.search( states, TRISTATE_OFF ) ? TRISTATE_OFF : TRISTATE_DISABLED );
}, command, null, 100 );
// Initialization. Save bookmarks and mark event as not handled
// by any plugin (command) yet.
command.on( 'exec', function( evt ) {
selection = editor.getSelection();
bookmarks = selection.createBookmarks( 1 );
// Mark execution as not handled yet.
if ( !evt.data )
evt.data = {};
evt.data.done = false;
}, command, null, 0 );
// Housekeeping. Make sure selectionChange will be called.
// Also re-select previously saved bookmarks.
command.on( 'exec', function() {
editor.forceNextSelectionCheck();
selection.selectBookmarks( bookmarks );
}, command, null, 100 );
}
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Handles the indentation of lists.
*/
( function() {
'use strict';
var isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
isNotBookmark = CKEDITOR.dom.walker.bookmark( false, true ),
TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED,
TRISTATE_OFF = CKEDITOR.TRISTATE_OFF;
CKEDITOR.plugins.add( 'indentlist', {
requires: 'indent',
init: function( editor ) {
var globalHelpers = CKEDITOR.plugins.indent;
// Register commands.
globalHelpers.registerCommands( editor, {
indentlist: new commandDefinition( editor, 'indentlist', true ),
outdentlist: new commandDefinition( editor, 'outdentlist' )
} );
function commandDefinition( editor ) {
globalHelpers.specificDefinition.apply( this, arguments );
// Require ul OR ol list.
this.requiredContent = [ 'ul', 'ol' ];
// Indent and outdent lists with TAB/SHIFT+TAB key. Indenting can
// be done for any list item that isn't the first child of the parent.
editor.on( 'key', function( evt ) {
var path = editor.elementPath();
if ( editor.mode != 'wysiwyg' )
return;
if ( evt.data.keyCode == this.indentKey ) {
// Prevent of getting context of empty path (#424)(https://dev.ckeditor.com/ticket/17028).
if ( !path ) {
return;
}
var list = this.getContext( path );
if ( list ) {
// Don't indent if in first list item of the parent.
// Outdent, however, can always be done to collapse
// the list into a paragraph (div).
if ( this.isIndent && CKEDITOR.plugins.indentList.firstItemInPath( this.context, path, list ) )
return;
// Exec related global indentation command. Global
// commands take care of bookmarks and selection,
// so it's much easier to use them instead of
// content-specific commands.
editor.execCommand( this.relatedGlobal );
// Cancel the key event so editor doesn't lose focus.
evt.cancel();
}
}
}, this );
// There are two different jobs for this plugin:
//
// * Indent job (priority=10), before indentblock.
//
// This job is before indentblock because, if this plugin is
// loaded it has higher priority over indentblock. It means that,
// if possible, nesting is performed, and then block manipulation,
// if necessary.
//
// * Outdent job (priority=30), after outdentblock.
//
// This job got to be after outdentblock because in some cases
// (margin, config#indentClass on list) outdent must be done on
// block-level.
this.jobs[ this.isIndent ? 10 : 30 ] = {
refresh: this.isIndent ?
function( editor, path ) {
var list = this.getContext( path ),
inFirstListItem = CKEDITOR.plugins.indentList.firstItemInPath( this.context, path, list );
if ( !list || !this.isIndent || inFirstListItem )
return TRISTATE_DISABLED;
return TRISTATE_OFF;
} : function( editor, path ) {
var list = this.getContext( path );
if ( !list || this.isIndent )
return TRISTATE_DISABLED;
return TRISTATE_OFF;
},
exec: CKEDITOR.tools.bind( indentList, this )
};
}
CKEDITOR.tools.extend( commandDefinition.prototype, globalHelpers.specificDefinition.prototype, {
// Elements that, if in an elementpath, will be handled by this
// command. They restrict the scope of the plugin.
context: { ol: 1, ul: 1 }
} );
}
} );
function indentList( editor ) {
var that = this,
database = this.database,
context = this.context,
range;
function indent( listNode ) {
// Our starting and ending points of the range might be inside some blocks under a list item...
// So before playing with the iterator, we need to expand the block to include the list items.
var startContainer = range.startContainer,
endContainer = range.endContainer;
while ( startContainer && !startContainer.getParent().equals( listNode ) )
startContainer = startContainer.getParent();
while ( endContainer && !endContainer.getParent().equals( listNode ) )
endContainer = endContainer.getParent();
if ( !startContainer || !endContainer )
return false;
// Now we can iterate over the individual items on the same tree depth.
var block = startContainer,
itemsToMove = [],
stopFlag = false;
while ( !stopFlag ) {
if ( block.equals( endContainer ) )
stopFlag = true;
itemsToMove.push( block );
block = block.getNext();
}
if ( itemsToMove.length < 1 )
return false;
// Do indent or outdent operations on the array model of the list, not the
// list's DOM tree itself. The array model demands that it knows as much as
// possible about the surrounding lists, we need to feed it the further
// ancestor node that is still a list.
var listParents = listNode.getParents( true );
for ( var i = 0; i < listParents.length; i++ ) {
if ( listParents[ i ].getName && context[ listParents[ i ].getName() ] ) {
listNode = listParents[ i ];
break;
}
}
var indentOffset = that.isIndent ? 1 : -1,
startItem = itemsToMove[ 0 ],
lastItem = itemsToMove[ itemsToMove.length - 1 ],
// Convert the list DOM tree into a one dimensional array.
listArray = CKEDITOR.plugins.list.listToArray( listNode, database ),
// Apply indenting or outdenting on the array.
baseIndent = listArray[ lastItem.getCustomData( 'listarray_index' ) ].indent;
for ( i = startItem.getCustomData( 'listarray_index' ); i <= lastItem.getCustomData( 'listarray_index' ); i++ ) {
listArray[ i ].indent += indentOffset;
// Make sure the newly created sublist get a brand-new element of the same type. (https://dev.ckeditor.com/ticket/5372)
if ( indentOffset > 0 ) {
var listRoot = listArray[ i ].parent;
// Find previous list item which has the same indention offset as the new indention offset
// of current item to copy its root tag (so the proper list-style-type is used) (#842).
for ( var j = i - 1; j >= 0; j-- ) {
if ( listArray[ j ].indent === indentOffset ) {
listRoot = listArray[ j ].parent;
break;
}
}
listArray[ i ].parent = new CKEDITOR.dom.element( listRoot.getName(), listRoot.getDocument() );
}
}
for ( i = lastItem.getCustomData( 'listarray_index' ) + 1; i < listArray.length && listArray[ i ].indent > baseIndent; i++ )
listArray[ i ].indent += indentOffset;
// Convert the array back to a DOM forest (yes we might have a few subtrees now).
// And replace the old list with the new forest.
var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode, listNode.getDirection() );
// Avoid nested after outdent even they're visually same,
// recording them for later refactoring.(https://dev.ckeditor.com/ticket/3982)
if ( !that.isIndent ) {
var parentLiElement;
if ( ( parentLiElement = listNode.getParent() ) && parentLiElement.is( 'li' ) ) {
var children = newList.listNode.getChildren(),
pendingLis = [],
count = children.count(),
child;
for ( i = count - 1; i >= 0; i-- ) {
if ( ( child = children.getItem( i ) ) && child.is && child.is( 'li' ) )
pendingLis.push( child );
}
}
}
if ( newList )
newList.listNode.replace( listNode );
// Move the nested to be appeared after the parent.
if ( pendingLis && pendingLis.length ) {
for ( i = 0; i < pendingLis.length; i++ ) {
var li = pendingLis[ i ],
followingList = li;
// Nest preceding / inside current if any.
while ( ( followingList = followingList.getNext() ) && followingList.is && followingList.getName() in context ) {
// IE requires a filler NBSP for nested list inside empty list item,
// otherwise the list item will be inaccessiable. (https://dev.ckeditor.com/ticket/4476)
if ( CKEDITOR.env.needsNbspFiller && !li.getFirst( neitherWhitespacesNorBookmark ) )
li.append( range.document.createText( '\u00a0' ) );
li.append( followingList );
}
li.insertAfter( parentLiElement );
}
}
if ( newList )
editor.fire( 'contentDomInvalidated' );
return true;
}
var selection = editor.getSelection(),
ranges = selection && selection.getRanges(),
iterator = ranges.createIterator();
while ( ( range = iterator.getNextRange() ) ) {
var nearestListBlock = range.getCommonAncestor();
while ( nearestListBlock && !( nearestListBlock.type == CKEDITOR.NODE_ELEMENT && context[ nearestListBlock.getName() ] ) ) {
// Avoid having plugin propagate to parent of editor in inline mode by canceling the indentation. (https://dev.ckeditor.com/ticket/12796)
if ( editor.editable().equals( nearestListBlock ) ) {
nearestListBlock = false;
break;
}
nearestListBlock = nearestListBlock.getParent();
}
// Avoid having selection boundaries out of the list.
// ...]
=> ...
if ( !nearestListBlock ) {
if ( ( nearestListBlock = range.startPath().contains( context ) ) )
range.setEndAt( nearestListBlock, CKEDITOR.POSITION_BEFORE_END );
}
// Avoid having selection enclose the entire list. (https://dev.ckeditor.com/ticket/6138)
// [] =>
if ( !nearestListBlock ) {
var selectedNode = range.getEnclosedNode();
if ( selectedNode && selectedNode.type == CKEDITOR.NODE_ELEMENT && selectedNode.getName() in context ) {
range.setStartAt( selectedNode, CKEDITOR.POSITION_AFTER_START );
range.setEndAt( selectedNode, CKEDITOR.POSITION_BEFORE_END );
nearestListBlock = selectedNode;
}
}
// Avoid selection anchors under list root.
// =>
if ( nearestListBlock && range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in context ) {
var walker = new CKEDITOR.dom.walker( range );
walker.evaluator = listItem;
range.startContainer = walker.next();
}
if ( nearestListBlock && range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in context ) {
walker = new CKEDITOR.dom.walker( range );
walker.evaluator = listItem;
range.endContainer = walker.previous();
}
if ( nearestListBlock )
return indent( nearestListBlock );
}
return 0;
}
// Determines whether a node is a list element.
function listItem( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && node.is( 'li' );
}
function neitherWhitespacesNorBookmark( node ) {
return isNotWhitespaces( node ) && isNotBookmark( node );
}
/**
* Global namespace for methods exposed by the Indent List plugin.
*
* @singleton
* @class
*/
CKEDITOR.plugins.indentList = {};
/**
* Checks whether the first child of the list is in the path.
* The list can be extracted from the path or given explicitly
* e.g. for better performance if cached.
*
* @since 4.4.6
* @param {Object} query See the {@link CKEDITOR.dom.elementPath#contains} method arguments.
* @param {CKEDITOR.dom.elementPath} path
* @param {CKEDITOR.dom.element} [list]
* @returns {Boolean}
* @member CKEDITOR.plugins.indentList
*/
CKEDITOR.plugins.indentList.firstItemInPath = function( query, path, list ) {
var firstListItemInPath = path.contains( listItem );
if ( !list )
list = path.contains( query );
return list && firstListItemInPath && firstListItemInPath.equals( list.getFirst( listItem ) );
};
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Handles the indentation of block elements.
*/
( function() {
'use strict';
var $listItem = CKEDITOR.dtd.$listItem,
$list = CKEDITOR.dtd.$list,
TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED,
TRISTATE_OFF = CKEDITOR.TRISTATE_OFF;
CKEDITOR.plugins.add( 'indentblock', {
requires: 'indent',
init: function( editor ) {
var globalHelpers = CKEDITOR.plugins.indent,
classes = editor.config.indentClasses;
// Register commands.
globalHelpers.registerCommands( editor, {
indentblock: new commandDefinition( editor, 'indentblock', true ),
outdentblock: new commandDefinition( editor, 'outdentblock' )
} );
function commandDefinition() {
globalHelpers.specificDefinition.apply( this, arguments );
this.allowedContent = {
'div h1 h2 h3 h4 h5 h6 ol p pre ul': {
// Do not add elements, but only text-align style if element is validated by other rule.
propertiesOnly: true,
styles: !classes ? 'margin-left,margin-right' : null,
classes: classes || null
}
};
this.contentTransformations = [
[ 'div: splitMarginShorthand' ],
[ 'h1: splitMarginShorthand' ],
[ 'h2: splitMarginShorthand' ],
[ 'h3: splitMarginShorthand' ],
[ 'h4: splitMarginShorthand' ],
[ 'h5: splitMarginShorthand' ],
[ 'h6: splitMarginShorthand' ],
[ 'ol: splitMarginShorthand' ],
[ 'p: splitMarginShorthand' ],
[ 'pre: splitMarginShorthand' ],
[ 'ul: splitMarginShorthand' ]
];
if ( this.enterBr )
this.allowedContent.div = true;
this.requiredContent = ( this.enterBr ? 'div' : 'p' ) +
( classes ? '(' + classes.join( ',' ) + ')' : '{margin-left}' );
this.jobs = {
'20': {
refresh: function( editor, path ) {
var firstBlock = path.block || path.blockLimit;
// Switch context from somewhere inside list item to list item,
// if not found just assign self (doing nothing).
if ( !firstBlock.is( $listItem ) ) {
var ascendant = firstBlock.getAscendant( $listItem );
firstBlock = ( ascendant && path.contains( ascendant ) ) || firstBlock;
}
// Switch context from list item to list
// because indentblock can indent entire list
// but not a single list element.
if ( firstBlock.is( $listItem ) )
firstBlock = firstBlock.getParent();
// [-] Context in the path or ENTER_BR
//
// Don't try to indent if the element is out of
// this plugin's scope. This assertion is omitted
// if ENTER_BR is in use since there may be no block
// in the path.
if ( !this.enterBr && !this.getContext( path ) )
return TRISTATE_DISABLED;
else if ( classes ) {
// [+] Context in the path or ENTER_BR
// [+] IndentClasses
//
// If there are indentation classes, check if reached
// the highest level of indentation. If so, disable
// the command.
if ( indentClassLeft.call( this, firstBlock, classes ) )
return TRISTATE_OFF;
else
return TRISTATE_DISABLED;
} else {
// [+] Context in the path or ENTER_BR
// [-] IndentClasses
// [+] Indenting
//
// No indent-level limitations due to indent classes.
// Indent-like command can always be executed.
if ( this.isIndent )
return TRISTATE_OFF;
// [+] Context in the path or ENTER_BR
// [-] IndentClasses
// [-] Indenting
// [-] Block in the path
//
// No block in path. There's no element to apply indentation
// so disable the command.
else if ( !firstBlock )
return TRISTATE_DISABLED;
// [+] Context in the path or ENTER_BR
// [-] IndentClasses
// [-] Indenting
// [+] Block in path.
//
// Not using indentClasses but there is firstBlock.
// We can calculate current indentation level and
// try to increase/decrease it.
else {
return CKEDITOR[
( getIndent( firstBlock ) || 0 ) <= 0 ? 'TRISTATE_DISABLED' : 'TRISTATE_OFF'
];
}
}
},
exec: function( editor ) {
var selection = editor.getSelection(),
range = selection && selection.getRanges()[ 0 ],
nearestListBlock;
// If there's some list in the path, then it will be
// a full-list indent by increasing or decreasing margin property.
if ( ( nearestListBlock = editor.elementPath().contains( $list ) ) )
indentElement.call( this, nearestListBlock, classes );
// If no list in the path, use iterator to indent all the possible
// paragraphs in the range, creating them if necessary.
else {
var iterator = range.createIterator(),
enterMode = editor.config.enterMode,
block;
iterator.enforceRealBlocks = true;
iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR;
while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) ) {
if ( !block.isReadOnly() )
indentElement.call( this, block, classes );
}
}
return true;
}
}
};
}
CKEDITOR.tools.extend( commandDefinition.prototype, globalHelpers.specificDefinition.prototype, {
// Elements that, if in an elementpath, will be handled by this
// command. They restrict the scope of the plugin.
context: { div: 1, dl: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, p: 1, pre: 1, table: 1 },
// A regex built on config#indentClasses to detect whether an
// element has some indentClass or not.
classNameRegex: classes ? new RegExp( '(?:^|\\s+)(' + classes.join( '|' ) + ')(?=$|\\s)' ) : null
} );
}
} );
// Generic indentation procedure for indentation of any element
// either with margin property or config#indentClass.
function indentElement( element, classes, dir ) {
if ( element.getCustomData( 'indent_processed' ) )
return;
var editor = this.editor,
isIndent = this.isIndent;
if ( classes ) {
// Transform current class f to indent step index.
var indentClass = element.$.className.match( this.classNameRegex ),
indentStep = 0;
if ( indentClass ) {
indentClass = indentClass[ 1 ];
indentStep = CKEDITOR.tools.indexOf( classes, indentClass ) + 1;
}
// Operate on indent step index, transform indent step index
// back to class name.
if ( ( indentStep += isIndent ? 1 : -1 ) < 0 )
return;
indentStep = Math.min( indentStep, classes.length );
indentStep = Math.max( indentStep, 0 );
element.$.className = CKEDITOR.tools.ltrim( element.$.className.replace( this.classNameRegex, '' ) );
if ( indentStep > 0 )
element.addClass( classes[ indentStep - 1 ] );
} else {
var indentCssProperty = getIndentCss( element, dir ),
currentOffset = parseInt( element.getStyle( indentCssProperty ), 10 ),
indentOffset = editor.config.indentOffset || 40;
if ( isNaN( currentOffset ) )
currentOffset = 0;
currentOffset += ( isIndent ? 1 : -1 ) * indentOffset;
if ( currentOffset < 0 )
return;
currentOffset = Math.max( currentOffset, 0 );
currentOffset = Math.ceil( currentOffset / indentOffset ) * indentOffset;
element.setStyle(
indentCssProperty,
currentOffset ? currentOffset + ( editor.config.indentUnit || 'px' ) : ''
);
if ( element.getAttribute( 'style' ) === '' )
element.removeAttribute( 'style' );
}
CKEDITOR.dom.element.setMarker( this.database, element, 'indent_processed', 1 );
return;
}
// Method that checks if current indentation level for an element
// reached the limit determined by config#indentClasses.
function indentClassLeft( node, classes ) {
var indentClass = node.$.className.match( this.classNameRegex ),
isIndent = this.isIndent;
// If node has one of the indentClasses:
// * If it holds the topmost indentClass, then
// no more classes have left.
// * If it holds any other indentClass, it can use the next one
// or the previous one.
// * Outdent is always possible. We can remove indentClass.
if ( indentClass )
return isIndent ? indentClass[ 1 ] != classes.slice( -1 ) : true;
// If node has no class which belongs to indentClasses,
// then it is at 0-level. It can be indented but not outdented.
else
return isIndent;
}
// Determines indent CSS property for an element according to
// what is the direction of such element. It can be either `margin-left`
// or `margin-right`.
function getIndentCss( element, dir ) {
return ( dir || element.getComputedStyle( 'direction' ) ) == 'ltr' ? 'margin-left' : 'margin-right';
}
// Return the numerical indent value of margin-left|right of an element,
// considering element's direction. If element has no margin specified,
// NaN is returned.
function getIndent( element ) {
return parseInt( element.getStyle( getIndentCss( element ) ), 10 );
}
} )();
/**
* A list of classes to use for indenting the contents. If set to `null`, no classes will be used
* and instead the {@link #indentUnit} and {@link #indentOffset} properties will be used.
*
* // Use the 'Indent1', 'Indent2', 'Indent3' classes.
* config.indentClasses = ['Indent1', 'Indent2', 'Indent3'];
*
* @cfg {Array} [indentClasses=null]
* @member CKEDITOR.config
*/
/**
* The size in {@link CKEDITOR.config#indentUnit indentation units} of each indentation step.
*
* config.indentOffset = 4;
*
* @cfg {Number} [indentOffset=40]
* @member CKEDITOR.config
*/
/**
* The unit used for {@link CKEDITOR.config#indentOffset indentation offset}.
*
* config.indentUnit = 'em';
*
* @cfg {String} [indentUnit='px']
* @member CKEDITOR.config
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Justify commands.
*/
( function() {
function getAlignment( element, useComputedState ) {
useComputedState = useComputedState === undefined || useComputedState;
var align;
if ( useComputedState )
align = element.getComputedStyle( 'text-align' );
else {
while ( !element.hasAttribute || !( element.hasAttribute( 'align' ) || element.getStyle( 'text-align' ) ) ) {
var parent = element.getParent();
if ( !parent )
break;
element = parent;
}
align = element.getStyle( 'text-align' ) || element.getAttribute( 'align' ) || '';
}
// Sometimes computed values doesn't tell.
align && ( align = align.replace( /(?:-(?:moz|webkit)-)?(?:start|auto)/i, '' ) );
!align && useComputedState && ( align = element.getComputedStyle( 'direction' ) == 'rtl' ? 'right' : 'left' );
return align;
}
function justifyCommand( editor, name, value ) {
this.editor = editor;
this.name = name;
this.value = value;
this.context = 'p';
var classes = editor.config.justifyClasses,
blockTag = editor.config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div';
if ( classes ) {
switch ( value ) {
case 'left':
this.cssClassName = classes[ 0 ];
break;
case 'center':
this.cssClassName = classes[ 1 ];
break;
case 'right':
this.cssClassName = classes[ 2 ];
break;
case 'justify':
this.cssClassName = classes[ 3 ];
break;
}
this.cssClassRegex = new RegExp( '(?:^|\\s+)(?:' + classes.join( '|' ) + ')(?=$|\\s)' );
this.requiredContent = blockTag + '(' + this.cssClassName + ')';
}
else {
this.requiredContent = blockTag + '{text-align}';
}
this.allowedContent = {
'caption div h1 h2 h3 h4 h5 h6 p pre td th li': {
// Do not add elements, but only text-align style if element is validated by other rule.
propertiesOnly: true,
styles: this.cssClassName ? null : 'text-align',
classes: this.cssClassName || null
}
};
// In enter mode BR we need to allow here for div, because when non other
// feature allows div justify is the only plugin that uses it.
if ( editor.config.enterMode == CKEDITOR.ENTER_BR )
this.allowedContent.div = true;
}
function onDirChanged( e ) {
var editor = e.editor;
var range = editor.createRange();
range.setStartBefore( e.data.node );
range.setEndAfter( e.data.node );
var walker = new CKEDITOR.dom.walker( range ),
node;
while ( ( node = walker.next() ) ) {
if ( node.type == CKEDITOR.NODE_ELEMENT ) {
// A child with the defined dir is to be ignored.
if ( !node.equals( e.data.node ) && node.getDirection() ) {
range.setStartAfter( node );
walker = new CKEDITOR.dom.walker( range );
continue;
}
// Switch the alignment.
var classes = editor.config.justifyClasses;
if ( classes ) {
// The left align class.
if ( node.hasClass( classes[ 0 ] ) ) {
node.removeClass( classes[ 0 ] );
node.addClass( classes[ 2 ] );
}
// The right align class.
else if ( node.hasClass( classes[ 2 ] ) ) {
node.removeClass( classes[ 2 ] );
node.addClass( classes[ 0 ] );
}
}
// Always switch CSS margins.
var style = 'text-align';
var align = node.getStyle( style );
if ( align == 'left' )
node.setStyle( style, 'right' );
else if ( align == 'right' )
node.setStyle( style, 'left' );
}
}
}
justifyCommand.prototype = {
exec: function( editor ) {
var selection = editor.getSelection(),
enterMode = editor.config.enterMode;
if ( !selection )
return;
var bookmarks = selection.createBookmarks(),
ranges = selection.getRanges();
var cssClassName = this.cssClassName,
iterator, block;
var useComputedState = editor.config.useComputedState;
useComputedState = useComputedState === undefined || useComputedState;
for ( var i = ranges.length - 1; i >= 0; i-- ) {
iterator = ranges[ i ].createIterator();
iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR;
while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) ) {
if ( block.isReadOnly() )
continue;
// Check if style or class might be applied to currently processed element (#455).
var tag = block.getName(),
isAllowedTextAlign, isAllowedCssClass;
isAllowedTextAlign = editor.activeFilter.check( tag + '{text-align}' );
isAllowedCssClass = editor.activeFilter.check( tag + '(' + cssClassName + ')' );
if ( !isAllowedCssClass && !isAllowedTextAlign ) {
continue;
}
block.removeAttribute( 'align' );
block.removeStyle( 'text-align' );
// Remove any of the alignment classes from the className.
var className = cssClassName && ( block.$.className = CKEDITOR.tools.ltrim( block.$.className.replace( this.cssClassRegex, '' ) ) );
var apply = ( this.state == CKEDITOR.TRISTATE_OFF ) && ( !useComputedState || ( getAlignment( block, true ) != this.value ) );
if ( cssClassName && isAllowedCssClass ) {
// Append the desired class name.
if ( apply )
block.addClass( cssClassName );
else if ( !className )
block.removeAttribute( 'class' );
} else if ( apply && isAllowedTextAlign ) {
block.setStyle( 'text-align', this.value );
}
}
}
editor.focus();
editor.forceNextSelectionCheck();
selection.selectBookmarks( bookmarks );
},
refresh: function( editor, path ) {
var firstBlock = path.block || path.blockLimit,
name = firstBlock.getName(),
isEditable = firstBlock.equals( editor.editable() ),
isStylable = this.cssClassName ? editor.activeFilter.check( name + '(' + this.cssClassName + ')' ) :
editor.activeFilter.check( name + '{text-align}' );
// #455
// 1. Check if we are directly in editbale. Justification should be always allowed, and not highlighted.
// Checking situation `body > ul` where ul is selected and path.blockLimit returns editable.
// 2. Check if current element can have applied specific class.
// 3. Check if current element can have applied text-align style.
if ( isEditable && !CKEDITOR.dtd.$list[ path.lastElement.getName() ] ) {
this.setState( CKEDITOR.TRISTATE_OFF );
} else if ( !isEditable && isStylable ) {
// 2 & 3 in one condition.
this.setState( getAlignment( firstBlock, this.editor.config.useComputedState ) == this.value ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
} else {
this.setState( CKEDITOR.TRISTATE_DISABLED );
}
}
};
CKEDITOR.plugins.add( 'justify', {
init: function( editor ) {
if ( editor.blockless )
return;
var left = new justifyCommand( editor, 'justifyleft', 'left' ),
center = new justifyCommand( editor, 'justifycenter', 'center' ),
right = new justifyCommand( editor, 'justifyright', 'right' ),
justify = new justifyCommand( editor, 'justifyblock', 'justify' );
editor.addCommand( 'justifyleft', left );
editor.addCommand( 'justifycenter', center );
editor.addCommand( 'justifyright', right );
editor.addCommand( 'justifyblock', justify );
if ( editor.ui.addButton ) {
editor.ui.addButton( 'JustifyLeft', {
label: editor.lang.common.alignLeft,
command: 'justifyleft',
toolbar: 'align,10'
} );
editor.ui.addButton( 'JustifyCenter', {
label: editor.lang.common.center,
command: 'justifycenter',
toolbar: 'align,20'
} );
editor.ui.addButton( 'JustifyRight', {
label: editor.lang.common.alignRight,
command: 'justifyright',
toolbar: 'align,30'
} );
editor.ui.addButton( 'JustifyBlock', {
label: editor.lang.common.justify,
command: 'justifyblock',
toolbar: 'align,40'
} );
}
editor.on( 'dirChanged', onDirChanged );
}
} );
} )();
/**
* List of classes to use for aligning the contents. If it's `null`, no classes will be used
* and instead the corresponding CSS values will be used.
*
* The array should contain 4 members, in the following order: left, center, right, justify.
*
* // Use the classes 'AlignLeft', 'AlignCenter', 'AlignRight', 'AlignJustify'
* config.justifyClasses = [ 'AlignLeft', 'AlignCenter', 'AlignRight', 'AlignJustify' ];
*
* @cfg {Array} [justifyClasses=null]
* @member CKEDITOR.config
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
( function() {
var cssStyle = CKEDITOR.htmlParser.cssStyle,
cssLength = CKEDITOR.tools.cssLength;
var cssLengthRegex = /^((?:\d*(?:\.\d+))|(?:\d+))(.*)?$/i;
// Replacing the former CSS length value with the later one, with
// adjustment to the length unit.
function replaceCssLength( length1, length2 ) {
var parts1 = cssLengthRegex.exec( length1 ),
parts2 = cssLengthRegex.exec( length2 );
// Omit pixel length unit when necessary,
// e.g. replaceCssLength( 10, '20px' ) -> 20
if ( parts1 ) {
if ( !parts1[ 2 ] && parts2[ 2 ] == 'px' )
return parts2[ 1 ];
if ( parts1[ 2 ] == 'px' && !parts2[ 2 ] )
return parts2[ 1 ] + 'px';
}
return length2;
}
var htmlFilterRules = {
elements: {
$: function( element ) {
var attributes = element.attributes,
realHtml = attributes && attributes[ 'data-cke-realelement' ],
realFragment = realHtml && new CKEDITOR.htmlParser.fragment.fromHtml( decodeURIComponent( realHtml ) ),
realElement = realFragment && realFragment.children[ 0 ];
// Width/height in the fake object are subjected to clone into the real element.
if ( realElement && element.attributes[ 'data-cke-resizable' ] ) {
var styles = new cssStyle( element ).rules,
realAttrs = realElement.attributes,
width = styles.width,
height = styles.height;
width && ( realAttrs.width = replaceCssLength( realAttrs.width, width ) );
height && ( realAttrs.height = replaceCssLength( realAttrs.height, height ) );
}
return realElement;
}
}
};
CKEDITOR.plugins.add( 'fakeobjects', {
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
// Allow image with all styles and classes plus src, alt and title attributes.
// We need them when fakeobject is pasted.
editor.filter.allow( 'img[!data-cke-realelement,src,alt,title](*){*}', 'fakeobjects' );
},
afterInit: function( editor ) {
var dataProcessor = editor.dataProcessor,
htmlFilter = dataProcessor && dataProcessor.htmlFilter;
if ( htmlFilter ) {
htmlFilter.addRules( htmlFilterRules, {
applyToAll: true
} );
}
}
} );
/**
* Creates fake {@link CKEDITOR.dom.element} based on real element.
* Fake element is an img with special attributes, which keep real element properties.
*
* @member CKEDITOR.editor
* @param {CKEDITOR.dom.element} realElement Real element to transform.
* @param {String} className Class name which will be used as class of fake element.
* @param {String} realElementType Stores type of fake element.
* @param {Boolean} isResizable Keeps information if element is resizable.
* @returns {CKEDITOR.dom.element} Fake element.
*/
CKEDITOR.editor.prototype.createFakeElement = function( realElement, className, realElementType, isResizable ) {
var lang = this.lang.fakeobjects,
label = lang[ realElementType ] || lang.unknown;
var attributes = {
'class': className,
'data-cke-realelement': encodeURIComponent( realElement.getOuterHtml() ),
'data-cke-real-node-type': realElement.type,
alt: label,
title: label,
align: realElement.getAttribute( 'align' ) || ''
};
// Do not set "src" on high-contrast so the alt text is displayed. (https://dev.ckeditor.com/ticket/8945)
if ( !CKEDITOR.env.hc )
attributes.src = CKEDITOR.tools.transparentImageData;
if ( realElementType )
attributes[ 'data-cke-real-element-type' ] = realElementType;
if ( isResizable ) {
attributes[ 'data-cke-resizable' ] = isResizable;
var fakeStyle = new cssStyle();
var width = realElement.getAttribute( 'width' ),
height = realElement.getAttribute( 'height' );
width && ( fakeStyle.rules.width = cssLength( width ) );
height && ( fakeStyle.rules.height = cssLength( height ) );
fakeStyle.populate( attributes );
}
return this.document.createElement( 'img', { attributes: attributes } );
};
/**
* Creates fake {@link CKEDITOR.htmlParser.element} based on real element.
*
* @member CKEDITOR.editor
* @param {CKEDITOR.dom.element} realElement Real element to transform.
* @param {String} className Class name which will be used as class of fake element.
* @param {String} realElementType Store type of fake element.
* @param {Boolean} isResizable Keep information if element is resizable.
* @returns {CKEDITOR.htmlParser.element} Fake htmlParser element.
*/
CKEDITOR.editor.prototype.createFakeParserElement = function( realElement, className, realElementType, isResizable ) {
var lang = this.lang.fakeobjects,
label = lang[ realElementType ] || lang.unknown,
html;
var writer = new CKEDITOR.htmlParser.basicWriter();
realElement.writeHtml( writer );
html = writer.getHtml();
var attributes = {
'class': className,
'data-cke-realelement': encodeURIComponent( html ),
'data-cke-real-node-type': realElement.type,
alt: label,
title: label,
align: realElement.attributes.align || ''
};
// Do not set "src" on high-contrast so the alt text is displayed. (https://dev.ckeditor.com/ticket/8945)
if ( !CKEDITOR.env.hc )
attributes.src = CKEDITOR.tools.transparentImageData;
if ( realElementType )
attributes[ 'data-cke-real-element-type' ] = realElementType;
if ( isResizable ) {
attributes[ 'data-cke-resizable' ] = isResizable;
var realAttrs = realElement.attributes,
fakeStyle = new cssStyle();
var width = realAttrs.width,
height = realAttrs.height;
width !== undefined && ( fakeStyle.rules.width = cssLength( width ) );
height !== undefined && ( fakeStyle.rules.height = cssLength( height ) );
fakeStyle.populate( attributes );
}
return new CKEDITOR.htmlParser.element( 'img', attributes );
};
/**
* Creates {@link CKEDITOR.dom.element} from fake element.
*
* @member CKEDITOR.editor
* @param {CKEDITOR.dom.element} fakeElement Fake element to transform.
* @returns {CKEDITOR.dom.element/null} Returns real element or `null` if transformed element wasn't fake.
*/
CKEDITOR.editor.prototype.restoreRealElement = function( fakeElement ) {
if ( fakeElement.data( 'cke-real-node-type' ) != CKEDITOR.NODE_ELEMENT )
return null;
var element = CKEDITOR.dom.element.createFromHtml( decodeURIComponent( fakeElement.data( 'cke-realelement' ) ), this.document );
if ( fakeElement.data( 'cke-resizable' ) ) {
var width = fakeElement.getStyle( 'width' ),
height = fakeElement.getStyle( 'height' );
width && element.setAttribute( 'width', replaceCssLength( element.getAttribute( 'width' ), width ) );
height && element.setAttribute( 'height', replaceCssLength( element.getAttribute( 'height' ), height ) );
}
return element;
};
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
'use strict';
( function() {
CKEDITOR.plugins.add( 'link', {
requires: 'dialog,fakeobjects',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
onLoad: function() {
// Add the CSS styles for anchor placeholders.
var iconPath = CKEDITOR.getUrl( this.path + 'images' + ( CKEDITOR.env.hidpi ? '/hidpi' : '' ) + '/anchor.png' ),
baseStyle = 'background:url(' + iconPath + ') no-repeat %1 center;border:1px dotted #00f;background-size:16px;';
var template = '.%2 a.cke_anchor,' +
'.%2 a.cke_anchor_empty' +
',.cke_editable.%2 a[name]' +
',.cke_editable.%2 a[data-cke-saved-name]' +
'{' +
baseStyle +
'padding-%1:18px;' +
// Show the arrow cursor for the anchor image (FF at least).
'cursor:auto;' +
'}' +
'.%2 img.cke_anchor' +
'{' +
baseStyle +
'width:16px;' +
'min-height:15px;' +
// The default line-height on IE.
'height:1.15em;' +
// Opera works better with "middle" (even if not perfect)
'vertical-align:text-bottom;' +
'}';
// Styles with contents direction awareness.
function cssWithDir( dir ) {
return template.replace( /%1/g, dir == 'rtl' ? 'right' : 'left' ).replace( /%2/g, 'cke_contents_' + dir );
}
CKEDITOR.addCss( cssWithDir( 'ltr' ) + cssWithDir( 'rtl' ) );
},
init: function( editor ) {
var allowed = 'a[!href]',
required = 'a[href]';
if ( CKEDITOR.dialog.isTabEnabled( editor, 'link', 'advanced' ) )
allowed = allowed.replace( ']', ',accesskey,charset,dir,id,lang,name,rel,tabindex,title,type,download]{*}(*)' );
if ( CKEDITOR.dialog.isTabEnabled( editor, 'link', 'target' ) )
allowed = allowed.replace( ']', ',target,onclick]' );
// Add the link and unlink buttons.
editor.addCommand( 'link', new CKEDITOR.dialogCommand( 'link', {
allowedContent: allowed,
requiredContent: required
} ) );
editor.addCommand( 'anchor', new CKEDITOR.dialogCommand( 'anchor', {
allowedContent: 'a[!name,id]',
requiredContent: 'a[name]'
} ) );
editor.addCommand( 'unlink', new CKEDITOR.unlinkCommand() );
editor.addCommand( 'removeAnchor', new CKEDITOR.removeAnchorCommand() );
editor.setKeystroke( CKEDITOR.CTRL + 76 /*L*/, 'link' );
// (#2478)
editor.setKeystroke( CKEDITOR.CTRL + 75 /*K*/, 'link' );
if ( editor.ui.addButton ) {
editor.ui.addButton( 'Link', {
label: editor.lang.link.toolbar,
command: 'link',
toolbar: 'links,10'
} );
editor.ui.addButton( 'Unlink', {
label: editor.lang.link.unlink,
command: 'unlink',
toolbar: 'links,20'
} );
editor.ui.addButton( 'Anchor', {
label: editor.lang.link.anchor.toolbar,
command: 'anchor',
toolbar: 'links,30'
} );
}
CKEDITOR.dialog.add( 'link', this.path + 'dialogs/link.js' );
CKEDITOR.dialog.add( 'anchor', this.path + 'dialogs/anchor.js' );
editor.on( 'doubleclick', function( evt ) {
// If the link has descendants and the last part of it is also a part of a word partially
// unlinked, clicked element may be a descendant of the link, not the link itself (https://dev.ckeditor.com/ticket/11956).
// The evt.data.element.getAscendant( 'img', 1 ) condition allows opening anchor dialog if the anchor is empty (#501).
var element = evt.data.element.getAscendant( { a: 1, img: 1 }, true );
if ( element && !element.isReadOnly() ) {
if ( element.is( 'a' ) ) {
evt.data.dialog = ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) ? 'anchor' : 'link';
// Pass the link to be selected along with event data.
evt.data.link = element;
} else if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) ) {
evt.data.dialog = 'anchor';
}
}
}, null, null, 0 );
// If event was cancelled, link passed in event data will not be selected.
editor.on( 'doubleclick', function( evt ) {
// Make sure both links and anchors are selected (https://dev.ckeditor.com/ticket/11822).
if ( evt.data.dialog in { link: 1, anchor: 1 } && evt.data.link )
editor.getSelection().selectElement( evt.data.link );
}, null, null, 20 );
// If the "menu" plugin is loaded, register the menu items.
if ( editor.addMenuItems ) {
editor.addMenuItems( {
anchor: {
label: editor.lang.link.anchor.menu,
command: 'anchor',
group: 'anchor',
order: 1
},
removeAnchor: {
label: editor.lang.link.anchor.remove,
command: 'removeAnchor',
group: 'anchor',
order: 5
},
link: {
label: editor.lang.link.menu,
command: 'link',
group: 'link',
order: 1
},
unlink: {
label: editor.lang.link.unlink,
command: 'unlink',
group: 'link',
order: 5
}
} );
}
// If the "contextmenu" plugin is loaded, register the listeners.
if ( editor.contextMenu ) {
editor.contextMenu.addListener( function( element ) {
if ( !element || element.isReadOnly() )
return null;
var anchor = CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element );
if ( !anchor && !( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) )
return null;
var menu = {};
if ( anchor.getAttribute( 'href' ) && anchor.getChildCount() )
menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF };
if ( anchor && anchor.hasAttribute( 'name' ) )
menu.anchor = menu.removeAnchor = CKEDITOR.TRISTATE_OFF;
return menu;
} );
}
this.compiledProtectionFunction = getCompiledProtectionFunction( editor );
},
afterInit: function( editor ) {
// Empty anchors upcasting to fake objects.
editor.dataProcessor.dataFilter.addRules( {
elements: {
a: function( element ) {
if ( !element.attributes.name )
return null;
if ( !element.children.length )
return editor.createFakeParserElement( element, 'cke_anchor', 'anchor' );
return null;
}
}
} );
var pathFilters = editor._.elementsPath && editor._.elementsPath.filters;
if ( pathFilters ) {
pathFilters.push( function( element, name ) {
if ( name == 'a' ) {
if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) || ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) )
return 'anchor';
}
} );
}
}
} );
// Loads the parameters in a selected link to the link dialog fields.
var javascriptProtocolRegex = /^javascript:/,
emailRegex = /^mailto:([^?]+)(?:\?(.+))?$/,
emailSubjectRegex = /subject=([^;?:@&=$,\/]*)/i,
emailBodyRegex = /body=([^;?:@&=$,\/]*)/i,
anchorRegex = /^#(.*)$/,
urlRegex = /^((?:http|https|ftp|news):\/\/)?(.*)$/,
selectableTargets = /^(_(?:self|top|parent|blank))$/,
encodedEmailLinkRegex = /^javascript:void\(location\.href='mailto:'\+String\.fromCharCode\(([^)]+)\)(?:\+'(.*)')?\)$/,
functionCallProtectedEmailLinkRegex = /^javascript:([^(]+)\(([^)]+)\)$/,
popupRegex = /\s*window.open\(\s*this\.href\s*,\s*(?:'([^']*)'|null)\s*,\s*'([^']*)'\s*\)\s*;\s*return\s*false;*\s*/,
popupFeaturesRegex = /(?:^|,)([^=]+)=(\d+|yes|no)/gi,
telRegex = /^tel:(.*)$/;
var advAttrNames = {
id: 'advId',
dir: 'advLangDir',
accessKey: 'advAccessKey',
// 'data-cke-saved-name': 'advName',
name: 'advName',
lang: 'advLangCode',
tabindex: 'advTabIndex',
title: 'advTitle',
type: 'advContentType',
'class': 'advCSSClasses',
charset: 'advCharset',
style: 'advStyles',
rel: 'advRel'
};
function unescapeSingleQuote( str ) {
return str.replace( /\\'/g, '\'' );
}
function escapeSingleQuote( str ) {
return str.replace( /'/g, '\\$&' );
}
function protectEmailAddressAsEncodedString( address ) {
var charCode,
length = address.length,
encodedChars = [];
for ( var i = 0; i < length; i++ ) {
charCode = address.charCodeAt( i );
encodedChars.push( charCode );
}
return 'String.fromCharCode(' + encodedChars.join( ',' ) + ')';
}
function protectEmailLinkAsFunction( editor, email ) {
var plugin = editor.plugins.link,
name = plugin.compiledProtectionFunction.name,
params = plugin.compiledProtectionFunction.params,
paramName, paramValue, retval;
retval = [ name, '(' ];
for ( var i = 0; i < params.length; i++ ) {
paramName = params[ i ].toLowerCase();
paramValue = email[ paramName ];
i > 0 && retval.push( ',' );
retval.push( '\'', paramValue ? escapeSingleQuote( encodeURIComponent( email[ paramName ] ) ) : '', '\'' );
}
retval.push( ')' );
return retval.join( '' );
}
function getCompiledProtectionFunction( editor ) {
var emailProtection = editor.config.emailProtection || '',
compiledProtectionFunction;
// Compile the protection function pattern.
if ( emailProtection && emailProtection != 'encode' ) {
compiledProtectionFunction = {};
emailProtection.replace( /^([^(]+)\(([^)]+)\)$/, function( match, funcName, params ) {
compiledProtectionFunction.name = funcName;
compiledProtectionFunction.params = [];
params.replace( /[^,\s]+/g, function( param ) {
compiledProtectionFunction.params.push( param );
} );
} );
}
return compiledProtectionFunction;
}
/**
* Set of Link plugin helpers.
*
* @class
* @singleton
*/
CKEDITOR.plugins.link = {
/**
* Get the surrounding link element of the current selection.
*
* CKEDITOR.plugins.link.getSelectedLink( editor );
*
* // The following selections will all return the link element.
*
* li^nk
* [link]
* text[link]
* li[nk ]
* [li]nk ]
* [li]nk
*
* @since 3.2.1
* @param {CKEDITOR.editor} editor
* @param {Boolean} [returnMultiple=false] Indicates whether the function should return only the first selected link or all of them.
* @returns {CKEDITOR.dom.element/CKEDITOR.dom.element[]/null} A single link element or an array of link
* elements relevant to the current selection.
*/
getSelectedLink: function( editor, returnMultiple ) {
var selection = editor.getSelection(),
selectedElement = selection.getSelectedElement(),
ranges = selection.getRanges(),
links = [],
link,
range,
i;
if ( !returnMultiple && selectedElement && selectedElement.is( 'a' ) ) {
return selectedElement;
}
for ( i = 0; i < ranges.length; i++ ) {
range = selection.getRanges()[ i ];
// Skip bogus to cover cases of multiple selection inside tables (#tp2245).
// Shrink to element to prevent losing anchor (#859).
range.shrink( CKEDITOR.SHRINK_ELEMENT, true, { skipBogus: true } );
link = editor.elementPath( range.getCommonAncestor() ).contains( 'a', 1 );
if ( link && returnMultiple ) {
links.push( link );
} else if ( link ) {
return link;
}
}
return returnMultiple ? links : null;
},
/**
* Collects anchors available in the editor (i.e. used by the Link plugin).
* Note that the scope of search is different for inline (the "global" document) and
* classic (`iframe`-based) editors (the "inner" document).
*
* @since 4.3.3
* @param {CKEDITOR.editor} editor
* @returns {CKEDITOR.dom.element[]} An array of anchor elements.
*/
getEditorAnchors: function( editor ) {
var editable = editor.editable(),
// The scope of search for anchors is the entire document for inline editors
// and editor's editable for classic editor/divarea (https://dev.ckeditor.com/ticket/11359).
scope = ( editable.isInline() && !editor.plugins.divarea ) ? editor.document : editable,
links = scope.getElementsByTag( 'a' ),
imgs = scope.getElementsByTag( 'img' ),
anchors = [],
i = 0,
item;
// Retrieve all anchors within the scope.
while ( ( item = links.getItem( i++ ) ) ) {
if ( item.data( 'cke-saved-name' ) || item.hasAttribute( 'name' ) ) {
anchors.push( {
name: item.data( 'cke-saved-name' ) || item.getAttribute( 'name' ),
id: item.getAttribute( 'id' )
} );
}
}
// Retrieve all "fake anchors" within the scope.
i = 0;
while ( ( item = imgs.getItem( i++ ) ) ) {
if ( ( item = this.tryRestoreFakeAnchor( editor, item ) ) ) {
anchors.push( {
name: item.getAttribute( 'name' ),
id: item.getAttribute( 'id' )
} );
}
}
return anchors;
},
/**
* Opera and WebKit do not make it possible to select empty anchors. Fake
* elements must be used for them.
*
* @readonly
* @deprecated 4.3.3 It is set to `true` in every browser.
* @property {Boolean}
*/
fakeAnchor: true,
/**
* For browsers that do not support CSS3 `a[name]:empty()`. Note that IE9 is included because of https://dev.ckeditor.com/ticket/7783.
*
* @readonly
* @deprecated 4.3.3 It is set to `false` in every browser.
* @property {Boolean} synAnchorSelector
*/
/**
* For browsers that have editing issues with an empty anchor.
*
* @readonly
* @deprecated 4.3.3 It is set to `false` in every browser.
* @property {Boolean} emptyAnchorFix
*/
/**
* Returns an element representing a real anchor restored from a fake anchor.
*
* @param {CKEDITOR.editor} editor
* @param {CKEDITOR.dom.element} element
* @returns {CKEDITOR.dom.element} Restored anchor element or nothing if the
* passed element was not a fake anchor.
*/
tryRestoreFakeAnchor: function( editor, element ) {
if ( element && element.data( 'cke-real-element-type' ) && element.data( 'cke-real-element-type' ) == 'anchor' ) {
var link = editor.restoreRealElement( element );
if ( link.data( 'cke-saved-name' ) )
return link;
}
},
/**
* Parses attributes of the link element and returns an object representing
* the current state (data) of the link. This data format is a plain object accepted
* e.g. by the Link dialog window and {@link #getLinkAttributes}.
*
* **Note:** Data model format produced by the parser must be compatible with the Link
* plugin dialog because it is passed directly to {@link CKEDITOR.dialog#setupContent}.
*
* @since 4.4.0
* @param {CKEDITOR.editor} editor
* @param {CKEDITOR.dom.element} element
* @returns {Object} An object of link data.
*/
parseLinkAttributes: function( editor, element ) {
var href = ( element && ( element.data( 'cke-saved-href' ) || element.getAttribute( 'href' ) ) ) || '',
compiledProtectionFunction = editor.plugins.link.compiledProtectionFunction,
emailProtection = editor.config.emailProtection,
javascriptMatch, emailMatch, anchorMatch, urlMatch, telMatch,
retval = {};
if ( ( javascriptMatch = href.match( javascriptProtocolRegex ) ) ) {
if ( emailProtection == 'encode' ) {
href = href.replace( encodedEmailLinkRegex, function( match, protectedAddress, rest ) {
// Without it 'undefined' is appended to e-mails without subject and body (https://dev.ckeditor.com/ticket/9192).
rest = rest || '';
return 'mailto:' +
String.fromCharCode.apply( String, protectedAddress.split( ',' ) ) +
unescapeSingleQuote( rest );
} );
}
// Protected email link as function call.
else if ( emailProtection ) {
href.replace( functionCallProtectedEmailLinkRegex, function( match, funcName, funcArgs ) {
if ( funcName == compiledProtectionFunction.name ) {
retval.type = 'email';
var email = retval.email = {};
var paramRegex = /[^,\s]+/g,
paramQuoteRegex = /(^')|('$)/g,
paramsMatch = funcArgs.match( paramRegex ),
paramsMatchLength = paramsMatch.length,
paramName, paramVal;
for ( var i = 0; i < paramsMatchLength; i++ ) {
paramVal = decodeURIComponent( unescapeSingleQuote( paramsMatch[ i ].replace( paramQuoteRegex, '' ) ) );
paramName = compiledProtectionFunction.params[ i ].toLowerCase();
email[ paramName ] = paramVal;
}
email.address = [ email.name, email.domain ].join( '@' );
}
} );
}
}
if ( !retval.type ) {
if ( ( anchorMatch = href.match( anchorRegex ) ) ) {
retval.type = 'anchor';
retval.anchor = {};
retval.anchor.name = retval.anchor.id = anchorMatch[ 1 ];
} else if ( ( telMatch = href.match( telRegex ) ) ) {
retval.type = 'tel';
retval.tel = telMatch[ 1 ];
}
// Protected email link as encoded string.
else if ( ( emailMatch = href.match( emailRegex ) ) ) {
var subjectMatch = href.match( emailSubjectRegex ),
bodyMatch = href.match( emailBodyRegex );
retval.type = 'email';
var email = ( retval.email = {} );
email.address = emailMatch[ 1 ];
subjectMatch && ( email.subject = decodeURIComponent( subjectMatch[ 1 ] ) );
bodyMatch && ( email.body = decodeURIComponent( bodyMatch[ 1 ] ) );
}
// urlRegex matches empty strings, so need to check for href as well.
else if ( href && ( urlMatch = href.match( urlRegex ) ) ) {
retval.type = 'url';
retval.url = {};
retval.url.protocol = urlMatch[ 1 ];
retval.url.url = urlMatch[ 2 ];
}
}
// Load target and popup settings.
if ( element ) {
var target = element.getAttribute( 'target' );
// IE BUG: target attribute is an empty string instead of null in IE if it's not set.
if ( !target ) {
var onclick = element.data( 'cke-pa-onclick' ) || element.getAttribute( 'onclick' ),
onclickMatch = onclick && onclick.match( popupRegex );
if ( onclickMatch ) {
retval.target = {
type: 'popup',
name: onclickMatch[ 1 ]
};
var featureMatch;
while ( ( featureMatch = popupFeaturesRegex.exec( onclickMatch[ 2 ] ) ) ) {
// Some values should remain numbers (https://dev.ckeditor.com/ticket/7300)
if ( ( featureMatch[ 2 ] == 'yes' || featureMatch[ 2 ] == '1' ) && !( featureMatch[ 1 ] in { height: 1, width: 1, top: 1, left: 1 } ) )
retval.target[ featureMatch[ 1 ] ] = true;
else if ( isFinite( featureMatch[ 2 ] ) )
retval.target[ featureMatch[ 1 ] ] = featureMatch[ 2 ];
}
}
} else {
retval.target = {
type: target.match( selectableTargets ) ? target : 'frame',
name: target
};
}
var download = element.getAttribute( 'download' );
if ( download !== null ) {
retval.download = true;
}
var advanced = {};
for ( var a in advAttrNames ) {
var val = element.getAttribute( a );
if ( val )
advanced[ advAttrNames[ a ] ] = val;
}
var advName = element.data( 'cke-saved-name' ) || advanced.advName;
if ( advName )
advanced.advName = advName;
if ( !CKEDITOR.tools.isEmpty( advanced ) )
retval.advanced = advanced;
}
return retval;
},
/**
* Converts link data produced by {@link #parseLinkAttributes} into an object which consists
* of attributes to be set (with their values) and an array of attributes to be removed.
* This method can be used to compose or to update any link element with the given data.
*
* @since 4.4.0
* @param {CKEDITOR.editor} editor
* @param {Object} data Data in {@link #parseLinkAttributes} format.
* @returns {Object} An object consisting of two keys, i.e.:
*
* {
* // Attributes to be set.
* set: {
* href: 'http://foo.bar',
* target: 'bang'
* },
* // Attributes to be removed.
* removed: [
* 'id', 'style'
* ]
* }
*
*/
getLinkAttributes: function( editor, data ) {
var emailProtection = editor.config.emailProtection || '',
set = {};
// Compose the URL.
switch ( data.type ) {
case 'url':
var protocol = ( data.url && data.url.protocol !== undefined ) ? data.url.protocol : 'http://',
url = ( data.url && CKEDITOR.tools.trim( data.url.url ) ) || '';
set[ 'data-cke-saved-href' ] = ( url.indexOf( '/' ) === 0 ) ? url : protocol + url;
break;
case 'anchor':
var name = ( data.anchor && data.anchor.name ),
id = ( data.anchor && data.anchor.id );
set[ 'data-cke-saved-href' ] = '#' + ( name || id || '' );
break;
case 'email':
var email = data.email,
address = email.address,
linkHref;
switch ( emailProtection ) {
case '':
case 'encode':
var subject = encodeURIComponent( email.subject || '' ),
body = encodeURIComponent( email.body || '' ),
argList = [];
// Build the e-mail parameters first.
subject && argList.push( 'subject=' + subject );
body && argList.push( 'body=' + body );
argList = argList.length ? '?' + argList.join( '&' ) : '';
if ( emailProtection == 'encode' ) {
linkHref = [
'javascript:void(location.href=\'mailto:\'+', // jshint ignore:line
protectEmailAddressAsEncodedString( address )
];
// parameters are optional.
argList && linkHref.push( '+\'', escapeSingleQuote( argList ), '\'' );
linkHref.push( ')' );
} else {
linkHref = [ 'mailto:', address, argList ];
}
break;
default:
// Separating name and domain.
var nameAndDomain = address.split( '@', 2 );
email.name = nameAndDomain[ 0 ];
email.domain = nameAndDomain[ 1 ];
linkHref = [ 'javascript:', protectEmailLinkAsFunction( editor, email ) ]; // jshint ignore:line
}
set[ 'data-cke-saved-href' ] = linkHref.join( '' );
break;
case 'tel':
set[ 'data-cke-saved-href' ] = 'tel:' + data.tel;
break;
}
// Popups and target.
if ( data.target ) {
if ( data.target.type == 'popup' ) {
var onclickList = [
'window.open(this.href, \'', data.target.name || '', '\', \''
],
featureList = [
'resizable', 'status', 'location', 'toolbar', 'menubar', 'fullscreen', 'scrollbars', 'dependent'
],
featureLength = featureList.length,
addFeature = function( featureName ) {
if ( data.target[ featureName ] )
featureList.push( featureName + '=' + data.target[ featureName ] );
};
for ( var i = 0; i < featureLength; i++ )
featureList[ i ] = featureList[ i ] + ( data.target[ featureList[ i ] ] ? '=yes' : '=no' );
addFeature( 'width' );
addFeature( 'left' );
addFeature( 'height' );
addFeature( 'top' );
onclickList.push( featureList.join( ',' ), '\'); return false;' );
set[ 'data-cke-pa-onclick' ] = onclickList.join( '' );
}
else if ( data.target.type != 'notSet' && data.target.name ) {
set.target = data.target.name;
}
}
// Force download attribute.
if ( data.download ) {
set.download = '';
}
// Advanced attributes.
if ( data.advanced ) {
for ( var a in advAttrNames ) {
var val = data.advanced[ advAttrNames[ a ] ];
if ( val )
set[ a ] = val;
}
if ( set.name )
set[ 'data-cke-saved-name' ] = set.name;
}
// Browser need the "href" fro copy/paste link to work. (https://dev.ckeditor.com/ticket/6641)
if ( set[ 'data-cke-saved-href' ] )
set.href = set[ 'data-cke-saved-href' ];
var removed = {
target: 1,
onclick: 1,
'data-cke-pa-onclick': 1,
'data-cke-saved-name': 1,
'download': 1
};
if ( data.advanced )
CKEDITOR.tools.extend( removed, advAttrNames );
// Remove all attributes which are not currently set.
for ( var s in set )
delete removed[ s ];
return {
set: set,
removed: CKEDITOR.tools.object.keys( removed )
};
},
/**
* Determines whether an element should have a "Display Text" field in the Link dialog.
*
* @since 4.5.11
* @param {CKEDITOR.dom.element/null} element Selected element, `null` if none selected or if a ranged selection
* is made.
* @param {CKEDITOR.editor} editor The editor instance for which the check is performed.
* @returns {Boolean}
*/
showDisplayTextForElement: function( element, editor ) {
var undesiredElements = {
img: 1,
table: 1,
tbody: 1,
thead: 1,
tfoot: 1,
input: 1,
select: 1,
textarea: 1
},
selection = editor.getSelection();
// Widget duck typing, we don't want to show display text for widgets.
if ( editor.widgets && editor.widgets.focused ) {
return false;
}
if ( selection && selection.getRanges().length > 1 ) {
return false;
}
return !element || !element.getName || !element.is( undesiredElements );
}
};
// TODO Much probably there's no need to expose these as public objects.
CKEDITOR.unlinkCommand = function() {};
CKEDITOR.unlinkCommand.prototype = {
exec: function( editor ) {
// IE/Edge removes link from selection while executing "unlink" command when cursor
// is right before/after link's text. Therefore whole link must be selected and the
// position of cursor must be restored to its initial state after unlinking. (https://dev.ckeditor.com/ticket/13062)
if ( CKEDITOR.env.ie ) {
var range = editor.getSelection().getRanges()[ 0 ],
link = ( range.getPreviousEditableNode() && range.getPreviousEditableNode().getAscendant( 'a', true ) ) ||
( range.getNextEditableNode() && range.getNextEditableNode().getAscendant( 'a', true ) ),
bookmark;
if ( range.collapsed && link ) {
bookmark = range.createBookmark();
range.selectNodeContents( link );
range.select();
}
}
var style = new CKEDITOR.style( { element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 } );
editor.removeStyle( style );
if ( bookmark ) {
range.moveToBookmark( bookmark );
range.select();
}
},
refresh: function( editor, path ) {
// Despite our initial hope, document.queryCommandEnabled() does not work
// for this in Firefox. So we must detect the state by element paths.
var element = path.lastElement && path.lastElement.getAscendant( 'a', true );
if ( element && element.getName() == 'a' && element.getAttribute( 'href' ) && element.getChildCount() )
this.setState( CKEDITOR.TRISTATE_OFF );
else
this.setState( CKEDITOR.TRISTATE_DISABLED );
},
contextSensitive: 1,
startDisabled: 1,
requiredContent: 'a[href]',
editorFocus: 1
};
CKEDITOR.removeAnchorCommand = function() {};
CKEDITOR.removeAnchorCommand.prototype = {
exec: function( editor ) {
var sel = editor.getSelection(),
bms = sel.createBookmarks(),
anchor;
if ( sel && ( anchor = sel.getSelectedElement() ) && ( !anchor.getChildCount() ? CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, anchor ) : anchor.is( 'a' ) ) )
anchor.remove( 1 );
else {
if ( ( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) ) {
if ( anchor.hasAttribute( 'href' ) ) {
anchor.removeAttributes( { name: 1, 'data-cke-saved-name': 1 } );
anchor.removeClass( 'cke_anchor' );
} else {
anchor.remove( 1 );
}
}
}
sel.selectBookmarks( bms );
},
requiredContent: 'a[name]'
};
CKEDITOR.tools.extend( CKEDITOR.config, {
/**
* Whether to show the Advanced tab in the Link dialog window.
*
* @cfg {Boolean} [linkShowAdvancedTab=true]
* @member CKEDITOR.config
*/
linkShowAdvancedTab: true,
/**
* Whether to show the Target tab in the Link dialog window.
*
* @cfg {Boolean} [linkShowTargetTab=true]
* @member CKEDITOR.config
*/
linkShowTargetTab: true
/**
* Whether JavaScript code is allowed as a `href` attribute in an anchor tag.
* With this option enabled it is possible to create links like:
*
* hello world
*
* By default JavaScript links are not allowed and will not pass
* the Link dialog window validation.
*
* @since 4.4.1
* @cfg {Boolean} [linkJavaScriptLinksAllowed=false]
* @member CKEDITOR.config
*/
/**
* Optional JavaScript regular expression used whenever phone numbers in the Link dialog should be validated.
*
* config.linkPhoneRegExp = /^[0-9]{9}$/;
*
* @since 4.11.0
* @cfg {RegExp} [linkPhoneRegExp]
* @member CKEDITOR.config
*/
/**
* Optional message for the alert popup used when the phone number in the Link dialog does not pass the validation.
*
* config.linkPhoneMsg = "Invalid number";
*
* @since 4.11.0
* @cfg {String} [linkPhoneMsg]
* @member CKEDITOR.config
*/
} );
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Insert and remove numbered and bulleted lists.
*/
( function() {
var listNodeNames = { ol: 1, ul: 1 };
var whitespaces = CKEDITOR.dom.walker.whitespaces(),
bookmarks = CKEDITOR.dom.walker.bookmark(),
nonEmpty = function( node ) {
return !( whitespaces( node ) || bookmarks( node ) );
},
blockBogus = CKEDITOR.dom.walker.bogus();
function cleanUpDirection( element ) {
var dir, parent, parentDir;
if ( ( dir = element.getDirection() ) ) {
parent = element.getParent();
while ( parent && !( parentDir = parent.getDirection() ) )
parent = parent.getParent();
if ( dir == parentDir )
element.removeAttribute( 'dir' );
}
}
// Inherit inline styles from another element.
function inheritInlineStyles( parent, el ) {
var style = parent.getAttribute( 'style' );
// Put parent styles before child styles.
style && el.setAttribute( 'style', style.replace( /([^;])$/, '$1;' ) + ( el.getAttribute( 'style' ) || '' ) );
}
CKEDITOR.plugins.list = {
/**
* Convert a DOM list tree into a data structure that is easier to
* manipulate. This operation should be non-intrusive in the sense that it
* does not change the DOM tree, with the exception that it may add some
* markers to the list item nodes when database is specified.
*
* @member CKEDITOR.plugins.list
* @todo params
*/
listToArray: function( listNode, database, baseArray, baseIndentLevel, grandparentNode ) {
if ( !listNodeNames[ listNode.getName() ] )
return [];
if ( !baseIndentLevel )
baseIndentLevel = 0;
if ( !baseArray )
baseArray = [];
// Iterate over all list items to and look for inner lists.
for ( var i = 0, count = listNode.getChildCount(); i < count; i++ ) {
var listItem = listNode.getChild( i );
// Fixing malformed nested lists by moving it into a previous list item. (https://dev.ckeditor.com/ticket/6236)
if ( listItem.type == CKEDITOR.NODE_ELEMENT && listItem.getName() in CKEDITOR.dtd.$list )
CKEDITOR.plugins.list.listToArray( listItem, database, baseArray, baseIndentLevel + 1 );
// It may be a text node or some funny stuff.
if ( listItem.$.nodeName.toLowerCase() != 'li' )
continue;
var itemObj = { 'parent': listNode, indent: baseIndentLevel, element: listItem, contents: [] };
if ( !grandparentNode ) {
itemObj.grandparent = listNode.getParent();
if ( itemObj.grandparent && itemObj.grandparent.$.nodeName.toLowerCase() == 'li' )
itemObj.grandparent = itemObj.grandparent.getParent();
} else {
itemObj.grandparent = grandparentNode;
}
if ( database )
CKEDITOR.dom.element.setMarker( database, listItem, 'listarray_index', baseArray.length );
baseArray.push( itemObj );
for ( var j = 0, itemChildCount = listItem.getChildCount(), child; j < itemChildCount; j++ ) {
child = listItem.getChild( j );
if ( child.type == CKEDITOR.NODE_ELEMENT && listNodeNames[ child.getName() ] )
// Note the recursion here, it pushes inner list items with
// +1 indentation in the correct order.
CKEDITOR.plugins.list.listToArray( child, database, baseArray, baseIndentLevel + 1, itemObj.grandparent );
else
itemObj.contents.push( child );
}
}
return baseArray;
},
/**
* Convert our internal representation of a list back to a DOM forest.
*
* @member CKEDITOR.plugins.list
* @todo params
*/
arrayToList: function( listArray, database, baseIndex, paragraphMode, dir ) {
if ( !baseIndex )
baseIndex = 0;
if ( !listArray || listArray.length < baseIndex + 1 )
return null;
var i,
doc = listArray[ baseIndex ].parent.getDocument(),
retval = new CKEDITOR.dom.documentFragment( doc ),
rootNode = null,
currentIndex = baseIndex,
indentLevel = Math.max( listArray[ baseIndex ].indent, 0 ),
currentListItem = null,
orgDir, block,
paragraphName = ( paragraphMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
while ( 1 ) {
var item = listArray[ currentIndex ],
itemGrandParent = item.grandparent;
orgDir = item.element.getDirection( 1 );
if ( item.indent == indentLevel ) {
if ( !rootNode || listArray[ currentIndex ].parent.getName() != rootNode.getName() ) {
rootNode = listArray[ currentIndex ].parent.clone( false, 1 );
dir && rootNode.setAttribute( 'dir', dir );
retval.append( rootNode );
}
currentListItem = rootNode.append( item.element.clone( 0, 1 ) );
if ( orgDir != rootNode.getDirection( 1 ) )
currentListItem.setAttribute( 'dir', orgDir );
for ( i = 0; i < item.contents.length; i++ )
currentListItem.append( item.contents[ i ].clone( 1, 1 ) );
currentIndex++;
} else if ( item.indent == Math.max( indentLevel, 0 ) + 1 ) {
// Maintain original direction (https://dev.ckeditor.com/ticket/6861).
var currDir = listArray[ currentIndex - 1 ].element.getDirection( 1 ),
listData = CKEDITOR.plugins.list.arrayToList( listArray, null, currentIndex, paragraphMode, currDir != orgDir ? orgDir : null );
// If the next block is an with another list tree as the first
// child, we'll need to append a filler ( /NBSP) or the list item
// wouldn't be editable. (https://dev.ckeditor.com/ticket/6724)
if ( !currentListItem.getChildCount() && CKEDITOR.env.needsNbspFiller && doc.$.documentMode <= 7 )
currentListItem.append( doc.createText( '\xa0' ) );
currentListItem.append( listData.listNode );
currentIndex = listData.nextIndex;
} else if ( item.indent == -1 && !baseIndex && itemGrandParent ) {
if ( listNodeNames[ itemGrandParent.getName() ] ) {
currentListItem = item.element.clone( false, true );
if ( orgDir != itemGrandParent.getDirection( 1 ) )
currentListItem.setAttribute( 'dir', orgDir );
} else {
currentListItem = new CKEDITOR.dom.documentFragment( doc );
}
// Migrate all children to the new container,
// apply the proper text direction.
var dirLoose = itemGrandParent.getDirection( 1 ) != orgDir,
li = item.element,
className = li.getAttribute( 'class' ),
style = li.getAttribute( 'style' );
var needsBlock = currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT && ( paragraphMode != CKEDITOR.ENTER_BR || dirLoose || style || className );
var child,
count = item.contents.length,
cachedBookmark;
for ( i = 0; i < count; i++ ) {
child = item.contents[ i ];
// Append bookmark if we can, or cache it and append it when we'll know
// what to do with it. Generally - we want to keep it next to its original neighbour.
// Exception: if bookmark is the only child it hasn't got any neighbour, so handle it normally
// (wrap with block if needed).
if ( bookmarks( child ) && count > 1 ) {
// If we don't need block, it's simple - append bookmark directly to the current list item.
if ( !needsBlock )
currentListItem.append( child.clone( 1, 1 ) );
else
cachedBookmark = child.clone( 1, 1 );
}
// Block content goes directly to the current list item, without wrapping.
else if ( child.type == CKEDITOR.NODE_ELEMENT && child.isBlockBoundary() ) {
// Apply direction on content blocks.
if ( dirLoose && !child.getDirection() )
child.setAttribute( 'dir', orgDir );
inheritInlineStyles( li, child );
className && child.addClass( className );
// Close the block which we started for inline content.
block = null;
// Append bookmark directly before current child.
if ( cachedBookmark ) {
currentListItem.append( cachedBookmark );
cachedBookmark = null;
}
// Append this block element to the list item.
currentListItem.append( child.clone( 1, 1 ) );
}
// Some inline content was found - wrap it with block and append that
// block to the current list item or append it to the block previously created.
else if ( needsBlock ) {
// Establish new block to hold text direction and styles.
if ( !block ) {
block = doc.createElement( paragraphName );
currentListItem.append( block );
dirLoose && block.setAttribute( 'dir', orgDir );
}
// Copy over styles to new block;
style && block.setAttribute( 'style', style );
className && block.setAttribute( 'class', className );
// Append bookmark directly before current child.
if ( cachedBookmark ) {
block.append( cachedBookmark );
cachedBookmark = null;
}
block.append( child.clone( 1, 1 ) );
}
// E.g. BR mode - inline content appended directly to the list item.
else {
currentListItem.append( child.clone( 1, 1 ) );
}
}
// No content after bookmark - append it to the block if we had one
// or directly to the current list item if we finished directly in the current list item.
if ( cachedBookmark ) {
( block || currentListItem ).append( cachedBookmark );
cachedBookmark = null;
}
if ( currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT && currentIndex != listArray.length - 1 ) {
var last;
// Remove bogus if this browser uses them.
if ( CKEDITOR.env.needsBrFiller ) {
last = currentListItem.getLast();
if ( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( 'br' ) )
last.remove();
}
// If the last element is not a block, append to separate merged list items.
last = currentListItem.getLast( nonEmpty );
if ( !( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( CKEDITOR.dtd.$block ) ) )
currentListItem.append( doc.createElement( 'br' ) );
}
var currentListItemName = currentListItem.$.nodeName.toLowerCase();
if ( currentListItemName == 'div' || currentListItemName == 'p' ) {
currentListItem.appendBogus();
}
retval.append( currentListItem );
rootNode = null;
currentIndex++;
} else {
return null;
}
block = null;
if ( listArray.length <= currentIndex || Math.max( listArray[ currentIndex ].indent, 0 ) < indentLevel )
break;
}
if ( database ) {
var currentNode = retval.getFirst();
while ( currentNode ) {
if ( currentNode.type == CKEDITOR.NODE_ELEMENT ) {
// Clear marker attributes for the new list tree made of cloned nodes, if any.
CKEDITOR.dom.element.clearMarkers( database, currentNode );
// Clear redundant direction attribute specified on list items.
if ( currentNode.getName() in CKEDITOR.dtd.$listItem )
cleanUpDirection( currentNode );
}
currentNode = currentNode.getNextSourceNode();
}
}
return { listNode: retval, nextIndex: currentIndex };
}
};
function changeListType( editor, groupObj, database, listsCreated ) {
// This case is easy...
// 1. Convert the whole list into a one-dimensional array.
// 2. Change the list type by modifying the array.
// 3. Recreate the whole list by converting the array to a list.
// 4. Replace the original list with the recreated list.
var listArray = CKEDITOR.plugins.list.listToArray( groupObj.root, database ),
selectedListItems = [];
for ( var i = 0; i < groupObj.contents.length; i++ ) {
var itemNode = groupObj.contents[ i ];
itemNode = itemNode.getAscendant( 'li', true );
if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) )
continue;
selectedListItems.push( itemNode );
CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );
}
var root = groupObj.root,
doc = root.getDocument(),
listNode, newListNode;
for ( i = 0; i < selectedListItems.length; i++ ) {
var listIndex = selectedListItems[ i ].getCustomData( 'listarray_index' );
listNode = listArray[ listIndex ].parent;
// Switch to new list node for this particular item.
if ( !listNode.is( this.type ) ) {
newListNode = doc.createElement( this.type );
// Copy all attributes, except from 'start' and 'type'.
listNode.copyAttributes( newListNode, { start: 1, type: 1 } );
// The list-style-type property should be ignored.
newListNode.removeStyle( 'list-style-type' );
listArray[ listIndex ].parent = newListNode;
}
}
var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode );
var child,
length = newList.listNode.getChildCount();
for ( i = 0; i < length && ( child = newList.listNode.getChild( i ) ); i++ ) {
if ( child.getName() == this.type )
listsCreated.push( child );
}
newList.listNode.replace( groupObj.root );
editor.fire( 'contentDomInvalidated' );
}
function createList( editor, groupObj, listsCreated ) {
var contents = groupObj.contents,
doc = groupObj.root.getDocument(),
listContents = [];
// It is possible to have the contents returned by DomRangeIterator to be the same as the root.
// e.g. when we're running into table cells.
// In such a case, enclose the childNodes of contents[0] into a .
if ( contents.length == 1 && contents[ 0 ].equals( groupObj.root ) ) {
var divBlock = doc.createElement( 'div' );
contents[ 0 ].moveChildren && contents[ 0 ].moveChildren( divBlock );
contents[ 0 ].append( divBlock );
contents[ 0 ] = divBlock;
}
// Calculate the common parent node of all content blocks.
var commonParent = groupObj.contents[ 0 ].getParent();
for ( var i = 0; i < contents.length; i++ )
commonParent = commonParent.getCommonAncestor( contents[ i ].getParent() );
var useComputedState = editor.config.useComputedState,
listDir, explicitDirection;
useComputedState = useComputedState === undefined || useComputedState;
// We want to insert things that are in the same tree level only, so calculate the contents again
// by expanding the selected blocks to the same tree level.
for ( i = 0; i < contents.length; i++ ) {
var contentNode = contents[ i ],
parentNode;
while ( ( parentNode = contentNode.getParent() ) ) {
if ( parentNode.equals( commonParent ) ) {
listContents.push( contentNode );
// Determine the lists's direction.
if ( !explicitDirection && contentNode.getDirection() )
explicitDirection = 1;
var itemDir = contentNode.getDirection( useComputedState );
if ( listDir !== null ) {
// If at least one LI have a different direction than current listDir, we can't have listDir.
if ( listDir && listDir != itemDir )
listDir = null;
else
listDir = itemDir;
}
break;
}
contentNode = parentNode;
}
}
if ( listContents.length < 1 )
return;
// Insert the list to the DOM tree.
var insertAnchor = listContents[ listContents.length - 1 ].getNext(),
listNode = doc.createElement( this.type );
listsCreated.push( listNode );
var contentBlock, listItem;
while ( listContents.length ) {
contentBlock = listContents.shift();
listItem = doc.createElement( 'li' );
// If current block should be preserved, append it to list item instead of
// transforming it to
element.
if ( shouldPreserveBlock( contentBlock ) )
contentBlock.appendTo( listItem );
else {
contentBlock.copyAttributes( listItem );
// Remove direction attribute after it was merged into list root. (https://dev.ckeditor.com/ticket/7657)
if ( listDir && contentBlock.getDirection() ) {
listItem.removeStyle( 'direction' );
listItem.removeAttribute( 'dir' );
}
contentBlock.moveChildren( listItem );
contentBlock.remove();
}
listItem.appendTo( listNode );
}
// Apply list root dir only if it has been explicitly declared.
if ( listDir && explicitDirection )
listNode.setAttribute( 'dir', listDir );
if ( insertAnchor )
listNode.insertBefore( insertAnchor );
else
listNode.appendTo( commonParent );
}
function removeList( editor, groupObj, database ) {
// This is very much like the change list type operation.
// Except that we're changing the selected items' indent to -1 in the list array.
var listArray = CKEDITOR.plugins.list.listToArray( groupObj.root, database ),
selectedListItems = [];
for ( var i = 0; i < groupObj.contents.length; i++ ) {
var itemNode = groupObj.contents[ i ];
itemNode = itemNode.getAscendant( 'li', true );
if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) )
continue;
selectedListItems.push( itemNode );
CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );
}
var lastListIndex = null;
for ( i = 0; i < selectedListItems.length; i++ ) {
var listIndex = selectedListItems[ i ].getCustomData( 'listarray_index' );
listArray[ listIndex ].indent = -1;
lastListIndex = listIndex;
}
// After cutting parts of the list out with indent=-1, we still have to maintain the array list
// model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the
// list cannot be converted back to a real DOM list.
for ( i = lastListIndex + 1; i < listArray.length; i++ ) {
if ( listArray[ i ].indent > listArray[ i - 1 ].indent + 1 ) {
var indentOffset = listArray[ i - 1 ].indent + 1 - listArray[ i ].indent;
var oldIndent = listArray[ i ].indent;
while ( listArray[ i ] && listArray[ i ].indent >= oldIndent ) {
listArray[ i ].indent += indentOffset;
i++;
}
i--;
}
}
var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode, groupObj.root.getAttribute( 'dir' ) );
// Compensate before/after the list node if the surrounds are non-blocks.(https://dev.ckeditor.com/ticket/3836)
var docFragment = newList.listNode,
boundaryNode, siblingNode;
function compensateBrs( isStart ) {
if (
( boundaryNode = docFragment[ isStart ? 'getFirst' : 'getLast' ]() ) &&
!( boundaryNode.is && boundaryNode.isBlockBoundary() ) &&
( siblingNode = groupObj.root[ isStart ? 'getPrevious' : 'getNext' ]( CKEDITOR.dom.walker.invisible( true ) ) ) &&
!( siblingNode.is && siblingNode.isBlockBoundary( { br: 1 } ) )
) {
editor.document.createElement( 'br' )[ isStart ? 'insertBefore' : 'insertAfter' ]( boundaryNode );
}
}
compensateBrs( true );
compensateBrs();
docFragment.replace( groupObj.root );
editor.fire( 'contentDomInvalidated' );
}
var headerTagRegex = /^h[1-6]$/;
// Checks wheather this block should be element preserved (not transformed to ) when creating list.
function shouldPreserveBlock( block ) {
return (
// https://dev.ckeditor.com/ticket/5335
block.is( 'pre' ) ||
// https://dev.ckeditor.com/ticket/5271 - this is a header.
headerTagRegex.test( block.getName() ) ||
// 11083 - this is a non-editable element.
block.getAttribute( 'contenteditable' ) == 'false'
);
}
function listCommand( name, type ) {
this.name = name;
this.type = type;
this.context = type;
this.allowedContent = type + ' li';
this.requiredContent = type;
}
var elementType = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT );
// Merge child nodes with direction preserved. (https://dev.ckeditor.com/ticket/7448)
function mergeChildren( from, into, refNode, forward ) {
var child, itemDir;
while ( ( child = from[ forward ? 'getLast' : 'getFirst' ]( elementType ) ) ) {
if ( ( itemDir = child.getDirection( 1 ) ) !== into.getDirection( 1 ) )
child.setAttribute( 'dir', itemDir );
child.remove();
refNode ? child[ forward ? 'insertBefore' : 'insertAfter' ]( refNode ) : into.append( child, forward );
refNode = child;
}
}
listCommand.prototype = {
exec: function( editor ) {
// Run state check first of all.
this.refresh( editor, editor.elementPath() );
var config = editor.config,
selection = editor.getSelection(),
ranges = selection && selection.getRanges();
// Midas lists rule #1 says we can create a list even in an empty document.
// But DOM iterator wouldn't run if the document is really empty.
// So create a paragraph if the document is empty and we're going to create a list.
if ( this.state == CKEDITOR.TRISTATE_OFF ) {
var editable = editor.editable();
if ( !editable.getFirst( nonEmpty ) ) {
config.enterMode == CKEDITOR.ENTER_BR ? editable.appendBogus() : ranges[ 0 ].fixBlock( 1, config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
selection.selectRanges( ranges );
}
// Maybe a single range there enclosing the whole list,
// turn on the list state manually(https://dev.ckeditor.com/ticket/4129).
else {
var range = ranges.length == 1 && ranges[ 0 ],
enclosedNode = range && range.getEnclosedNode();
if ( enclosedNode && enclosedNode.is && this.type == enclosedNode.getName() )
this.setState( CKEDITOR.TRISTATE_ON );
}
}
var bookmarks = selection.createBookmarks( true );
// Group the blocks up because there are many cases where multiple lists have to be created,
// or multiple lists have to be cancelled.
var listGroups = [],
database = {},
rangeIterator = ranges.createIterator(),
index = 0;
while ( ( range = rangeIterator.getNextRange() ) && ++index ) {
var boundaryNodes = range.getBoundaryNodes(),
startNode = boundaryNodes.startNode,
endNode = boundaryNodes.endNode;
if ( startNode.type == CKEDITOR.NODE_ELEMENT && startNode.getName() == 'td' )
range.setStartAt( boundaryNodes.startNode, CKEDITOR.POSITION_AFTER_START );
if ( endNode.type == CKEDITOR.NODE_ELEMENT && endNode.getName() == 'td' )
range.setEndAt( boundaryNodes.endNode, CKEDITOR.POSITION_BEFORE_END );
var iterator = range.createIterator(),
block;
iterator.forceBrBreak = ( this.state == CKEDITOR.TRISTATE_OFF );
while ( ( block = iterator.getNextParagraph() ) ) {
// Avoid duplicate blocks get processed across ranges.
// Avoid processing comments, we don't want to touch it.
if ( block.getCustomData( 'list_block' ) || hasCommentsChildOnly( block ) )
continue;
else
CKEDITOR.dom.element.setMarker( database, block, 'list_block', 1 );
var path = editor.elementPath( block ),
pathElements = path.elements,
pathElementsCount = pathElements.length,
processedFlag = 0,
blockLimit = path.blockLimit,
element;
// First, try to group by a list ancestor.
for ( var i = pathElementsCount - 1; i >= 0 && ( element = pathElements[ i ] ); i-- ) {
// Don't leak outside block limit (https://dev.ckeditor.com/ticket/3940).
if ( listNodeNames[ element.getName() ] && blockLimit.contains( element ) ) {
// If we've encountered a list inside a block limit
// The last group object of the block limit element should
// no longer be valid. Since paragraphs after the list
// should belong to a different group of paragraphs before
// the list. (Bug https://dev.ckeditor.com/ticket/1309)
blockLimit.removeCustomData( 'list_group_object_' + index );
var groupObj = element.getCustomData( 'list_group_object' );
if ( groupObj )
groupObj.contents.push( block );
else {
groupObj = { root: element, contents: [ block ] };
listGroups.push( groupObj );
CKEDITOR.dom.element.setMarker( database, element, 'list_group_object', groupObj );
}
processedFlag = 1;
break;
}
}
if ( processedFlag )
continue;
// No list ancestor? Group by block limit, but don't mix contents from different ranges.
var root = blockLimit;
if ( root.getCustomData( 'list_group_object_' + index ) )
root.getCustomData( 'list_group_object_' + index ).contents.push( block );
else {
groupObj = { root: root, contents: [ block ] };
CKEDITOR.dom.element.setMarker( database, root, 'list_group_object_' + index, groupObj );
listGroups.push( groupObj );
}
}
}
// Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element.
// We either have to build lists or remove lists, for removing a list does not makes sense when we are looking
// at the group that's not rooted at lists. So we have three cases to handle.
var listsCreated = [];
while ( listGroups.length > 0 ) {
groupObj = listGroups.shift();
if ( this.state == CKEDITOR.TRISTATE_OFF ) {
if ( isEmptyList( groupObj ) ) {
continue;
} else if ( listNodeNames[ groupObj.root.getName() ] ) {
changeListType.call( this, editor, groupObj, database, listsCreated );
} else {
createList.call( this, editor, groupObj, listsCreated );
}
} else if ( this.state == CKEDITOR.TRISTATE_ON && listNodeNames[ groupObj.root.getName() ] && !isEmptyList( groupObj ) ) {
removeList.call( this, editor, groupObj, database );
}
}
// For all new lists created, merge into adjacent, same type lists.
for ( i = 0; i < listsCreated.length; i++ )
mergeListSiblings( listsCreated[ i ] );
// Clean up, restore selection and update toolbar button states.
CKEDITOR.dom.element.clearAllMarkers( database );
selection.selectBookmarks( bookmarks );
editor.focus();
function isEmptyList( groupObj ) {
// If list is without any li item, then ignore such element from transformation, because it throws errors in console (#2411, #2438).
return listNodeNames[ groupObj.root.getName() ] && !getChildCount( groupObj.root, [ CKEDITOR.NODE_COMMENT ] );
}
function getChildCount( element, excludeTypes ) {
return CKEDITOR.tools.array.filter( element.getChildren().toArray(), function( node ) {
return CKEDITOR.tools.array.indexOf( excludeTypes, node.type ) === -1;
} ).length;
}
function hasCommentsChildOnly( element ) {
var ret = true;
if ( element.getChildCount() === 0 ) {
return false;
}
element.forEach( function( node ) {
if ( node.type !== CKEDITOR.NODE_COMMENT ) {
ret = false;
return false;
}
}, null, true );
return ret;
}
},
refresh: function( editor, path ) {
var list = path.contains( listNodeNames, 1 ),
limit = path.blockLimit || path.root;
// 1. Only a single type of list activate.
// 2. Do not show list outside of block limit.
if ( list && limit.contains( list ) )
this.setState( list.is( this.type ) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
else
this.setState( CKEDITOR.TRISTATE_OFF );
}
};
// Merge list adjacent, of same type lists.
function mergeListSiblings( listNode ) {
function mergeSibling( rtl ) {
var sibling = listNode[ rtl ? 'getPrevious' : 'getNext' ]( nonEmpty );
if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT && sibling.is( listNode.getName() ) ) {
// Move children order by merge direction.(https://dev.ckeditor.com/ticket/3820)
mergeChildren( listNode, sibling, null, !rtl );
listNode.remove();
listNode = sibling;
}
}
mergeSibling();
mergeSibling( 1 );
}
// Check if node is block element that recieves text.
function isTextBlock( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && ( node.getName() in CKEDITOR.dtd.$block || node.getName() in CKEDITOR.dtd.$listItem ) && CKEDITOR.dtd[ node.getName() ][ '#' ];
}
// Join visually two block lines.
function joinNextLineToCursor( editor, cursor, nextCursor ) {
editor.fire( 'saveSnapshot' );
// Merge with previous block's content.
nextCursor.enlarge( CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS );
var frag = nextCursor.extractContents();
cursor.trim( false, true );
var bm = cursor.createBookmark();
// Kill original bogus;
var currentPath = new CKEDITOR.dom.elementPath( cursor.startContainer ),
pathBlock = currentPath.block,
currentBlock = currentPath.lastElement.getAscendant( 'li', 1 ) || pathBlock,
nextPath = new CKEDITOR.dom.elementPath( nextCursor.startContainer ),
nextLi = nextPath.contains( CKEDITOR.dtd.$listItem ),
nextList = nextPath.contains( CKEDITOR.dtd.$list ),
last;
// Remove bogus node the current block/pseudo block.
if ( pathBlock ) {
var bogus = pathBlock.getBogus();
bogus && bogus.remove();
}
else if ( nextList ) {
last = nextList.getPrevious( nonEmpty );
if ( last && blockBogus( last ) )
last.remove();
}
// Kill the tail br in extracted.
last = frag.getLast();
if ( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( 'br' ) )
last.remove();
// Insert fragment at the range position.
var nextNode = cursor.startContainer.getChild( cursor.startOffset );
if ( nextNode )
frag.insertBefore( nextNode );
else
cursor.startContainer.append( frag );
// Move the sub list nested in the next list item.
if ( nextLi ) {
var sublist = getSubList( nextLi );
if ( sublist ) {
// If next line is in the sub list of the current list item.
if ( currentBlock.contains( nextLi ) ) {
mergeChildren( sublist, nextLi.getParent(), nextLi );
sublist.remove();
}
// Migrate the sub list to current list item.
else {
currentBlock.append( sublist );
}
}
}
var nextBlock, parent;
// Remove any remaining zombies path blocks at the end after line merged.
while ( nextCursor.checkStartOfBlock() && nextCursor.checkEndOfBlock() ) {
nextPath = nextCursor.startPath();
nextBlock = nextPath.block;
// Abort when nothing to be removed (https://dev.ckeditor.com/ticket/10890).
if ( !nextBlock )
break;
// Check if also to remove empty list.
if ( nextBlock.is( 'li' ) ) {
parent = nextBlock.getParent();
if ( nextBlock.equals( parent.getLast( nonEmpty ) ) && nextBlock.equals( parent.getFirst( nonEmpty ) ) )
nextBlock = parent;
}
nextCursor.moveToPosition( nextBlock, CKEDITOR.POSITION_BEFORE_START );
nextBlock.remove();
}
// Check if need to further merge with the list resides after the merged block. (https://dev.ckeditor.com/ticket/9080)
var walkerRng = nextCursor.clone(), editable = editor.editable();
walkerRng.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END );
var walker = new CKEDITOR.dom.walker( walkerRng );
walker.evaluator = function( node ) {
return nonEmpty( node ) && !blockBogus( node );
};
var next = walker.next();
if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getName() in CKEDITOR.dtd.$list )
mergeListSiblings( next );
cursor.moveToBookmark( bm );
// Make fresh selection.
cursor.select();
editor.fire( 'saveSnapshot' );
}
function getSubList( li ) {
var last = li.getLast( nonEmpty );
return last && last.type == CKEDITOR.NODE_ELEMENT && last.getName() in listNodeNames ? last : null;
}
CKEDITOR.plugins.add( 'list', {
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
requires: 'indentlist',
init: function( editor ) {
if ( editor.blockless )
return;
// Register commands.
editor.addCommand( 'numberedlist', new listCommand( 'numberedlist', 'ol' ) );
editor.addCommand( 'bulletedlist', new listCommand( 'bulletedlist', 'ul' ) );
// Register the toolbar button.
if ( editor.ui.addButton ) {
editor.ui.addButton( 'NumberedList', {
label: editor.lang.list.numberedlist,
command: 'numberedlist',
directional: true,
toolbar: 'list,10'
} );
editor.ui.addButton( 'BulletedList', {
label: editor.lang.list.bulletedlist,
command: 'bulletedlist',
directional: true,
toolbar: 'list,20'
} );
}
// Handled backspace/del key to join list items. (https://dev.ckeditor.com/ticket/8248,https://dev.ckeditor.com/ticket/9080)
editor.on( 'key', function( evt ) {
// Use getKey directly in order to ignore modifiers.
// Justification: https://dev.ckeditor.com/ticket/11861#comment:13
var key = evt.data.domEvent.getKey(), li;
// DEl/BACKSPACE
if ( editor.mode == 'wysiwyg' && key in { 8: 1, 46: 1 } ) {
var sel = editor.getSelection(),
range = sel.getRanges()[ 0 ],
path = range && range.startPath();
if ( !range || !range.collapsed )
return;
var isBackspace = key == 8;
var editable = editor.editable();
var walker = new CKEDITOR.dom.walker( range.clone() );
walker.evaluator = function( node ) {
return nonEmpty( node ) && !blockBogus( node );
};
// Backspace/Del behavior at the start/end of table is handled in core.
walker.guard = function( node, isOut ) {
return !( isOut && node.type == CKEDITOR.NODE_ELEMENT && node.is( 'table' ) );
};
var cursor = range.clone();
if ( isBackspace ) {
var previous, joinWith;
// Join a sub list's first line, with the previous visual line in parent.
if (
( previous = path.contains( listNodeNames ) ) &&
range.checkBoundaryOfElement( previous, CKEDITOR.START ) &&
( previous = previous.getParent() ) && previous.is( 'li' ) &&
( previous = getSubList( previous ) )
) {
joinWith = previous;
previous = previous.getPrevious( nonEmpty );
// Place cursor before the nested list.
cursor.moveToPosition(
previous && blockBogus( previous ) ? previous : joinWith,
CKEDITOR.POSITION_BEFORE_START );
}
// Join any line following a list, with the last visual line of the list.
else {
walker.range.setStartAt( editable, CKEDITOR.POSITION_AFTER_START );
walker.range.setEnd( range.startContainer, range.startOffset );
previous = walker.previous();
if (
previous && previous.type == CKEDITOR.NODE_ELEMENT &&
( previous.getName() in listNodeNames ||
previous.is( 'li' ) )
) {
if ( !previous.is( 'li' ) ) {
walker.range.selectNodeContents( previous );
walker.reset();
walker.evaluator = isTextBlock;
previous = walker.previous();
}
joinWith = previous;
// Place cursor at the end of previous block.
cursor.moveToElementEditEnd( joinWith );
// And then just before end of closest block element (https://dev.ckeditor.com/ticket/12729).
cursor.moveToPosition( cursor.endPath().block, CKEDITOR.POSITION_BEFORE_END );
}
}
if ( joinWith ) {
joinNextLineToCursor( editor, cursor, range );
evt.cancel();
}
else {
var list = path.contains( listNodeNames );
// Backspace pressed at the start of list outdents the first list item. (https://dev.ckeditor.com/ticket/9129)
if ( list && range.checkBoundaryOfElement( list, CKEDITOR.START ) ) {
li = list.getFirst( nonEmpty );
if ( range.checkBoundaryOfElement( li, CKEDITOR.START ) ) {
previous = list.getPrevious( nonEmpty );
// Only if the list item contains a sub list, do nothing but
// simply move cursor backward one character.
if ( getSubList( li ) ) {
if ( previous ) {
range.moveToElementEditEnd( previous );
range.select();
}
evt.cancel();
}
else {
editor.execCommand( 'outdent' );
evt.cancel();
}
}
}
}
} else {
var next, nextLine;
li = path.contains( 'li' );
if ( li ) {
walker.range.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END );
var last = li.getLast( nonEmpty );
var block = last && isTextBlock( last ) ? last : li;
// Indicate cursor at the visual end of an list item.
var isAtEnd = 0;
next = walker.next();
// When list item contains a sub list.
if (
next && next.type == CKEDITOR.NODE_ELEMENT &&
next.getName() in listNodeNames &&
next.equals( last )
) {
isAtEnd = 1;
// Move to the first item in sub list.
next = walker.next();
}
// Right at the end of list item.
else if ( range.checkBoundaryOfElement( block, CKEDITOR.END ) ) {
isAtEnd = 2;
}
if ( isAtEnd && next ) {
// Put cursor range there.
nextLine = range.clone();
nextLine.moveToElementEditStart( next );
// https://dev.ckeditor.com/ticket/13409
// For the following case and similar
//
//
if ( isAtEnd == 1 ) {
// Move the cursor to if attached to "x" text node.
cursor.optimize();
// Abort if the range is attached directly in , like
//
//
if ( !cursor.startContainer.equals( li ) ) {
var node = cursor.startContainer,
farthestInlineAscendant;
// Find , which is farthest from but still inline element.
while ( node.is( CKEDITOR.dtd.$inline ) ) {
farthestInlineAscendant = node;
node = node.getParent();
}
// Move the range so it does not contain inline elements.
// It prevents from being included in .
//
//
//
// so instead of
//
//
//
// pressing DELETE produces
//
//
if ( farthestInlineAscendant ) {
cursor.moveToPosition( farthestInlineAscendant, CKEDITOR.POSITION_AFTER_END );
}
}
}
// Moving `cursor` and `next line` only when at the end literally (https://dev.ckeditor.com/ticket/12729).
if ( isAtEnd == 2 ) {
cursor.moveToPosition( cursor.endPath().block, CKEDITOR.POSITION_BEFORE_END );
// Next line might be text node not wrapped in block element.
if ( nextLine.endPath().block ) {
nextLine.moveToPosition( nextLine.endPath().block, CKEDITOR.POSITION_AFTER_START );
}
}
joinNextLineToCursor( editor, cursor, nextLine );
evt.cancel();
}
} else {
// Handle Del key pressed before the list.
walker.range.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END );
next = walker.next();
if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.is( listNodeNames ) ) {
// The start
next = next.getFirst( nonEmpty );
// Simply remove the current empty block, move cursor to the
// subsequent list.
if ( path.block && range.checkStartOfBlock() && range.checkEndOfBlock() ) {
path.block.remove();
range.moveToElementEditStart( next );
range.select();
evt.cancel();
}
// Preventing the default (merge behavior), but simply move
// the cursor one character forward if subsequent list item
// contains sub list.
else if ( getSubList( next ) ) {
range.moveToElementEditStart( next );
range.select();
evt.cancel();
}
// Merge the first list item with the current line.
else {
nextLine = range.clone();
nextLine.moveToElementEditStart( next );
joinNextLineToCursor( editor, cursor, nextLine );
evt.cancel();
}
}
}
}
// The backspace/del could potentially put cursor at a bad position,
// being it handled or not, check immediately the selection to have it fixed.
setTimeout( function() {
editor.selectionChange( 1 );
} );
}
} );
}
} );
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
CKEDITOR.plugins.add( 'menu', {
requires: 'floatpanel',
beforeInit: function( editor ) {
var groups = editor.config.menu_groups.split( ',' ),
groupsOrder = editor._.menuGroups = {},
menuItems = editor._.menuItems = {};
for ( var i = 0; i < groups.length; i++ )
groupsOrder[ groups[ i ] ] = i + 1;
/**
* Registers an item group to the editor context menu in order to make it
* possible to associate it with menu items later.
*
* @param {String} name Specify a group name.
* @param {Number} [order=100] Define the display sequence of this group
* inside the menu. A smaller value gets displayed first.
* @member CKEDITOR.editor
*/
editor.addMenuGroup = function( name, order ) {
groupsOrder[ name ] = order || 100;
};
/**
* Adds an item from the specified definition to the editor context menu.
*
* @method
* @param {String} name The menu item name.
* @param {Object} definition The menu item definition.
* @member CKEDITOR.editor
*/
editor.addMenuItem = function( name, definition ) {
if ( groupsOrder[ definition.group ] )
menuItems[ name ] = new CKEDITOR.menuItem( this, name, definition );
};
/**
* Adds one or more items from the specified definition object to the editor context menu.
*
* @method
* @param {Object} definitions Object where keys are used as itemName and corresponding values as definition for a {@link #addMenuItem} call.
* @member CKEDITOR.editor
*/
editor.addMenuItems = function( definitions ) {
for ( var itemName in definitions ) {
this.addMenuItem( itemName, definitions[ itemName ] );
}
};
/**
* Retrieves a particular menu item definition from the editor context menu.
*
* @method
* @param {String} name The name of the desired menu item.
* @returns {Object}
* @member CKEDITOR.editor
*/
editor.getMenuItem = function( name ) {
return menuItems[ name ];
};
/**
* Removes a particular menu item added before from the editor context menu.
*
* @since 3.6.1
* @method
* @param {String} name The name of the desired menu item.
* @member CKEDITOR.editor
*/
editor.removeMenuItem = function( name ) {
delete menuItems[ name ];
};
}
} );
( function() {
var menuItemSource = '';
var menuArrowSource = '';
var menuShortcutSource = '';
var menuItemTpl = CKEDITOR.addTemplate( 'menuItem', menuItemSource ),
menuArrowTpl = CKEDITOR.addTemplate( 'menuArrow', menuArrowSource ),
menuShortcutTpl = CKEDITOR.addTemplate( 'menuShortcut', menuShortcutSource );
/**
* @class
* @todo
*/
CKEDITOR.menu = CKEDITOR.tools.createClass( {
/**
* @constructor
*/
$: function( editor, definition ) {
definition = this._.definition = definition || {};
this.id = CKEDITOR.tools.getNextId();
this.editor = editor;
this.items = [];
this._.listeners = [];
this._.level = definition.level || 1;
var panelDefinition = CKEDITOR.tools.extend( {}, definition.panel, {
css: [ CKEDITOR.skin.getPath( 'editor' ) ],
level: this._.level - 1,
block: {}
} );
var attrs = panelDefinition.block.attributes = ( panelDefinition.attributes || {} );
// Provide default role of 'menu'.
!attrs.role && ( attrs.role = 'menu' );
this._.panelDefinition = panelDefinition;
},
_: {
onShow: function() {
var selection = this.editor.getSelection(),
start = selection && selection.getStartElement(),
path = this.editor.elementPath(),
listeners = this._.listeners;
this.removeAll();
// Call all listeners, filling the list of items to be displayed.
for ( var i = 0; i < listeners.length; i++ ) {
var listenerItems = listeners[ i ]( start, selection, path );
if ( listenerItems ) {
for ( var itemName in listenerItems ) {
var item = this.editor.getMenuItem( itemName );
if ( item && ( !item.command || this.editor.getCommand( item.command ).state ) ) {
item.state = listenerItems[ itemName ];
this.add( item );
}
}
}
}
},
onClick: function( item ) {
this.hide();
if ( item.onClick )
item.onClick();
else if ( item.command )
this.editor.execCommand( item.command );
},
onEscape: function( keystroke ) {
var parent = this.parent;
// 1. If it's sub-menu, close it, with focus restored on this.
// 2. In case of a top-menu, close it, with focus returned to page.
if ( parent )
parent._.panel.hideChild( 1 );
else if ( keystroke == 27 )
this.hide( 1 );
return false;
},
onHide: function() {
this.onHide && this.onHide();
},
showSubMenu: function( index ) {
var menu = this._.subMenu,
item = this.items[ index ],
subItemDefs = item.getItems && item.getItems();
// If this item has no subitems, we just hide the submenu, if
// available, and return back.
if ( !subItemDefs ) {
// Hide sub menu with focus returned.
this._.panel.hideChild( 1 );
return;
}
// Create the submenu, if not available, or clean the existing
// one.
if ( menu )
menu.removeAll();
else {
menu = this._.subMenu = new CKEDITOR.menu( this.editor, CKEDITOR.tools.extend( {}, this._.definition, { level: this._.level + 1 }, true ) );
menu.parent = this;
menu._.onClick = CKEDITOR.tools.bind( this._.onClick, this );
}
// Add all submenu items to the menu.
for ( var subItemName in subItemDefs ) {
var subItem = this.editor.getMenuItem( subItemName );
if ( subItem ) {
subItem.state = subItemDefs[ subItemName ];
menu.add( subItem );
}
}
// Get the element representing the current item.
var element = this._.panel.getBlock( this.id ).element.getDocument().getById( this.id + String( index ) );
// Show the submenu.
// This timeout is needed to give time for the sub-menu get
// focus when JAWS is running. (https://dev.ckeditor.com/ticket/9844)
setTimeout( function() {
menu.show( element, 2 );
}, 0 );
}
},
proto: {
/**
* Adds an item.
*
* @param item
*/
add: function( item ) {
// Later we may sort the items, but Array#sort is not stable in
// some browsers, here we're forcing the original sequence with
// 'order' attribute if it hasn't been assigned. (https://dev.ckeditor.com/ticket/3868)
if ( !item.order )
item.order = this.items.length;
this.items.push( item );
},
/**
* Removes all items.
*/
removeAll: function() {
this.items = [];
},
/**
* Shows the menu in given location.
*
* @param {CKEDITOR.dom.element} offsetParent
* @param {Number} [corner]
* @param {Number} [offsetX]
* @param {Number} [offsetY]
*/
show: function( offsetParent, corner, offsetX, offsetY ) {
// Not for sub menu.
if ( !this.parent ) {
this._.onShow();
// Don't menu with zero items.
if ( !this.items.length )
return;
}
corner = corner || ( this.editor.lang.dir == 'rtl' ? 2 : 1 );
var items = this.items,
editor = this.editor,
panel = this._.panel,
element = this._.element;
// Create the floating panel for this menu.
if ( !panel ) {
panel = this._.panel = new CKEDITOR.ui.floatPanel( this.editor, CKEDITOR.document.getBody(), this._.panelDefinition, this._.level );
panel.onEscape = CKEDITOR.tools.bind( function( keystroke ) {
if ( this._.onEscape( keystroke ) === false )
return false;
}, this );
panel.onShow = function() {
// Menu need CSS resets, compensate class name.
var holder = panel._.panel.getHolderElement();
holder.getParent().addClass( 'cke' ).addClass( 'cke_reset_all' );
};
panel.onHide = CKEDITOR.tools.bind( function() {
this._.onHide && this._.onHide();
}, this );
// Create an autosize block inside the panel.
var block = panel.addBlock( this.id, this._.panelDefinition.block );
block.autoSize = true;
var keys = block.keys;
keys[ 40 ] = 'next'; // ARROW-DOWN
keys[ 9 ] = 'next'; // TAB
keys[ 38 ] = 'prev'; // ARROW-UP
keys[ CKEDITOR.SHIFT + 9 ] = 'prev'; // SHIFT + TAB
keys[ ( editor.lang.dir == 'rtl' ? 37 : 39 ) ] = CKEDITOR.env.ie ? 'mouseup' : 'click'; // ARROW-RIGHT/ARROW-LEFT(rtl)
keys[ 32 ] = CKEDITOR.env.ie ? 'mouseup' : 'click'; // SPACE
CKEDITOR.env.ie && ( keys[ 13 ] = 'mouseup' ); // Manage ENTER, since onclick is blocked in IE (https://dev.ckeditor.com/ticket/8041).
element = this._.element = block.element;
var elementDoc = element.getDocument();
elementDoc.getBody().setStyle( 'overflow', 'hidden' );
elementDoc.getElementsByTag( 'html' ).getItem( 0 ).setStyle( 'overflow', 'hidden' );
this._.itemOverFn = CKEDITOR.tools.addFunction( function( index ) {
clearTimeout( this._.showSubTimeout );
this._.showSubTimeout = CKEDITOR.tools.setTimeout( this._.showSubMenu, editor.config.menu_subMenuDelay || 400, this, [ index ] );
}, this );
this._.itemOutFn = CKEDITOR.tools.addFunction( function() {
clearTimeout( this._.showSubTimeout );
}, this );
this._.itemClickFn = CKEDITOR.tools.addFunction( function( index ) {
var item = this.items[ index ];
if ( item.state == CKEDITOR.TRISTATE_DISABLED ) {
this.hide( 1 );
return;
}
if ( item.getItems )
this._.showSubMenu( index );
else
this._.onClick( item );
}, this );
}
// Put the items in the right order.
sortItems( items );
// Apply the editor mixed direction status to menu.
var path = editor.elementPath(),
mixedDirCls = ( path && path.direction() != editor.lang.dir ) ? ' cke_mixed_dir_content' : '';
// Build the HTML that composes the menu and its items.
var output = [ '' );
// Inject the HTML inside the panel.
element.setHtml( output.join( '' ) );
CKEDITOR.ui.fire( 'ready', this );
// Show the panel.
if ( this.parent ) {
this.parent._.panel.showAsChild( panel, this.id, offsetParent, corner, offsetX, offsetY );
} else {
panel.showBlock( this.id, offsetParent, corner, offsetX, offsetY );
}
var data = [ panel ];
editor.fire( 'menuShow', data );
},
/**
* Adds a callback executed on opening the menu. Items
* returned by that callback are added to the menu.
*
* @param {Function} listenerFn
* @param {CKEDITOR.dom.element} listenerFn.startElement The selection start anchor element.
* @param {CKEDITOR.dom.selection} listenerFn.selection The current selection.
* @param {CKEDITOR.dom.elementPath} listenerFn.path The current elements path.
* @param listenerFn.return Object (`commandName` => `state`) of items that should be added to the menu.
*/
addListener: function( listenerFn ) {
this._.listeners.push( listenerFn );
},
/**
* Hides the menu.
*
* @param {Boolean} [returnFocus]
*/
hide: function( returnFocus ) {
this._.onHide && this._.onHide();
this._.panel && this._.panel.hide( returnFocus );
},
/**
* Finds the menu item corresponding to a given command.
*
* **Notice**: Keep in mind that the menu is re-rendered on each opening, so caching items (especially DOM elements)
* may not work. Also executing this method when the menu is not visible may give unexpected results as the
* items may not be rendered.
*
* @since 4.9.0
* @param {String} commandName
* @returns {Object/null} return An object containing a given item. If the item was not found, `null` is returned.
* @returns {CKEDITOR.menuItem} return.item The item definition.
* @returns {CKEDITOR.dom.element} return.element The rendered element representing the item in the menu.
*/
findItemByCommandName: function( commandName ) {
var commands = CKEDITOR.tools.array.filter( this.items, function( item ) {
return commandName === item.command;
} );
if ( commands.length ) {
var commandItem = commands[ 0 ];
return {
item: commandItem,
element: this._.element.findOne( '.' + commandItem.className )
};
}
return null;
}
}
} );
function sortItems( items ) {
items.sort( function( itemA, itemB ) {
if ( itemA.group < itemB.group )
return -1;
else if ( itemA.group > itemB.group )
return 1;
return itemA.order < itemB.order ? -1 : itemA.order > itemB.order ? 1 : 0;
} );
}
/**
* @class
* @todo
*/
CKEDITOR.menuItem = CKEDITOR.tools.createClass( {
$: function( editor, name, definition ) {
CKEDITOR.tools.extend( this, definition,
// Defaults
{
order: 0,
className: 'cke_menubutton__' + name
} );
// Transform the group name into its order number.
this.group = editor._.menuGroups[ this.group ];
this.editor = editor;
this.name = name;
},
proto: {
render: function( menu, index, output ) {
var id = menu.id + String( index ),
state = ( typeof this.state == 'undefined' ) ? CKEDITOR.TRISTATE_OFF : this.state,
ariaChecked = '',
editor = this.editor,
keystroke,
command,
shortcut;
var stateName = state == CKEDITOR.TRISTATE_ON ? 'on' : state == CKEDITOR.TRISTATE_DISABLED ? 'disabled' : 'off';
if ( this.role in { menuitemcheckbox: 1, menuitemradio: 1 } )
ariaChecked = ' aria-checked="' + ( state == CKEDITOR.TRISTATE_ON ? 'true' : 'false' ) + '"';
var hasSubMenu = this.getItems;
// ltr: BLACK LEFT-POINTING POINTER
// rtl: BLACK RIGHT-POINTING POINTER
var arrowLabel = '' + ( this.editor.lang.dir == 'rtl' ? '9668' : '9658' ) + ';';
var iconName = this.name;
if ( this.icon && !( /\./ ).test( this.icon ) )
iconName = this.icon;
if ( this.command ) {
command = editor.getCommand( this.command );
keystroke = editor.getCommandKeystroke( command );
if ( keystroke ) {
shortcut = CKEDITOR.tools.keystrokeToString( editor.lang.common.keyboard, keystroke );
}
}
var params = {
id: id,
name: this.name,
iconName: iconName,
label: this.label,
cls: this.className || '',
state: stateName,
hasPopup: hasSubMenu ? 'true' : 'false',
disabled: state == CKEDITOR.TRISTATE_DISABLED,
title: this.label + ( shortcut ? ' (' + shortcut.display + ')' : '' ),
ariaShortcut: shortcut ? editor.lang.common.keyboardShortcut + ' ' + shortcut.aria : '',
href: 'javascript:void(\'' + ( this.label || '' ).replace( "'" + '' ) + '\')', // jshint ignore:line
hoverFn: menu._.itemOverFn,
moveOutFn: menu._.itemOutFn,
clickFn: menu._.itemClickFn,
index: index,
iconStyle: CKEDITOR.skin.getIconStyle( iconName, ( this.editor.lang.dir == 'rtl' ), iconName == this.icon ? null : this.icon, this.iconOffset ),
shortcutHtml: shortcut ? menuShortcutTpl.output( { shortcut: shortcut.display } ) : '',
arrowHtml: hasSubMenu ? menuArrowTpl.output( { label: arrowLabel } ) : '',
role: this.role ? this.role : 'menuitem',
ariaChecked: ariaChecked
};
menuItemTpl.output( params, output );
}
}
} );
} )();
/**
* The amount of time, in milliseconds, the editor waits before displaying submenu
* options when moving the mouse over options that contain submenus, like the
* "Cell Properties" entry for tables.
*
* // Remove the submenu delay.
* config.menu_subMenuDelay = 0;
*
* @cfg {Number} [menu_subMenuDelay=400]
* @member CKEDITOR.config
*/
/**
* Fired when a menu is shown.
*
* @event menuShow
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param {CKEDITOR.ui.panel[]} data
*/
/**
* A comma separated list of items group names to be displayed in the context
* menu. The order of items will reflect the order specified in this list if
* no priority was defined in the groups.
*
* config.menu_groups = 'clipboard,table,anchor,link,image';
*
* @cfg {String} [menu_groups=see source]
* @member CKEDITOR.config
*/
CKEDITOR.config.menu_groups = 'clipboard,' +
'form,' +
'tablecell,tablecellproperties,tablerow,tablecolumn,table,' +
'anchor,link,image,flash,' +
'checkbox,radio,textfield,hiddenfield,imagebutton,button,select,textarea,div';
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
CKEDITOR.plugins.add( 'contextmenu', {
requires: 'menu',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
// Make sure the base class (CKEDITOR.menu) is loaded before it (https://dev.ckeditor.com/ticket/3318).
onLoad: function() {
/**
* Class replacing the non-configurable native context menu with a configurable CKEditor's equivalent.
*
* @class
* @extends CKEDITOR.menu
*/
CKEDITOR.plugins.contextMenu = CKEDITOR.tools.createClass( {
base: CKEDITOR.menu,
/**
* Creates the CKEDITOR.plugins.contextMenu class instance.
*
* @constructor
* @param {CKEDITOR.editor} editor
*/
$: function( editor ) {
this.base.call( this, editor, {
panel: {
// Allow adding custom CSS (#2202).
css: editor.config.contextmenu_contentsCss,
className: 'cke_menu_panel',
attributes: {
'aria-label': editor.lang.contextmenu.options
}
}
} );
},
proto: {
/**
* Starts watching on native context menu triggers (Option key, right click) on the given element.
*
* @param {CKEDITOR.dom.element} element
* @param {Boolean} [nativeContextMenuOnCtrl] Whether to open native context menu if the
* Ctrl key is held on opening the context menu. See {@link CKEDITOR.config#browserContextMenuOnCtrl}.
*/
addTarget: function( element, nativeContextMenuOnCtrl ) {
var holdCtrlKey,
keystrokeActive;
element.on( 'contextmenu', function( event ) {
var domEvent = event.data,
isCtrlKeyDown =
// Safari on Windows always show 'ctrlKey' as true in 'contextmenu' event,
// which make this property unreliable. (https://dev.ckeditor.com/ticket/4826)
( CKEDITOR.env.webkit ? holdCtrlKey : ( CKEDITOR.env.mac ? domEvent.$.metaKey : domEvent.$.ctrlKey ) );
if ( nativeContextMenuOnCtrl && isCtrlKeyDown ) {
return;
}
// Cancel the browser context menu.
domEvent.preventDefault();
// Do not react to this event, as it might open context menu in wrong position (#2548).
if ( keystrokeActive ) {
return;
}
// Fix selection when non-editable element in Webkit/Blink (Mac) (https://dev.ckeditor.com/ticket/11306).
if ( CKEDITOR.env.mac && CKEDITOR.env.webkit ) {
var editor = this.editor,
contentEditableParent = new CKEDITOR.dom.elementPath( domEvent.getTarget(), editor.editable() ).contains( function( el ) {
// Return when non-editable or nested editable element is found.
return el.hasAttribute( 'contenteditable' );
}, true ); // Exclude editor's editable.
// Fake selection for non-editables only (to exclude nested editables).
if ( contentEditableParent && contentEditableParent.getAttribute( 'contenteditable' ) == 'false' ) {
editor.getSelection().fake( contentEditableParent );
}
}
var doc = domEvent.getTarget().getDocument(),
offsetParent = domEvent.getTarget().getDocument().getDocumentElement(),
fromFrame = !doc.equals( CKEDITOR.document ),
scroll = doc.getWindow().getScrollPosition(),
offsetX = fromFrame ? domEvent.$.clientX : domEvent.$.pageX || scroll.x + domEvent.$.clientX,
offsetY = fromFrame ? domEvent.$.clientY : domEvent.$.pageY || scroll.y + domEvent.$.clientY;
CKEDITOR.tools.setTimeout( function() {
this.open( offsetParent, null, offsetX, offsetY );
// IE needs a short while to allow selection change before opening menu. (https://dev.ckeditor.com/ticket/7908)
}, CKEDITOR.env.ie ? 200 : 0, this );
}, this );
if ( CKEDITOR.env.webkit ) {
var onKeyDown = function( event ) {
holdCtrlKey = CKEDITOR.env.mac ? event.data.$.metaKey : event.data.$.ctrlKey;
},
resetOnKeyUp = function() {
holdCtrlKey = 0;
};
element.on( 'keydown', onKeyDown );
element.on( 'keyup', resetOnKeyUp );
element.on( 'contextmenu', resetOnKeyUp );
}
// Block subsequent contextmenu event, when Shift + F10 is pressed (#2548).
if ( CKEDITOR.env.gecko && !CKEDITOR.env.mac ) {
element.on( 'keydown', function( evt ) {
if ( evt.data.$.shiftKey && evt.data.$.keyCode === 121 ) {
keystrokeActive = true;
}
}, null, null, 0 );
element.on( 'keyup', resetKeystrokeState );
element.on( 'contextmenu', resetKeystrokeState );
}
function resetKeystrokeState() {
keystrokeActive = false;
}
},
/**
* Opens the context menu in the given location. See the {@link CKEDITOR.menu#show} method.
*
* @param {CKEDITOR.dom.element} offsetParent
* @param {Number} [corner]
* @param {Number} [offsetX]
* @param {Number} [offsetY]
*/
open: function( offsetParent, corner, offsetX, offsetY ) {
// Do not open context menu if it's disabled or there is no selection in the editor (#1181).
if ( this.editor.config.enableContextMenu === false ||
this.editor.getSelection().getType() === CKEDITOR.SELECTION_NONE ) {
return;
}
this.editor.focus();
offsetParent = offsetParent || CKEDITOR.document.getDocumentElement();
// https://dev.ckeditor.com/ticket/9362: Force selection check to update commands' states in the new context.
this.editor.selectionChange( 1 );
this.show( offsetParent, corner, offsetX, offsetY );
}
}
} );
},
beforeInit: function( editor ) {
/**
* @readonly
* @property {CKEDITOR.plugins.contextMenu} contextMenu
* @member CKEDITOR.editor
*/
var contextMenu = editor.contextMenu = new CKEDITOR.plugins.contextMenu( editor );
editor.on( 'contentDom', function() {
contextMenu.addTarget( editor.editable(), editor.config.browserContextMenuOnCtrl !== false );
} );
editor.addCommand( 'contextMenu', {
exec: function( editor ) {
var offsetX = 0,
offsetY = 0,
ranges = editor.getSelection().getRanges(),
rects,
rect;
// When opening context menu via keystroke there is no offsetX and Y passed (#1451).
rects = ranges[ ranges.length - 1 ].getClientRects( editor.editable().isInline() );
rect = rects[ rects.length - 1 ];
if ( rect ) {
offsetX = rect[ editor.lang.dir === 'rtl' ? 'left' : 'right' ];
offsetY = rect.bottom;
}
editor.contextMenu.open( editor.document.getBody().getParent(), null, offsetX, offsetY );
}
} );
editor.setKeystroke( CKEDITOR.SHIFT + 121 /*F10*/, 'contextMenu' );
editor.setKeystroke( CKEDITOR.CTRL + CKEDITOR.SHIFT + 121 /*F10*/, 'contextMenu' );
}
} );
/**
* Whether to show the browser native context menu when the Ctrl or
* Meta (Mac) key is pressed on opening the context menu with the
* right mouse button click or the Menu key.
*
* ```javascript
* config.browserContextMenuOnCtrl = false;
* ```
*
* @since 3.0.2
* @cfg {Boolean} [browserContextMenuOnCtrl=true]
* @member CKEDITOR.config
*/
/**
* Whether to enable the context menu. Regardless of the setting the [Context Menu](https://ckeditor.com/cke4/addon/contextmenu)
* plugin is still loaded.
*
* ```javascript
* config.enableContextMenu = false;
* ```
*
* @since 4.7.0
* @cfg {Boolean} [enableContextMenu=true]
* @member CKEDITOR.config
*/
/**
* The CSS file(s) to be used to apply the style to the context menu content.
*
* ```javascript
* config.contextmenu_contentsCss = '/css/myfile.css';
* config.contextmenu_contentsCss = [ '/css/myfile.css', '/css/anotherfile.css' ];
* ```
*
* @since 4.11.0
* @cfg {String/String[]} [contextmenu_contentsCss=CKEDITOR.skin.getPath( 'editor' )]
* @member CKEDITOR.config
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
( function() {
CKEDITOR.plugins.liststyle = {
requires: 'dialog,contextmenu',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
if ( editor.blockless )
return;
var def, cmd;
def = new CKEDITOR.dialogCommand( 'numberedListStyle', {
requiredContent: 'ol',
allowedContent: 'ol{list-style-type}[start]; li{list-style-type}[value]',
contentTransformations: [
[ 'ol: listTypeToStyle' ]
]
} );
cmd = editor.addCommand( 'numberedListStyle', def );
editor.addFeature( cmd );
CKEDITOR.dialog.add( 'numberedListStyle', this.path + 'dialogs/liststyle.js' );
def = new CKEDITOR.dialogCommand( 'bulletedListStyle', {
requiredContent: 'ul',
allowedContent: 'ul{list-style-type}',
contentTransformations: [
[ 'ul: listTypeToStyle' ]
]
} );
cmd = editor.addCommand( 'bulletedListStyle', def );
editor.addFeature( cmd );
CKEDITOR.dialog.add( 'bulletedListStyle', this.path + 'dialogs/liststyle.js' );
//Register map group;
editor.addMenuGroup( 'list', 108 );
editor.addMenuItems( {
numberedlist: {
label: editor.lang.liststyle.numberedTitle,
group: 'list',
command: 'numberedListStyle'
},
bulletedlist: {
label: editor.lang.liststyle.bulletedTitle,
group: 'list',
command: 'bulletedListStyle'
}
} );
editor.contextMenu.addListener( function( element ) {
if ( !element || element.isReadOnly() )
return null;
while ( element ) {
var name = element.getName();
if ( name == 'ol' )
return { numberedlist: CKEDITOR.TRISTATE_OFF };
else if ( name == 'ul' )
return { bulletedlist: CKEDITOR.TRISTATE_OFF };
element = element.getParent();
}
return null;
} );
}
};
CKEDITOR.plugins.add( 'liststyle', CKEDITOR.plugins.liststyle );
} )();
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview The [Magic Line](https://ckeditor.com/cke4/addon/magicline) plugin that makes it easier to access some document areas that
* are difficult to focus.
*/
'use strict';
( function() {
CKEDITOR.plugins.add( 'magicline', {
init: initPlugin
} );
// Activates the box inside of an editor.
function initPlugin( editor ) {
// Configurables
var config = editor.config,
triggerOffset = config.magicline_triggerOffset || 30,
enterMode = config.enterMode,
that = {
// Global stuff is being initialized here.
editor: editor,
enterMode: enterMode,
triggerOffset: triggerOffset,
holdDistance: 0 | triggerOffset * ( config.magicline_holdDistance || 0.5 ),
boxColor: config.magicline_color || '#ff0000',
rtl: config.contentsLangDirection == 'rtl',
tabuList: [ 'data-cke-hidden-sel' ].concat( config.magicline_tabuList || [] ),
triggers: config.magicline_everywhere ? DTD_BLOCK : { table: 1, hr: 1, div: 1, ul: 1, ol: 1, dl: 1, form: 1, blockquote: 1 }
},
scrollTimeout, checkMouseTimeoutPending, checkMouseTimer;
// Simple irrelevant elements filter.
that.isRelevant = function( node ) {
return isHtml( node ) && // -> Node must be an existing HTML element.
!isLine( that, node ) && // -> Node can be neither the box nor its child.
!isFlowBreaker( node ); // -> Node can be neither floated nor positioned nor aligned.
};
editor.on( 'contentDom', addListeners, this );
function addListeners() {
var editable = editor.editable(),
doc = editor.document,
win = editor.window;
// Global stuff is being initialized here.
extend( that, {
editable: editable,
inInlineMode: editable.isInline(),
doc: doc,
win: win,
hotNode: null
}, true );
// This is the boundary of the editor. For inline the boundary is editable itself.
// For classic (`iframe`-based) editor, the HTML element is a real boundary.
that.boundary = that.inInlineMode ? that.editable : that.doc.getDocumentElement();
// Enabling the box inside of inline editable is pointless.
// There's no need to access spaces inside paragraphs, links, spans, etc.
if ( editable.is( dtd.$inline ) )
return;
// Handle in-line editing by setting appropriate position.
// If current position is static, make it relative and clear top/left coordinates.
if ( that.inInlineMode && !isPositioned( editable ) ) {
editable.setStyles( {
position: 'relative',
top: null,
left: null
} );
}
// Enable the box. Let it produce children elements, initialize
// event handlers and own methods.
initLine.call( this, that );
// Get view dimensions and scroll positions.
// At this stage (before any checkMouse call) it is used mostly
// by tests. Nevertheless it a crucial thing.
updateWindowSize( that );
// Remove the box before an undo image is created.
// This is important. If we didn't do that, the *undo thing* would revert the box into an editor.
// Thanks to that, undo doesn't even know about the existence of the box.
editable.attachListener( editor, 'beforeUndoImage', function() {
that.line.detach();
} );
// Removes the box HTML from editor data string if getData is called.
// Thanks to that, an editor never yields data polluted by the box.
// Listen with very high priority, so line will be removed before other
// listeners will see it.
editable.attachListener( editor, 'beforeGetData', function() {
// If the box is in editable, remove it.
if ( that.line.wrap.getParent() ) {
that.line.detach();
// Restore line in the last listener for 'getData'.
editor.once( 'getData', function() {
that.line.attach();
}, null, null, 1000 );
}
}, null, null, 0 );
// Hide the box on mouseout if mouse leaves document.
editable.attachListener( that.inInlineMode ? doc : doc.getWindow().getFrame(), 'mouseout', function( event ) {
if ( editor.mode != 'wysiwyg' )
return;
// Check for inline-mode editor. If so, check mouse position
// and remove the box if mouse outside of an editor.
if ( that.inInlineMode ) {
var mouse = {
x: event.data.$.clientX,
y: event.data.$.clientY
};
updateWindowSize( that );
updateEditableSize( that, true );
var size = that.view.editable,
scroll = that.view.scroll;
// If outside of an editor...
if ( !inBetween( mouse.x, size.left - scroll.x, size.right - scroll.x ) || !inBetween( mouse.y, size.top - scroll.y, size.bottom - scroll.y ) ) {
clearTimeout( checkMouseTimer );
checkMouseTimer = null;
that.line.detach();
}
}
else {
clearTimeout( checkMouseTimer );
checkMouseTimer = null;
that.line.detach();
}
} );
// This one deactivates hidden mode of an editor which
// prevents the box from being shown.
editable.attachListener( editable, 'keyup', function() {
that.hiddenMode = 0;
} );
editable.attachListener( editable, 'keydown', function( event ) {
if ( editor.mode != 'wysiwyg' )
return;
var keyStroke = event.data.getKeystroke();
switch ( keyStroke ) {
// Shift pressed
case 2228240: // IE
case 16:
that.hiddenMode = 1;
that.line.detach();
}
} );
// This method ensures that checkMouse aren't executed
// in parallel and no more frequently than specified in timeout function.
// In classic (`iframe`-based) editor, document is used as a trigger, to provide magicline
// functionality when mouse is below the body (short content, short body).
editable.attachListener( that.inInlineMode ? editable : doc, 'mousemove', function( event ) {
checkMouseTimeoutPending = true;
if ( editor.mode != 'wysiwyg' || editor.readOnly || checkMouseTimer )
return;
// IE<9 requires this event-driven object to be created
// outside of the setTimeout statement.
// Otherwise it loses the event object with its properties.
var mouse = {
x: event.data.$.clientX,
y: event.data.$.clientY
};
checkMouseTimer = setTimeout( function() {
checkMouse( mouse );
}, 30 ); // balances performance and accessibility
} );
// This one removes box on scroll event.
// It is to avoid box displacement.
editable.attachListener( win, 'scroll', function() {
if ( editor.mode != 'wysiwyg' )
return;
that.line.detach();
// To figure this out just look at the mouseup
// event handler below.
if ( env.webkit ) {
that.hiddenMode = 1;
clearTimeout( scrollTimeout );
scrollTimeout = setTimeout( function() {
// Don't leave hidden mode until mouse remains pressed and
// scroll is being used, i.e. when dragging something.
if ( !that.mouseDown )
that.hiddenMode = 0;
}, 50 );
}
} );
// Those event handlers remove the box on mousedown
// and don't reveal it until the mouse is released.
// It is to prevent box insertion e.g. while scrolling
// (w/ scrollbar), selecting and so on.
editable.attachListener( env_ie8 ? doc : win, 'mousedown', function() {
if ( editor.mode != 'wysiwyg' )
return;
that.line.detach();
that.hiddenMode = 1;
that.mouseDown = 1;
} );
// Google Chrome doesn't trigger this on the scrollbar (since 2009...)
// so it is totally useless to check for scroll finish
// see: http://code.google.com/p/chromium/issues/detail?id=14204
editable.attachListener( env_ie8 ? doc : win, 'mouseup', function() {
that.hiddenMode = 0;
that.mouseDown = 0;
} );
// Editor commands for accessing difficult focus spaces.
editor.addCommand( 'accessPreviousSpace', accessFocusSpaceCmd( that ) );
editor.addCommand( 'accessNextSpace', accessFocusSpaceCmd( that, true ) );
editor.setKeystroke( [
[ config.magicline_keystrokePrevious, 'accessPreviousSpace' ],
[ config.magicline_keystrokeNext, 'accessNextSpace' ]
] );
// Revert magicline hot node on undo/redo.
editor.on( 'loadSnapshot', function() {
var elements, element, i;
for ( var t in { p: 1, br: 1, div: 1 } ) {
// document.find is not available in QM (https://dev.ckeditor.com/ticket/11149).
elements = editor.document.getElementsByTag( t );
for ( i = elements.count(); i--; ) {
if ( ( element = elements.getItem( i ) ).data( 'cke-magicline-hot' ) ) {
// Restore hotNode
that.hotNode = element;
// Restore last access direction
that.lastCmdDirection = element.data( 'cke-magicline-dir' ) === 'true' ? true : false;
return;
}
}
}
} );
// This method handles mousemove mouse for box toggling.
// It uses mouse position to determine underlying element, then
// it tries to use different trigger type in order to place the box
// in correct place. The following procedure is executed periodically.
function checkMouse( mouse ) {
that.mouse = mouse;
that.trigger = null;
checkMouseTimer = null;
updateWindowSize( that );
if (
checkMouseTimeoutPending && // There must be an event pending.
!that.hiddenMode && // Can't be in hidden mode.
editor.focusManager.hasFocus && // Editor must have focus.
!that.line.mouseNear() && // Mouse pointer can't be close to the box.
( that.element = elementFromMouse( that, true ) ) // There must be valid element.
) {
// If trigger exists, and trigger is correct -> show the box.
// Don't show the line if trigger is a descendant of some tabu-list element.
if ( ( that.trigger = triggerEditable( that ) || triggerEdge( that ) || triggerExpand( that ) ) &&
!isInTabu( that, that.trigger.upper || that.trigger.lower ) ) {
that.line.attach().place();
}
// Otherwise remove the box
else {
that.trigger = null;
that.line.detach();
}
checkMouseTimeoutPending = false;
}
}
// This one allows testing and debugging. It reveals some
// inner methods to the world.
editor._.magiclineBackdoor = {
accessFocusSpace: accessFocusSpace,
boxTrigger: boxTrigger,
isLine: isLine,
getAscendantTrigger: getAscendantTrigger,
getNonEmptyNeighbour: getNonEmptyNeighbour,
getSize: getSize,
that: that,
triggerEdge: triggerEdge,
triggerEditable: triggerEditable,
triggerExpand: triggerExpand
};
}
}
// Some shorthands for common methods to save bytes
var extend = CKEDITOR.tools.extend,
newElement = CKEDITOR.dom.element,
newElementFromHtml = newElement.createFromHtml,
env = CKEDITOR.env,
env_ie8 = CKEDITOR.env.ie && CKEDITOR.env.version < 9,
dtd = CKEDITOR.dtd,
// Global object associating enter modes with elements.
enterElements = {},
// Constant values, types and so on.
EDGE_TOP = 128,
EDGE_BOTTOM = 64,
EDGE_MIDDLE = 32,
TYPE_EDGE = 16,
TYPE_EXPAND = 8,
LOOK_TOP = 4,
LOOK_BOTTOM = 2,
LOOK_NORMAL = 1,
WHITE_SPACE = '\u00A0',
DTD_LISTITEM = dtd.$listItem,
DTD_TABLECONTENT = dtd.$tableContent,
DTD_NONACCESSIBLE = extend( {}, dtd.$nonEditable, dtd.$empty ),
DTD_BLOCK = dtd.$block,
// Minimum time that must elapse between two update*Size calls.
// It prevents constant getComuptedStyle calls and improves performance.
CACHE_TIME = 100,
// Shared CSS stuff for box elements
CSS_COMMON = 'width:0px;height:0px;padding:0px;margin:0px;display:block;' + 'z-index:9999;color:#fff;position:absolute;font-size: 0px;line-height:0px;',
CSS_TRIANGLE = CSS_COMMON + 'border-color:transparent;display:block;border-style:solid;',
TRIANGLE_HTML = '' + WHITE_SPACE + ' ';
enterElements[ CKEDITOR.ENTER_BR ] = 'br';
enterElements[ CKEDITOR.ENTER_P ] = 'p';
enterElements[ CKEDITOR.ENTER_DIV ] = 'div';
function areSiblings( that, upper, lower ) {
return isHtml( upper ) && isHtml( lower ) && lower.equals( upper.getNext( function( node ) {
return !( isEmptyTextNode( node ) || isComment( node ) || isFlowBreaker( node ) );
} ) );
}
// boxTrigger is an abstract type which describes
// the relationship between elements that may result
// in showing the box.
//
// The following type is used by numerous methods
// to share information about the hypothetical box placement
// and look by referring to boxTrigger properties.
function boxTrigger( triggerSetup ) {
this.upper = triggerSetup[ 0 ];
this.lower = triggerSetup[ 1 ];
this.set.apply( this, triggerSetup.slice( 2 ) );
}
boxTrigger.prototype = {
set: function( edge, type, look ) {
this.properties = edge + type + ( look || LOOK_NORMAL );
return this;
},
is: function( property ) {
return ( this.properties & property ) == property;
}
};
var elementFromMouse = ( function() {
function elementFromPoint( doc, mouse ) {
var pointedElement = doc.$.elementFromPoint( mouse.x, mouse.y );
// IE9QM: from times to times it will return an empty object on scroll bar hover. (https://dev.ckeditor.com/ticket/12185)
return pointedElement && pointedElement.nodeType ?
new CKEDITOR.dom.element( pointedElement ) :
null;
}
return function( that, ignoreBox, forceMouse ) {
if ( !that.mouse )
return null;
var doc = that.doc,
lineWrap = that.line.wrap,
mouse = forceMouse || that.mouse,
// Note: element might be null.
element = elementFromPoint( doc, mouse );
// If ignoreBox is set and element is the box, it means that we
// need to hide the box for a while, repeat elementFromPoint
// and show it again.
if ( ignoreBox && isLine( that, element ) ) {
lineWrap.hide();
element = elementFromPoint( doc, mouse );
lineWrap.show();
}
// Return nothing if:
// \-> Element is not HTML.
if ( !( element && element.type == CKEDITOR.NODE_ELEMENT && element.$ ) )
return null;
// Also return nothing if:
// \-> We're IE<9 and element is out of the top-level element (editable for inline and HTML for classic (`iframe`-based)).
// This is due to the bug which allows IE<9 firing mouse events on element
// with contenteditable=true while doing selection out (far, away) of the element.
// Thus we must always be sure that we stay in editable or HTML.
if ( env.ie && env.version < 9 ) {
if ( !( that.boundary.equals( element ) || that.boundary.contains( element ) ) )
return null;
}
return element;
};
} )();
// Gets the closest parent node that belongs to triggers group.
function getAscendantTrigger( that ) {
var node = that.element,
trigger;
if ( node && isHtml( node ) ) {
trigger = node.getAscendant( that.triggers, true );
// If trigger is an element, neither editable nor editable's ascendant.
if ( trigger && that.editable.contains( trigger ) ) {
// Check for closest editable limit.
// Don't consider trigger as a limit as it may be nested editable (includeSelf=false) (https://dev.ckeditor.com/ticket/12009).
var limit = getClosestEditableLimit( trigger );
// Trigger in nested editable area.
if ( limit.getAttribute( 'contenteditable' ) == 'true' )
return trigger;
// Trigger in non-editable area.
else if ( limit.is( that.triggers ) )
return limit;
else
return null;
} else {
return null;
}
}
return null;
}
function getMidpoint( that, upper, lower ) {
updateSize( that, upper );
updateSize( that, lower );
var upperSizeBottom = upper.size.bottom,
lowerSizeTop = lower.size.top;
return upperSizeBottom && lowerSizeTop ? 0 | ( upperSizeBottom + lowerSizeTop ) / 2 : upperSizeBottom || lowerSizeTop;
}
// Get nearest node (either text or HTML), but:
// \-> Omit all empty text nodes (containing white characters only).
// \-> Omit BR elements
// \-> Omit flow breakers.
function getNonEmptyNeighbour( that, node, goBack ) {
node = node[ goBack ? 'getPrevious' : 'getNext' ]( function( node ) {
return ( isTextNode( node ) && !isEmptyTextNode( node ) ) ||
( isHtml( node ) && !isFlowBreaker( node ) && !isLine( that, node ) );
} );
return node;
}
function inBetween( val, lower, upper ) {
return val > lower && val < upper;
}
// Returns the closest ancestor that has contenteditable attribute.
// Such ancestor is the limit of (non-)editable DOM branch that element
// belongs to. This method omits editor editable.
function getClosestEditableLimit( element, includeSelf ) {
if ( element.data( 'cke-editable' ) )
return null;
if ( !includeSelf )
element = element.getParent();
while ( element ) {
if ( element.data( 'cke-editable' ) )
return null;
if ( element.hasAttribute( 'contenteditable' ) )
return element;
element = element.getParent();
}
return null;
}
// Access space line consists of a few elements (spans):
// \-> Line wrapper.
// \-> Line.
// \-> Line triangles: left triangle (LT), right triangle (RT).
// \-> Button handler (BTN).
//
// +--------------------------------------------------- line.wrap (span) -----+
// | +---------------------------------------------------- line (span) -----+ |
// | | +- LT \ +- BTN -+ / RT -+ | |
// | | | \ | | | / | | |
// | | | / | <__| | \ | | |
// | | +-----/ +-------+ \-----+ | |
// | +----------------------------------------------------------------------+ |
// +--------------------------------------------------------------------------+
//
function initLine( that ) {
var doc = that.doc,
// This the main box element that holds triangles and the insertion button
line = newElementFromHtml( ' ', doc ),
iconPath = CKEDITOR.getUrl( this.path + 'images/' + ( env.hidpi ? 'hidpi/' : '' ) + 'icon' + ( that.rtl ? '-rtl' : '' ) + '.png' );
extend( line, {
attach: function() {
// Only if not already attached
if ( !this.wrap.getParent() )
this.wrap.appendTo( that.editable, true );
return this;
},
// Looks are as follows: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].
lineChildren: [
extend(
newElementFromHtml(
'↵ ', doc
), {
base: CSS_COMMON + 'height:17px;width:17px;' + ( that.rtl ? 'left' : 'right' ) + ':17px;' +
'background:url(' + iconPath + ') center no-repeat ' + that.boxColor + ';cursor:pointer;' +
( env.hc ? 'font-size: 15px;line-height:14px;border:1px solid #fff;text-align:center;' : '' ) +
( env.hidpi ? 'background-size: 9px 10px;' : '' ),
looks: [
'top:-8px; border-radius: 2px;',
'top:-17px; border-radius: 2px 2px 0px 0px;',
'top:-1px; border-radius: 0px 0px 2px 2px;'
]
}
),
extend( newElementFromHtml( TRIANGLE_HTML, doc ), {
base: CSS_TRIANGLE + 'left:0px;border-left-color:' + that.boxColor + ';',
looks: [
'border-width:8px 0 8px 8px;top:-8px',
'border-width:8px 0 0 8px;top:-8px',
'border-width:0 0 8px 8px;top:0px'
]
} ),
extend( newElementFromHtml( TRIANGLE_HTML, doc ), {
base: CSS_TRIANGLE + 'right:0px;border-right-color:' + that.boxColor + ';',
looks: [
'border-width:8px 8px 8px 0;top:-8px',
'border-width:8px 8px 0 0;top:-8px',
'border-width:0 8px 8px 0;top:0px'
]
} )
],
detach: function() {
// Detach only if already attached.
if ( this.wrap.getParent() )
this.wrap.remove();
return this;
},
// Checks whether mouseY is around an element by comparing boundaries and considering
// an offset distance.
mouseNear: function() {
updateSize( that, this );
var offset = that.holdDistance,
size = this.size;
// Determine neighborhood by element dimensions and offsets.
if ( size && inBetween( that.mouse.y, size.top - offset, size.bottom + offset ) && inBetween( that.mouse.x, size.left - offset, size.right + offset ) ) {
return true;
}
return false;
},
// Adjusts position of the box according to the trigger properties.
// If also affects look of the box depending on the type of the trigger.
place: function() {
var view = that.view,
editable = that.editable,
trigger = that.trigger,
upper = trigger.upper,
lower = trigger.lower,
any = upper || lower,
parent = any.getParent(),
styleSet = {};
// Save recent trigger for further insertion.
// It is necessary due to the fact, that that.trigger may
// contain different boxTrigger at the moment of insertion
// or may be even null.
this.trigger = trigger;
upper && updateSize( that, upper, true );
lower && updateSize( that, lower, true );
updateSize( that, parent, true );
// Yeah, that's gonna be useful in inline-mode case.
if ( that.inInlineMode )
updateEditableSize( that, true );
// Set X coordinate (left, right, width).
if ( parent.equals( editable ) ) {
styleSet.left = view.scroll.x;
styleSet.right = -view.scroll.x;
styleSet.width = '';
} else {
styleSet.left = any.size.left - any.size.margin.left + view.scroll.x - ( that.inInlineMode ? view.editable.left + view.editable.border.left : 0 );
styleSet.width = any.size.outerWidth + any.size.margin.left + any.size.margin.right + view.scroll.x;
styleSet.right = '';
}
// Set Y coordinate (top) for trigger consisting of two elements.
if ( upper && lower ) {
// No margins at all or they're equal. Place box right between.
if ( upper.size.margin.bottom === lower.size.margin.top )
styleSet.top = 0 | ( upper.size.bottom + upper.size.margin.bottom / 2 );
else {
// Upper margin < lower margin. Place at lower margin.
if ( upper.size.margin.bottom < lower.size.margin.top )
styleSet.top = upper.size.bottom + upper.size.margin.bottom;
// Upper margin > lower margin. Place at upper margin - lower margin.
else
styleSet.top = upper.size.bottom + upper.size.margin.bottom - lower.size.margin.top;
}
}
// Set Y coordinate (top) for single-edge trigger.
else if ( !upper )
styleSet.top = lower.size.top - lower.size.margin.top;
else if ( !lower ) {
styleSet.top = upper.size.bottom + upper.size.margin.bottom;
}
// Set box button modes if close to the viewport horizontal edge
// or look forced by the trigger.
if ( trigger.is( LOOK_TOP ) || inBetween( styleSet.top, view.scroll.y - 15, view.scroll.y + 5 ) ) {
styleSet.top = that.inInlineMode ? 0 : view.scroll.y;
this.look( LOOK_TOP );
} else if ( trigger.is( LOOK_BOTTOM ) || inBetween( styleSet.top, view.pane.bottom - 5, view.pane.bottom + 15 ) ) {
styleSet.top = that.inInlineMode ? (
view.editable.height + view.editable.padding.top + view.editable.padding.bottom
) : (
view.pane.bottom - 1
);
this.look( LOOK_BOTTOM );
} else {
if ( that.inInlineMode )
styleSet.top -= view.editable.top + view.editable.border.top;
this.look( LOOK_NORMAL );
}
if ( that.inInlineMode ) {
// 1px bug here...
styleSet.top--;
// Consider the editable to be an element with overflow:scroll
// and non-zero scrollTop/scrollLeft value.
// For example: divarea editable. (https://dev.ckeditor.com/ticket/9383)
styleSet.top += view.editable.scroll.top;
styleSet.left += view.editable.scroll.left;
}
// Append `px` prefixes.
for ( var style in styleSet )
styleSet[ style ] = CKEDITOR.tools.cssLength( styleSet[ style ] );
this.setStyles( styleSet );
},
// Changes look of the box according to current needs.
// Three different styles are available: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].
look: function( look ) {
if ( this.oldLook == look )
return;
for ( var i = this.lineChildren.length, child; i--; )
( child = this.lineChildren[ i ] ).setAttribute( 'style', child.base + child.looks[ 0 | look / 2 ] );
this.oldLook = look;
},
wrap: new newElement( 'span', that.doc )
} );
// Insert children into the box.
for ( var i = line.lineChildren.length; i--; )
line.lineChildren[ i ].appendTo( line );
// Set default look of the box.
line.look( LOOK_NORMAL );
// Using that wrapper prevents IE (8,9) from resizing editable area at the moment
// of box insertion. This works thanks to the fact, that positioned box is wrapped by
// an inline element. So much tricky.
line.appendTo( line.wrap );
// Make the box unselectable.
line.unselectable();
// Handle accessSpace node insertion.
line.lineChildren[ 0 ].on( 'mouseup', function( event ) {
line.detach();
accessFocusSpace( that, function( accessNode ) {
// Use old trigger that was saved by 'place' method. Look: line.place
var trigger = that.line.trigger;
accessNode[ trigger.is( EDGE_TOP ) ? 'insertBefore' : 'insertAfter' ](
trigger.is( EDGE_TOP ) ? trigger.lower : trigger.upper );
}, true );
that.editor.focus();
if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR )
that.hotNode.scrollIntoView();
event.data.preventDefault( true );
} );
// Prevents IE9 from displaying the resize box and disables drag'n'drop functionality.
line.on( 'mousedown', function( event ) {
event.data.preventDefault( true );
} );
that.line = line;
}
// This function allows accessing any focus space according to the insert function:
// * For enterMode ENTER_P it creates P element filled with dummy white-space.
// * For enterMode ENTER_DIV it creates DIV element filled with dummy white-space.
// * For enterMode ENTER_BR it creates BR element or in IE.
//
// The node is being inserted according to insertFunction. Finally the method
// selects the non-breaking space making the node ready for typing.
function accessFocusSpace( that, insertFunction, doSave ) {
var range = new CKEDITOR.dom.range( that.doc ),
editor = that.editor,
accessNode;
// IE requires text node of in ENTER_BR mode.
if ( env.ie && that.enterMode == CKEDITOR.ENTER_BR )
accessNode = that.doc.createText( WHITE_SPACE );
// In other cases a regular element is used.
else {
// Use the enterMode of editable's limit or editor's
// enter mode if not in nested editable.
var limit = getClosestEditableLimit( that.element, true ),
// This is an enter mode for the context. We cannot use
// editor.activeEnterMode because the focused nested editable will
// have a different enterMode as editor but magicline will be inserted
// directly into editor's editable.
enterMode = limit && limit.data( 'cke-enter-mode' ) || that.enterMode;
accessNode = new newElement( enterElements[ enterMode ], that.doc );
if ( !accessNode.is( 'br' ) ) {
var dummy = that.doc.createText( WHITE_SPACE );
dummy.appendTo( accessNode );
}
}
doSave && editor.fire( 'saveSnapshot' );
insertFunction( accessNode );
//dummy.appendTo( accessNode );
range.moveToPosition( accessNode, CKEDITOR.POSITION_AFTER_START );
editor.getSelection().selectRanges( [ range ] );
that.hotNode = accessNode;
doSave && editor.fire( 'saveSnapshot' );
}
// Access focus space on demand by taking an element under the caret as a reference.
// The space is accessed provided the element under the caret is trigger AND:
//
// 1. First/last-child of its parent:
// +----------------------- Parent element -+
// | +------------------------------ DIV -+ | <-- Access before
// | | Foo^ | |
// | | | |
// | +------------------------------------+ | <-- Access after
// +----------------------------------------+
//
// OR
//
// 2. It has a direct sibling element, which is also a trigger:
// +-------------------------------- DIV#1 -+
// | Foo^ |
// | |
// +----------------------------------------+
// <-- Access here
// +-------------------------------- DIV#2 -+
// | Bar |
// | |
// +----------------------------------------+
//
// OR
//
// 3. It has a direct sibling, which is a trigger and has a valid neighbour trigger,
// but belongs to dtd.$.empty/nonEditable:
// +------------------------------------ P -+
// | Foo^ |
// | |
// +----------------------------------------+
// +----------------------------------- HR -+
// <-- Access here
// +-------------------------------- DIV#2 -+
// | Bar |
// | |
// +----------------------------------------+
//
function accessFocusSpaceCmd( that, insertAfter ) {
return {
canUndo: true,
modes: { wysiwyg: 1 },
exec: ( function() {
// Inserts line (accessNode) at the position by taking target node as a reference.
function doAccess( target ) {
// Remove old hotNode under certain circumstances.
var hotNodeChar = ( env.ie && env.version < 9 ? ' ' : WHITE_SPACE ),
removeOld = that.hotNode && // Old hotNode must exist.
that.hotNode.getText() == hotNodeChar && // Old hotNode hasn't been changed.
that.element.equals( that.hotNode ) && // Caret is inside old hotNode.
// Command is executed in the same direction.
that.lastCmdDirection === !!insertAfter; // jshint ignore:line
accessFocusSpace( that, function( accessNode ) {
if ( removeOld && that.hotNode )
that.hotNode.remove();
accessNode[ insertAfter ? 'insertAfter' : 'insertBefore' ]( target );
// Make this element distinguishable. Also remember the direction
// it's been inserted into document.
accessNode.setAttributes( {
'data-cke-magicline-hot': 1,
'data-cke-magicline-dir': !!insertAfter
} );
// Save last direction of the command (is insertAfter?).
that.lastCmdDirection = !!insertAfter;
} );
if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR )
that.hotNode.scrollIntoView();
// Detach the line if was visible (previously triggered by mouse).
that.line.detach();
}
return function( editor ) {
var selected = editor.getSelection().getStartElement(),
limit;
// (https://dev.ckeditor.com/ticket/9833) Go down to the closest non-inline element in DOM structure
// since inline elements don't participate in in magicline.
selected = selected.getAscendant( DTD_BLOCK, 1 );
// Stop if selected is a child of a tabu-list element.
if ( isInTabu( that, selected ) )
return;
// Sometimes it may happen that there's no parent block below selected element
// or, for example, getAscendant reaches editable or editable parent.
// We must avoid such pathological cases.
if ( !selected || selected.equals( that.editable ) || selected.contains( that.editable ) )
return;
// Executing the command directly in nested editable should
// access space before/after it.
if ( ( limit = getClosestEditableLimit( selected ) ) && limit.getAttribute( 'contenteditable' ) == 'false' )
selected = limit;
// That holds element from mouse. Replace it with the
// element under the caret.
that.element = selected;
// (3.) Handle the following cases where selected neighbour
// is a trigger inaccessible for the caret AND:
// - Is first/last-child
// OR
// - Has a sibling, which is also a trigger.
var neighbor = getNonEmptyNeighbour( that, selected, !insertAfter ),
neighborSibling;
// Check for a neighbour that belongs to triggers.
// Consider only non-accessible elements (they cannot have any children)
// since they cannot be given a caret inside, to run the command
// the regular way (1. & 2.).
if (
isHtml( neighbor ) && neighbor.is( that.triggers ) && neighbor.is( DTD_NONACCESSIBLE ) &&
(
// Check whether neighbor is first/last-child.
!getNonEmptyNeighbour( that, neighbor, !insertAfter ) ||
// Check for a sibling of a neighbour that also is a trigger.
(
( neighborSibling = getNonEmptyNeighbour( that, neighbor, !insertAfter ) ) &&
isHtml( neighborSibling ) &&
neighborSibling.is( that.triggers )
)
)
) {
doAccess( neighbor );
return;
}
// Look for possible target element DOWN "selected" DOM branch (towards editable)
// that belong to that.triggers
var target = getAscendantTrigger( that, selected );
// No HTML target -> no access.
if ( !isHtml( target ) )
return;
// (1.) Target is first/last child -> access.
if ( !getNonEmptyNeighbour( that, target, !insertAfter ) ) {
doAccess( target );
return;
}
var sibling = getNonEmptyNeighbour( that, target, !insertAfter );
// (2.) Target has a sibling that belongs to that.triggers -> access.
if ( sibling && isHtml( sibling ) && sibling.is( that.triggers ) ) {
doAccess( target );
return;
}
};
} )()
};
}
function isLine( that, node ) {
if ( !( node && node.type == CKEDITOR.NODE_ELEMENT && node.$ ) )
return false;
var line = that.line;
return line.wrap.equals( node ) || line.wrap.contains( node );
}
// Is text node containing white-spaces only?
var isEmptyTextNode = CKEDITOR.dom.walker.whitespaces();
// Is fully visible HTML node?
function isHtml( node ) {
return node && node.type == CKEDITOR.NODE_ELEMENT && node.$; // IE requires that
}
function isFloated( element ) {
if ( !isHtml( element ) )
return false;
var options = { left: 1, right: 1, center: 1 };
return !!( options[ element.getComputedStyle( 'float' ) ] || options[ element.getAttribute( 'align' ) ] );
}
function isFlowBreaker( element ) {
if ( !isHtml( element ) )
return false;
return isPositioned( element ) || isFloated( element );
}
// Isn't node of NODE_COMMENT type?
var isComment = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_COMMENT );
function isPositioned( element ) {
return !!{ absolute: 1, fixed: 1 }[ element.getComputedStyle( 'position' ) ];
}
// Is text node?
function isTextNode( node ) {
return node && node.type == CKEDITOR.NODE_TEXT;
}
function isTrigger( that, element ) {
return isHtml( element ) ? element.is( that.triggers ) : null;
}
function isInTabu( that, element ) {
if ( !element )
return false;
var parents = element.getParents( 1 );
for ( var i = parents.length ; i-- ; ) {
for ( var j = that.tabuList.length ; j-- ; ) {
if ( parents[ i ].hasAttribute( that.tabuList[ j ] ) )
return true;
}
}
return false;
}
// This function checks vertically is there's a relevant child between element's edge
// and the pointer.
// \-> Table contents are omitted.
function isChildBetweenPointerAndEdge( that, parent, edgeBottom ) {
var edgeChild = parent[ edgeBottom ? 'getLast' : 'getFirst' ]( function( node ) {
return that.isRelevant( node ) && !node.is( DTD_TABLECONTENT );
} );
if ( !edgeChild )
return false;
updateSize( that, edgeChild );
return edgeBottom ? edgeChild.size.top > that.mouse.y : edgeChild.size.bottom < that.mouse.y;
}
// This method handles edge cases:
// \-> Mouse is around upper or lower edge of view pane.
// \-> Also scroll position is either minimal or maximal.
// \-> It's OK to show LOOK_TOP(BOTTOM) type line.
//
// This trigger doesn't need additional post-filtering.
//
// +----------------------------- Editable -+ /--
// | +---------------------- First child -+ | | <-- Top edge (first child)
// | | | | |
// | | | | | * Mouse activation area *
// | | | | |
// | | ... | | \-- Top edge + trigger offset
// | . . |
// | |
// | . . |
// | | ... | | /-- Bottom edge - trigger offset
// | | | | |
// | | | | | * Mouse activation area *
// | | | | |
// | +----------------------- Last child -+ | | <-- Bottom edge (last child)
// +----------------------------------------+ \--
//
function triggerEditable( that ) {
var editable = that.editable,
mouse = that.mouse,
view = that.view,
triggerOffset = that.triggerOffset,
triggerLook;
// Update editable dimensions.
updateEditableSize( that );
// This flag determines whether checking bottom trigger.
var bottomTrigger = mouse.y > (
that.inInlineMode ? (
view.editable.top + view.editable.height / 2
) : (
// This is to handle case when editable.height / 2 <<< pane.height.
Math.min( view.editable.height, view.pane.height ) / 2
)
),
// Edge node according to bottomTrigger.
edgeNode = editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( function( node ) {
return !( isEmptyTextNode( node ) || isComment( node ) );
} );
// There's no edge node. Abort.
if ( !edgeNode ) {
return null;
}
// If the edgeNode in editable is ML, get the next one.
if ( isLine( that, edgeNode ) ) {
edgeNode = that.line.wrap[ bottomTrigger ? 'getPrevious' : 'getNext' ]( function( node ) {
return !( isEmptyTextNode( node ) || isComment( node ) );
} );
}
// Exclude bad nodes (no ML needed then):
// \-> Edge node is text.
// \-> Edge node is floated, etc.
//
// Edge node *must be* a valid trigger at this stage as well.
if ( !isHtml( edgeNode ) || isFlowBreaker( edgeNode ) || !isTrigger( that, edgeNode ) ) {
return null;
}
// Update size of edge node. Dimensions will be necessary.
updateSize( that, edgeNode );
// Return appropriate trigger according to bottomTrigger.
// \-> Top edge trigger case first.
if ( !bottomTrigger && // Top trigger case.
edgeNode.size.top >= 0 && // Check if the first element is fully visible.
inBetween( mouse.y, 0, edgeNode.size.top + triggerOffset ) ) { // Check if mouse in [0, edgeNode.top + triggerOffset].
// Determine trigger look.
triggerLook = that.inInlineMode || view.scroll.y === 0 ?
LOOK_TOP : LOOK_NORMAL;
return new boxTrigger( [ null, edgeNode,
EDGE_TOP,
TYPE_EDGE,
triggerLook
] );
}
// \-> Bottom case.
else if ( bottomTrigger &&
edgeNode.size.bottom <= view.pane.height && // Check if the last element is fully visible
inBetween( mouse.y, // Check if mouse in...
edgeNode.size.bottom - triggerOffset, view.pane.height ) ) { // [ edgeNode.bottom - triggerOffset, paneHeight ]
// Determine trigger look.
triggerLook = that.inInlineMode ||
inBetween( edgeNode.size.bottom, view.pane.height - triggerOffset, view.pane.height ) ?
LOOK_BOTTOM : LOOK_NORMAL;
return new boxTrigger( [ edgeNode, null,
EDGE_BOTTOM,
TYPE_EDGE,
triggerLook
] );
}
return null;
}
// This method covers cases *inside* of an element:
// \-> The pointer is in the top (bottom) area of an element and there's
// HTML node before (after) this element.
// \-> An element being the first or last child of its parent.
//
// +----------------------- Parent element -+
// | +----------------------- Element #1 -+ | /--
// | | | | | * Mouse activation area (as first child) *
// | | | | \--
// | | | | /--
// | | | | | * Mouse activation area (Element #2) *
// | +------------------------------------+ | \--
// | |
// | +----------------------- Element #2 -+ | /--
// | | | | | * Mouse activation area (Element #1) *
// | | | | \--
// | | | |
// | +------------------------------------+ |
// | |
// | Text node is here. |
// | |
// | +----------------------- Element #3 -+ |
// | | | |
// | | | |
// | | | | /--
// | | | | | * Mouse activation area (as last child) *
// | +------------------------------------+ | \--
// +----------------------------------------+
//
function triggerEdge( that ) {
var mouse = that.mouse,
view = that.view,
triggerOffset = that.triggerOffset;
// Get the ascendant trigger basing on elementFromMouse.
var element = getAscendantTrigger( that );
// Abort if there's no appropriate element.
if ( !element ) {
return null;
}
// Dimensions will be necessary.
updateSize( that, element );
// If triggerOffset is larger than a half of element's height,
// use an offset of 1/2 of element's height. If the offset wasn't reduced,
// top area would cover most (all) cases.
var fixedOffset = Math.min( triggerOffset,
0 | ( element.size.outerHeight / 2 ) ),
// This variable will hold the trigger to be returned.
triggerSetup = [],
triggerLook,
// This flag determines whether dealing with a bottom trigger.
bottomTrigger;
// \-> Top trigger.
if ( inBetween( mouse.y, element.size.top - 1, element.size.top + fixedOffset ) )
bottomTrigger = false;
// \-> Bottom trigger.
else if ( inBetween( mouse.y, element.size.bottom - fixedOffset, element.size.bottom + 1 ) )
bottomTrigger = true;
// \-> Abort. Not in a valid trigger space.
else {
return null;
}
// Reject wrong elements.
// \-> Reject an element which is a flow breaker.
// \-> Reject an element which has a child above/below the mouse pointer.
// \-> Reject an element which belongs to list items.
if (
isFlowBreaker( element ) ||
isChildBetweenPointerAndEdge( that, element, bottomTrigger ) ||
element.getParent().is( DTD_LISTITEM )
) {
return null;
}
// Get sibling according to bottomTrigger.
var elementSibling = getNonEmptyNeighbour( that, element, !bottomTrigger );
// No sibling element.
// This is a first or last child case.
if ( !elementSibling ) {
// No need to reject the element as it has already been done before.
// Prepare a trigger.
// Determine trigger look.
if ( element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ) {
updateEditableSize( that );
if (
bottomTrigger && inBetween( mouse.y,
element.size.bottom - fixedOffset, view.pane.height ) &&
inBetween( element.size.bottom, view.pane.height - fixedOffset, view.pane.height )
) {
triggerLook = LOOK_BOTTOM;
} else if ( inBetween( mouse.y, 0, element.size.top + fixedOffset ) ) {
triggerLook = LOOK_TOP;
}
} else {
triggerLook = LOOK_NORMAL;
}
triggerSetup = [ null, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [
bottomTrigger ? EDGE_BOTTOM : EDGE_TOP,
TYPE_EDGE,
triggerLook,
element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ?
( bottomTrigger ? LOOK_BOTTOM : LOOK_TOP ) : LOOK_NORMAL
] );
}
// Abort. Sibling is a text element.
else if ( isTextNode( elementSibling ) ) {
return null;
}
// Check if the sibling is a HTML element.
// If so, create an TYPE_EDGE, EDGE_MIDDLE trigger.
else if ( isHtml( elementSibling ) ) {
// Reject wrong elementSiblings.
// \-> Reject an elementSibling which is a flow breaker.
// \-> Reject an elementSibling which isn't a trigger.
// \-> Reject an elementSibling which belongs to list items.
if (
isFlowBreaker( elementSibling ) ||
!isTrigger( that, elementSibling ) ||
elementSibling.getParent().is( DTD_LISTITEM )
) {
return null;
}
// Prepare a trigger.
triggerSetup = [ elementSibling, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [
EDGE_MIDDLE,
TYPE_EDGE
] );
}
if ( 0 in triggerSetup ) {
return new boxTrigger( triggerSetup );
}
return null;
}
// Checks iteratively up and down in search for elements using elementFromMouse method.
// Useful if between two triggers.
//
// +----------------------- Parent element -+
// | +----------------------- Element #1 -+ |
// | | | |
// | | | |
// | | | |
// | +------------------------------------+ |
// | | /--
// | . | |
// | . +-- Floated -+ | |
// | | | | | | * Mouse activation area *
// | | | IGNORE | | |
// | X | | | | Method searches vertically for sibling elements.
// | | +------------+ | | Start point is X (mouse-y coordinate).
// | | | | Floated elements, comments and empty text nodes are omitted.
// | . | |
// | . | |
// | | \--
// | +----------------------- Element #2 -+ |
// | | | |
// | | | |
// | | | |
// | | | |
// | +------------------------------------+ |
// +----------------------------------------+
//
var triggerExpand = ( function() {
// The heart of the procedure. This method creates triggers that are
// filtered by expandFilter method.
function expandEngine( that ) {
var startElement = that.element,
upper, lower, trigger;
if ( !isHtml( startElement ) || startElement.contains( that.editable ) ) {
return null;
}
// Stop searching if element is in non-editable branch of DOM.
if ( startElement.isReadOnly() )
return null;
trigger = verticalSearch( that,
function( current, startElement ) {
return !startElement.equals( current ); // stop when start element and the current one differ
}, function( that, mouse ) {
return elementFromMouse( that, true, mouse );
}, startElement ),
upper = trigger.upper,
lower = trigger.lower;
// Success: two siblings have been found
if ( areSiblings( that, upper, lower ) ) {
return trigger.set( EDGE_MIDDLE, TYPE_EXPAND );
}
// Danger. Dragons ahead.
// No siblings have been found during previous phase, post-processing may be necessary.
// We can traverse DOM until a valid pair of elements around the pointer is found.
// Prepare for post-processing:
// 1. Determine if upper and lower are children of startElement.
// 1.1. If so, find their ascendants that are closest to startElement (one level deeper than startElement).
// 1.2. Otherwise use first/last-child of the startElement as upper/lower. Why?:
// a) upper/lower belongs to another branch of the DOM tree.
// b) verticalSearch encountered an edge of the viewport and failed.
// 1.3. Make sure upper and lower still exist. Why?:
// a) Upper and lower may be not belong to the branch of the startElement (may not exist at all) and
// startElement has no children.
// 2. Perform the post-processing.
// 2.1. Gather dimensions of an upper element.
// 2.2. Abort if lower edge of upper is already under the mouse pointer. Why?:
// a) We expect upper to be above and lower below the mouse pointer.
// 3. Perform iterative search while upper != lower.
// 3.1. Find the upper-next element. If there's no such element, break current search. Why?:
// a) There's no point in further search if there are only text nodes ahead.
// 3.2. Calculate the distance between the middle point of ( upper, upperNext ) and mouse-y.
// 3.3. If the distance is shorter than the previous best, save it (save upper, upperNext as well).
// 3.4. If the optimal pair is found, assign it back to the trigger.
// 1.1., 1.2.
if ( upper && startElement.contains( upper ) ) {
while ( !upper.getParent().equals( startElement ) )
upper = upper.getParent();
} else {
upper = startElement.getFirst( function( node ) {
return expandSelector( that, node );
} );
}
if ( lower && startElement.contains( lower ) ) {
while ( !lower.getParent().equals( startElement ) )
lower = lower.getParent();
} else {
lower = startElement.getLast( function( node ) {
return expandSelector( that, node );
} );
}
// 1.3.
if ( !upper || !lower ) {
return null;
}
// 2.1.
updateSize( that, upper );
updateSize( that, lower );
if ( !checkMouseBetweenElements( that, upper, lower ) ) {
return null;
}
var minDistance = Number.MAX_VALUE,
currentDistance, upperNext, minElement, minElementNext;
while ( lower && !lower.equals( upper ) ) {
// 3.1.
if ( !( upperNext = upper.getNext( that.isRelevant ) ) )
break;
// 3.2.
currentDistance = Math.abs( getMidpoint( that, upper, upperNext ) - that.mouse.y );
// 3.3.
if ( currentDistance < minDistance ) {
minDistance = currentDistance;
minElement = upper;
minElementNext = upperNext;
}
upper = upperNext;
updateSize( that, upper );
}
// 3.4.
if ( !minElement || !minElementNext ) {
return null;
}
if ( !checkMouseBetweenElements( that, minElement, minElementNext ) ) {
return null;
}
// An element of minimal distance has been found. Assign it to the trigger.
trigger.upper = minElement;
trigger.lower = minElementNext;
// Success: post-processing revealed a pair of elements.
return trigger.set( EDGE_MIDDLE, TYPE_EXPAND );
}
// This is default element selector used by the engine.
function expandSelector( that, node ) {
return !( isTextNode( node ) ||
isComment( node ) ||
isFlowBreaker( node ) ||
isLine( that, node ) ||
( node.type == CKEDITOR.NODE_ELEMENT && node.$ && node.is( 'br' ) ) );
}
// This method checks whether mouse-y is between the top edge of upper
// and bottom edge of lower.
//
// NOTE: This method assumes that updateSize has already been called
// for the elements and is up-to-date.
//
// +---------------------------- Upper -+ /--
// | | |
// +------------------------------------+ |
// |
// ... |
// |
// X | * Return true for mouse-y in this range *
// |
// ... |
// |
// +---------------------------- Lower -+ |
// | | |
// +------------------------------------+ \--
//
function checkMouseBetweenElements( that, upper, lower ) {
return inBetween( that.mouse.y, upper.size.top, lower.size.bottom );
}
// A method for trigger filtering. Accepts or rejects trigger pairs
// by their location in DOM etc.
function expandFilter( that, trigger ) {
var upper = trigger.upper,
lower = trigger.lower;
if (
!upper || !lower || // NOT: EDGE_MIDDLE trigger ALWAYS has two elements.
isFlowBreaker( lower ) || isFlowBreaker( upper ) || // NOT: one of the elements is floated or positioned
lower.equals( upper ) || upper.equals( lower ) || // NOT: two trigger elements, one equals another.
lower.contains( upper ) || upper.contains( lower )
) { // NOT: two trigger elements, one contains another.
return false;
}
// YES: two trigger elements, pure siblings.
else if ( isTrigger( that, upper ) && isTrigger( that, lower ) && areSiblings( that, upper, lower ) ) {
return true;
}
return false;
}
// Simple wrapper for expandEngine and expandFilter.
return function( that ) {
var trigger = expandEngine( that );
return trigger && expandFilter( that, trigger ) ? trigger : null;
};
} )();
// Collects dimensions of an element.
var sizePrefixes = [ 'top', 'left', 'right', 'bottom' ];
function getSize( that, element, ignoreScroll, force ) {
var docPosition = element.getDocumentPosition(),
border = {},
margin = {},
padding = {},
box = {};
for ( var i = sizePrefixes.length; i--; ) {
border[ sizePrefixes[ i ] ] = parseInt( getStyle( 'border-' + sizePrefixes[ i ] + '-width' ), 10 ) || 0;
padding[ sizePrefixes[ i ] ] = parseInt( getStyle( 'padding-' + sizePrefixes[ i ] ), 10 ) || 0;
margin[ sizePrefixes[ i ] ] = parseInt( getStyle( 'margin-' + sizePrefixes[ i ] ), 10 ) || 0;
}
// updateWindowSize if forced to do so OR NOT ignoring scroll.
if ( !ignoreScroll || force )
updateWindowSize( that, force );
box.top = docPosition.y - ( ignoreScroll ? 0 : that.view.scroll.y ), box.left = docPosition.x - ( ignoreScroll ? 0 : that.view.scroll.x ),
// w/ borders and paddings.
box.outerWidth = element.$.offsetWidth, box.outerHeight = element.$.offsetHeight,
// w/o borders and paddings.
box.height = box.outerHeight - ( padding.top + padding.bottom + border.top + border.bottom ), box.width = box.outerWidth - ( padding.left + padding.right + border.left + border.right ),
box.bottom = box.top + box.outerHeight, box.right = box.left + box.outerWidth;
if ( that.inInlineMode ) {
box.scroll = {
top: element.$.scrollTop,
left: element.$.scrollLeft
};
}
return extend( {
border: border,
padding: padding,
margin: margin,
ignoreScroll: ignoreScroll
}, box, true );
function getStyle( propertyName ) {
return element.getComputedStyle.call( element, propertyName );
}
}
function updateSize( that, element, ignoreScroll ) {
if ( !isHtml( element ) ) // i.e. an element is hidden
return ( element.size = null ); // -> reset size to make it useless for other methods
if ( !element.size )
element.size = {};
// Abort if there was a similar query performed recently.
// This kind of caching provides great performance improvement.
else if ( element.size.ignoreScroll == ignoreScroll && element.size.date > new Date() - CACHE_TIME ) {
return null;
}
return extend( element.size, getSize( that, element, ignoreScroll ), {
date: +new Date()
}, true );
}
// Updates that.view.editable object.
// This one must be called separately outside of updateWindowSize
// to prevent cyclic dependency getSize<->updateWindowSize.
// It calls getSize with force flag to avoid getWindowSize cache (look: getSize).
function updateEditableSize( that, ignoreScroll ) {
that.view.editable = getSize( that, that.editable, ignoreScroll, true );
}
function updateWindowSize( that, force ) {
if ( !that.view )
that.view = {};
var view = that.view;
if ( !force && view && view.date > new Date() - CACHE_TIME ) {
return;
}
var win = that.win,
scroll = win.getScrollPosition(),
paneSize = win.getViewPaneSize();
extend( that.view, {
scroll: {
x: scroll.x,
y: scroll.y,
width: that.doc.$.documentElement.scrollWidth - paneSize.width,
height: that.doc.$.documentElement.scrollHeight - paneSize.height
},
pane: {
width: paneSize.width,
height: paneSize.height,
bottom: paneSize.height + scroll.y
},
date: +new Date()
}, true );
}
// This method searches document vertically using given
// select criterion until stop criterion is fulfilled.
function verticalSearch( that, stopCondition, selectCriterion, startElement ) {
var upper = startElement,
lower = startElement,
mouseStep = 0,
upperFound = false,
lowerFound = false,
viewPaneHeight = that.view.pane.height,
mouse = that.mouse;
while ( mouse.y + mouseStep < viewPaneHeight && mouse.y - mouseStep > 0 ) {
if ( !upperFound )
upperFound = stopCondition( upper, startElement );
if ( !lowerFound )
lowerFound = stopCondition( lower, startElement );
// Still not found...
if ( !upperFound && mouse.y - mouseStep > 0 )
upper = selectCriterion( that, { x: mouse.x, y: mouse.y - mouseStep } );
if ( !lowerFound && mouse.y + mouseStep < viewPaneHeight )
lower = selectCriterion( that, { x: mouse.x, y: mouse.y + mouseStep } );
if ( upperFound && lowerFound )
break;
// Instead of ++ to reduce the number of invocations by half.
// It's trades off accuracy in some edge cases for improved performance.
mouseStep += 2;
}
return new boxTrigger( [ upper, lower, null, null ] );
}
} )();
/**
* Sets the default vertical distance between the edge of the element and the mouse pointer that
* causes the magic line to appear. This option accepts a value in pixels, without the unit (for example:
* `15` for 15 pixels).
*
* Read more in the {@glink features/magicline documentation}
* and see the {@glink examples/magicline example}.
*
* // Changes the offset to 15px.
* CKEDITOR.config.magicline_triggerOffset = 15;
*
* @cfg {Number} [magicline_triggerOffset=30]
* @member CKEDITOR.config
* @see CKEDITOR.config#magicline_holdDistance
*/
/**
* Defines the distance between the mouse pointer and the box within
* which the magic line stays revealed and no other focus space is offered to be accessed.
* This value is relative to {@link #magicline_triggerOffset}.
*
* Read more in the {@glink features/magicline documentation}
* and see the {@glink examples/magicline example}.
*
* // Increases the distance to 80% of CKEDITOR.config.magicline_triggerOffset.
* CKEDITOR.config.magicline_holdDistance = .8;
*
* @cfg {Number} [magicline_holdDistance=0.5]
* @member CKEDITOR.config
* @see CKEDITOR.config#magicline_triggerOffset
*/
/**
* Defines the default keystroke that accesses the closest unreachable focus space **before**
* the caret (start of the selection). If there is no focus space available, the selection remains unchanged.
*
* Read more in the {@glink features/magicline documentation}
* and see the {@glink examples/magicline example}.
*
* // Changes the default keystroke to "Ctrl + ,".
* CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + 188;
*
* @cfg {Number} [magicline_keystrokePrevious=CKEDITOR.CTRL + CKEDITOR.SHIFT + 51 (CTRL + SHIFT + 3)]
* @member CKEDITOR.config
*/
CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + CKEDITOR.SHIFT + 51; // CTRL + SHIFT + 3
/**
* Defines the default keystroke that accesses the closest unreachable focus space **after**
* the caret (start of the selection). If there is no focus space available, the selection remains unchanged.
*
* Read more in the {@glink features/magicline documentation}
* and see the {@glink examples/magicline example}.
*
* // Changes keystroke to "Ctrl + .".
* CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + 190;
*
* @cfg {Number} [magicline_keystrokeNext=CKEDITOR.CTRL + CKEDITOR.SHIFT + 52 (CTRL + SHIFT + 4)]
* @member CKEDITOR.config
*/
CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + CKEDITOR.SHIFT + 52; // CTRL + SHIFT + 4
/**
* Defines a list of attributes that, if assigned to some elements, prevent the magic line from being
* used within these elements.
*
* Read more in the {@glink features/magicline documentation}
* and see the {@glink examples/magicline example}.
*
* // Adds the "data-tabu" attribute to the magic line tabu list.
* CKEDITOR.config.magicline_tabuList = [ 'data-tabu' ];
*
* @cfg {Number} [magicline_tabuList=[ 'data-widget-wrapper' ]]
* @member CKEDITOR.config
*/
/**
* Defines the color of the magic line. The color may be adjusted to enhance readability.
*
* Read more in the {@glink features/magicline documentation}
* and see the {@glink examples/magicline example}.
*
* // Changes magic line color to blue.
* CKEDITOR.config.magicline_color = '#0000FF';
*
* @cfg {String} [magicline_color='#FF0000']
* @member CKEDITOR.config
*/
/**
* Activates the special all-encompassing mode that considers all focus spaces between
* {@link CKEDITOR.dtd#$block} elements as accessible by the magic line.
*
* Read more in the {@glink features/magicline documentation}
* and see the {@glink examples/magicline example}.
*
* // Enables the greedy "put everywhere" mode.
* CKEDITOR.config.magicline_everywhere = true;
*
* @cfg {Boolean} [magicline_everywhere=false]
* @member CKEDITOR.config
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
( function() {
function protectFormStyles( formElement ) {
if ( !formElement || formElement.type != CKEDITOR.NODE_ELEMENT || formElement.getName() != 'form' )
return [];
var hijackRecord = [],
hijackNames = [ 'style', 'className' ];
for ( var i = 0; i < hijackNames.length; i++ ) {
var name = hijackNames[ i ];
var $node = formElement.$.elements.namedItem( name );
if ( $node ) {
var hijackNode = new CKEDITOR.dom.element( $node );
hijackRecord.push( [ hijackNode, hijackNode.nextSibling ] );
hijackNode.remove();
}
}
return hijackRecord;
}
function restoreFormStyles( formElement, hijackRecord ) {
if ( !formElement || formElement.type != CKEDITOR.NODE_ELEMENT || formElement.getName() != 'form' )
return;
if ( hijackRecord.length > 0 ) {
for ( var i = hijackRecord.length - 1; i >= 0; i-- ) {
var node = hijackRecord[ i ][ 0 ];
var sibling = hijackRecord[ i ][ 1 ];
if ( sibling )
node.insertBefore( sibling );
else
node.appendTo( formElement );
}
}
}
function saveStyles( element, isInsideEditor ) {
var data = protectFormStyles( element );
var retval = {};
var $element = element.$;
if ( !isInsideEditor ) {
retval[ 'class' ] = $element.className || '';
$element.className = '';
}
retval.inline = $element.style.cssText || '';
if ( !isInsideEditor ) // Reset any external styles that might interfere. (https://dev.ckeditor.com/ticket/2474)
$element.style.cssText = 'position: static; overflow: visible';
restoreFormStyles( data );
return retval;
}
function restoreStyles( element, savedStyles ) {
var data = protectFormStyles( element );
var $element = element.$;
if ( 'class' in savedStyles )
$element.className = savedStyles[ 'class' ];
if ( 'inline' in savedStyles )
$element.style.cssText = savedStyles.inline;
restoreFormStyles( data );
}
function refreshCursor( editor ) {
if ( editor.editable().isInline() )
return;
// Refresh all editor instances on the page (https://dev.ckeditor.com/ticket/5724).
var all = CKEDITOR.instances;
for ( var i in all ) {
var one = all[ i ];
if ( one.mode == 'wysiwyg' && !one.readOnly ) {
var body = one.document.getBody();
// Refresh 'contentEditable' otherwise
// DOM lifting breaks design mode. (https://dev.ckeditor.com/ticket/5560)
body.setAttribute( 'contentEditable', false );
body.setAttribute( 'contentEditable', true );
}
}
if ( editor.editable().hasFocus ) {
editor.toolbox.focus();
editor.focus();
}
}
CKEDITOR.plugins.add( 'maximize', {
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
// Maximize plugin isn't available in inline mode yet.
if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE )
return;
var lang = editor.lang;
var mainDocument = CKEDITOR.document,
mainWindow = mainDocument.getWindow();
// Saved selection and scroll position for the editing area.
var savedSelection, savedScroll;
// Saved scroll position for the outer window.
var outerScroll;
// Saved resize handler function.
function resizeHandler() {
var viewPaneSize = mainWindow.getViewPaneSize();
editor.resize( viewPaneSize.width, viewPaneSize.height, null, true );
}
// Retain state after mode switches.
var savedState = CKEDITOR.TRISTATE_OFF;
editor.addCommand( 'maximize', {
// Disabled on iOS (https://dev.ckeditor.com/ticket/8307).
modes: { wysiwyg: !CKEDITOR.env.iOS, source: !CKEDITOR.env.iOS },
readOnly: 1,
editorFocus: false,
exec: function() {
var container = editor.container.getFirst( function( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && node.hasClass( 'cke_inner' );
} );
var contents = editor.ui.space( 'contents' );
// Save current selection and scroll position in editing area.
if ( editor.mode == 'wysiwyg' ) {
var selection = editor.getSelection();
savedSelection = selection && selection.getRanges();
savedScroll = mainWindow.getScrollPosition();
} else {
var $textarea = editor.editable().$;
savedSelection = !CKEDITOR.env.ie && [ $textarea.selectionStart, $textarea.selectionEnd ];
savedScroll = [ $textarea.scrollLeft, $textarea.scrollTop ];
}
// Go fullscreen if the state is off.
if ( this.state == CKEDITOR.TRISTATE_OFF ) {
// Add event handler for resizing.
mainWindow.on( 'resize', resizeHandler );
// Save the scroll bar position.
outerScroll = mainWindow.getScrollPosition();
// Save and reset the styles for the entire node tree.
var currentNode = editor.container;
while ( ( currentNode = currentNode.getParent() ) ) {
currentNode.setCustomData( 'maximize_saved_styles', saveStyles( currentNode ) );
// Show under floatpanels (-1) and context menu (-2).
currentNode.setStyle( 'z-index', editor.config.baseFloatZIndex - 5 );
}
contents.setCustomData( 'maximize_saved_styles', saveStyles( contents, true ) );
container.setCustomData( 'maximize_saved_styles', saveStyles( container, true ) );
// Hide scroll bars.
var styles = {
overflow: CKEDITOR.env.webkit ? '' : 'hidden', // https://dev.ckeditor.com/ticket/6896
width: 0,
height: 0
};
mainDocument.getDocumentElement().setStyles( styles );
!CKEDITOR.env.gecko && mainDocument.getDocumentElement().setStyle( 'position', 'fixed' );
!( CKEDITOR.env.gecko && CKEDITOR.env.quirks ) && mainDocument.getBody().setStyles( styles );
// Scroll to the top left (IE needs some time for it - https://dev.ckeditor.com/ticket/4923).
CKEDITOR.env.ie ? setTimeout( function() {
mainWindow.$.scrollTo( 0, 0 );
}, 0 ) : mainWindow.$.scrollTo( 0, 0 );
// Resize and move to top left.
// Special treatment for FF Quirks (https://dev.ckeditor.com/ticket/7284)
container.setStyle( 'position', CKEDITOR.env.gecko && CKEDITOR.env.quirks ? 'fixed' : 'absolute' );
container.$.offsetLeft; // SAFARI BUG: See https://dev.ckeditor.com/ticket/2066.
container.setStyles( {
// Show under floatpanels (-1) and context menu (-2).
'z-index': editor.config.baseFloatZIndex - 5,
left: '0px',
top: '0px'
} );
// Add cke_maximized class before resize handle since that will change things sizes (https://dev.ckeditor.com/ticket/5580)
container.addClass( 'cke_maximized' );
resizeHandler();
// Still not top left? Fix it. (Bug https://dev.ckeditor.com/ticket/174)
var offset = container.getDocumentPosition();
container.setStyles( {
left: ( -1 * offset.x ) + 'px',
top: ( -1 * offset.y ) + 'px'
} );
// Fixing positioning editor chrome in Firefox break design mode. (https://dev.ckeditor.com/ticket/5149)
CKEDITOR.env.gecko && refreshCursor( editor );
}
// Restore from fullscreen if the state is on.
else if ( this.state == CKEDITOR.TRISTATE_ON ) {
// Remove event handler for resizing.
mainWindow.removeListener( 'resize', resizeHandler );
// Restore CSS styles for the entire node tree.
var editorElements = [ contents, container ];
for ( var i = 0; i < editorElements.length; i++ ) {
restoreStyles( editorElements[ i ], editorElements[ i ].getCustomData( 'maximize_saved_styles' ) );
editorElements[ i ].removeCustomData( 'maximize_saved_styles' );
}
currentNode = editor.container;
while ( ( currentNode = currentNode.getParent() ) ) {
restoreStyles( currentNode, currentNode.getCustomData( 'maximize_saved_styles' ) );
currentNode.removeCustomData( 'maximize_saved_styles' );
}
// Restore the window scroll position.
CKEDITOR.env.ie ? setTimeout( function() {
mainWindow.$.scrollTo( outerScroll.x, outerScroll.y );
}, 0 ) : mainWindow.$.scrollTo( outerScroll.x, outerScroll.y );
// Remove cke_maximized class.
container.removeClass( 'cke_maximized' );
// Webkit requires a re-layout on editor chrome. (https://dev.ckeditor.com/ticket/6695)
if ( CKEDITOR.env.webkit ) {
container.setStyle( 'display', 'inline' );
setTimeout( function() {
container.setStyle( 'display', 'block' );
}, 0 );
}
// Emit a resize event, because this time the size is modified in
// restoreStyles.
editor.fire( 'resize', {
outerHeight: editor.container.$.offsetHeight,
contentsHeight: contents.$.offsetHeight,
outerWidth: editor.container.$.offsetWidth
} );
}
this.toggleState();
// Toggle button label.
var button = this.uiItems[ 0 ];
// Only try to change the button if it exists (https://dev.ckeditor.com/ticket/6166)
if ( button ) {
var label = ( this.state == CKEDITOR.TRISTATE_OFF ) ? lang.maximize.maximize : lang.maximize.minimize;
var buttonNode = CKEDITOR.document.getById( button._.id );
buttonNode.getChild( 1 ).setHtml( label );
buttonNode.setAttribute( 'title', label );
buttonNode.setAttribute( 'href', 'javascript:void("' + label + '");' ); // jshint ignore:line
}
// Restore selection and scroll position in editing area.
if ( editor.mode == 'wysiwyg' ) {
if ( savedSelection ) {
// Fixing positioning editor chrome in Firefox break design mode. (https://dev.ckeditor.com/ticket/5149)
CKEDITOR.env.gecko && refreshCursor( editor );
editor.getSelection().selectRanges( savedSelection );
var element = editor.getSelection().getStartElement();
element && element.scrollIntoView( true );
} else {
mainWindow.$.scrollTo( savedScroll.x, savedScroll.y );
}
} else {
if ( savedSelection ) {
$textarea.selectionStart = savedSelection[ 0 ];
$textarea.selectionEnd = savedSelection[ 1 ];
}
$textarea.scrollLeft = savedScroll[ 0 ];
$textarea.scrollTop = savedScroll[ 1 ];
}
savedSelection = savedScroll = null;
savedState = this.state;
editor.fire( 'maximize', this.state );
},
canUndo: false
} );
editor.ui.addButton && editor.ui.addButton( 'Maximize', {
label: lang.maximize.maximize,
command: 'maximize',
toolbar: 'tools,10'
} );
// Restore the command state after mode change, unless it has been changed to disabled (https://dev.ckeditor.com/ticket/6467)
editor.on( 'mode', function() {
var command = editor.getCommand( 'maximize' );
command.setState( command.state == CKEDITOR.TRISTATE_DISABLED ? CKEDITOR.TRISTATE_DISABLED : savedState );
}, null, null, 100 );
}
} );
} )();
/**
* Event fired when the maximize command is called.
* It also indicates whether an editor is maximized or not.
*
* @event maximize
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param {Number} data Current state of the command. See {@link CKEDITOR#TRISTATE_ON} and {@link CKEDITOR#TRISTATE_OFF}.
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
( function() {
/* global confirm */
CKEDITOR.plugins.add( 'pastefromword', {
requires: 'clipboard',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
// Flag indicate this command is actually been asked instead of a generic pasting.
var forceFromWord = 0,
path = this.path,
configInlineImages = editor.config.pasteFromWord_inlineImages === undefined ? true : editor.config.pasteFromWord_inlineImages;
editor.addCommand( 'pastefromword', {
// Snapshots are done manually by editable.insertXXX methods.
canUndo: false,
async: true,
/**
* The Paste from Word command. It will determine its pasted content from Word automatically if possible.
*
* At the time of writing it was working correctly only in Internet Explorer browsers, due to their
* `paste` support in `document.execCommand`.
*
* @private
* @param {CKEDITOR.editor} editor An instance of the editor where the command is being executed.
* @param {Object} [data] The options object.
* @param {Boolean/String} [data.notification=true] Content for a notification shown after an unsuccessful
* paste attempt. If `false`, the notification will not be displayed. This parameter was added in 4.7.0.
* @member CKEDITOR.editor.commands.pastefromword
*/
exec: function( editor, data ) {
forceFromWord = 1;
editor.execCommand( 'paste', {
type: 'html',
notification: data && typeof data.notification !== 'undefined' ? data.notification : true
} );
}
} );
// Register the toolbar button.
CKEDITOR.plugins.clipboard.addPasteButton( editor, 'PasteFromWord', {
label: editor.lang.pastefromword.toolbar,
command: 'pastefromword',
toolbar: 'clipboard,50'
} );
// Features brought by this command beside the normal process:
// 1. No more bothering of user about the clean-up.
// 2. Perform the clean-up even if content is not from Microsoft Word.
// (e.g. from a Microsoft Word similar application.)
// 3. Listen with high priority (3), so clean up is done before content
// type sniffing (priority = 6).
editor.on( 'paste', function( evt ) {
var data = evt.data,
dataTransferHtml = CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ?
data.dataTransfer.getData( 'text/html', true ) : null,
// Required in Paste from Word Image plugin (#662).
dataTransferRtf = CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ?
data.dataTransfer.getData( 'text/rtf' ) : null,
// Some commands fire paste event without setting dataTransfer property. In such case
// dataValue should be used.
mswordHtml = dataTransferHtml || data.dataValue,
pfwEvtData = { dataValue: mswordHtml, dataTransfer: { 'text/rtf': dataTransferRtf } },
officeMetaRegexp = / |<\/font>)/,
isOfficeContent = officeMetaRegexp.test( mswordHtml ) || wordRegexp.test( mswordHtml );
if ( !mswordHtml || !( forceFromWord || isOfficeContent ) ) {
return;
}
// PFW might still get prevented, if it's not forced.
if ( editor.fire( 'pasteFromWord', pfwEvtData ) === false && !forceFromWord ) {
return;
}
// Do not apply paste filter to data filtered by the Word filter (https://dev.ckeditor.com/ticket/13093).
data.dontFilter = true;
// If filter rules aren't loaded then cancel 'paste' event,
// load them and when they'll get loaded fire new paste event
// for which data will be filtered in second execution of
// this listener.
var isLazyLoad = loadFilterRules( editor, path, function() {
// Event continuation with the original data.
if ( isLazyLoad ) {
editor.fire( 'paste', data );
} else if ( !editor.config.pasteFromWordPromptCleanup || ( forceFromWord || confirm( editor.lang.pastefromword.confirmCleanup ) ) ) {
pfwEvtData.dataValue = CKEDITOR.cleanWord( pfwEvtData.dataValue, editor );
editor.fire( 'afterPasteFromWord', pfwEvtData );
data.dataValue = pfwEvtData.dataValue;
if ( editor.config.forcePasteAsPlainText === true ) {
// If `config.forcePasteAsPlainText` set to true, force plain text even on Word content (#1013).
data.type = 'text';
} else if ( !CKEDITOR.plugins.clipboard.isCustomCopyCutSupported && editor.config.forcePasteAsPlainText === 'allow-word' ) {
// In browsers using pastebin when pasting from Word, evt.data.type is 'auto' (not 'html') so it gets converted
// by 'pastetext' plugin to 'text'. We need to restore 'html' type (#1013) and (#1638).
data.type = 'html';
}
}
// Reset forceFromWord.
forceFromWord = 0;
} );
// The cleanup rules are to be loaded, we should just cancel
// this event.
isLazyLoad && evt.cancel();
}, null, null, 3 );
// Paste From Word Image:
// RTF clipboard is required for embedding images.
// If img tags are not allowed there is no point to process images.
if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported && configInlineImages ) {
editor.on( 'afterPasteFromWord', imagePastingListener );
}
function imagePastingListener( evt ) {
var pfw = CKEDITOR.plugins.pastefromword && CKEDITOR.plugins.pastefromword.images,
imgTags,
hexImages,
newSrcValues = [],
i;
// If pfw images namespace is unavailable or img tags are not allowed we simply skip adding images.
if ( !pfw || !evt.editor.filter.check( 'img[src]' ) ) {
return;
}
function createSrcWithBase64( img ) {
return img.type ? 'data:' + img.type + ';base64,' + CKEDITOR.tools.convertBytesToBase64( CKEDITOR.tools.convertHexStringToBytes( img.hex ) ) : null;
}
imgTags = pfw.extractTagsFromHtml( evt.data.dataValue );
if ( imgTags.length === 0 ) {
return;
}
hexImages = pfw.extractFromRtf( evt.data.dataTransfer[ 'text/rtf' ] );
if ( hexImages.length === 0 ) {
return;
}
CKEDITOR.tools.array.forEach( hexImages, function( img ) {
newSrcValues.push( createSrcWithBase64( img ) );
}, this );
// Assuming there is equal amount of Images in RTF and HTML source, so we can match them accordingly to the existing order.
if ( imgTags.length === newSrcValues.length ) {
for ( i = 0; i < imgTags.length; i++ ) {
// Replace only `file` urls of images ( shapes get newSrcValue with null ).
if ( ( imgTags[ i ].indexOf( 'file://' ) === 0 ) && newSrcValues[ i ] ) {
evt.data.dataValue = evt.data.dataValue.replace( imgTags[ i ], newSrcValues[ i ] );
}
}
}
}
}
} );
function loadFilterRules( editor, path, callback ) {
var isLoaded = CKEDITOR.cleanWord;
if ( isLoaded )
callback();
else {
var filterFilePath = CKEDITOR.getUrl( editor.config.pasteFromWordCleanupFile || ( path + 'filter/default.js' ) );
// Load with busy indicator.
CKEDITOR.scriptLoader.load( filterFilePath, callback, null, true );
}
return !isLoaded;
}
} )();
/**
* Whether to prompt the user about the clean up of content being pasted from Microsoft Word.
*
* config.pasteFromWordPromptCleanup = true;
*
* @since 3.1.0
* @cfg {Boolean} [pasteFromWordPromptCleanup=false]
* @member CKEDITOR.config
*/
/**
* The file that provides the Microsoft Word cleanup function for pasting operations.
*
* **Note:** This is a global configuration shared by all editor instances present
* on the page.
*
* // Load from the 'pastefromword' plugin 'filter' sub folder (custom.js file) using a path relative to the CKEditor installation folder.
* CKEDITOR.config.pasteFromWordCleanupFile = 'plugins/pastefromword/filter/custom.js';
*
* // Load from the 'pastefromword' plugin 'filter' sub folder (custom.js file) using a full path (including the CKEditor installation folder).
* CKEDITOR.config.pasteFromWordCleanupFile = '/ckeditor/plugins/pastefromword/filter/custom.js';
*
* // Load custom.js file from the 'customFilters' folder (located in server's root) using the full URL.
* CKEDITOR.config.pasteFromWordCleanupFile = 'http://my.example.com/customFilters/custom.js';
*
* @since 3.1.0
* @cfg {String} [pasteFromWordCleanupFile= + 'filter/default.js']
* @member CKEDITOR.config
*/
/**
* Flag decides whether embedding images pasted with Word content is enabled or not.
*
* **Note:** Please be aware that embedding images requires Clipboard API support, available only in modern browsers, that is indicated by
* {@link CKEDITOR.plugins.clipboard#isCustomDataTypesSupported} flag.
*
* // Disable embedding images pasted from Word.
* config.pasteFromWord_inlineImages = false;
*
* @since 4.8.0
* @cfg {Boolean} [pasteFromWord_inlineImages=true]
* @member CKEDITOR.config
*/
/**
* Whether pasted element `margin` style that equals to 0 should be removed.
*
* // Disable removing `margin:0`, `margin-left:0`, etc.
* config.pasteFromWord_keepZeroMargins = true;
*
* @since 4.12.0
* @cfg {Boolean} [pasteFromWord_keepZeroMargins=false]
* @member CKEDITOR.config
*/
/**
* Fired when the pasted content was recognized as Microsoft Word content.
*
* This event is cancellable. If canceled, it will prevent Paste from Word processing.
*
* @since 4.6.0
* @event pasteFromWord
* @param data
* @param {String} data.dataValue Pasted content. Changes to this property will affect the pasted content.
* @member CKEDITOR.editor
*/
/**
* Fired after the Paste form Word filters have been applied.
*
* @since 4.6.0
* @event afterPasteFromWord
* @param data
* @param {String} data.dataValue Pasted content after processing. Changes to this property will affect the pasted content.
* @member CKEDITOR.editor
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview The Paste as plain text plugin.
*/
( function() {
// The pastetext command definition.
var pasteTextCmd = {
// Snapshots are done manually by editable.insertXXX methods.
canUndo: false,
async: true,
/**
* The Paste as plain text command. It will determine its pasted text automatically if possible.
*
* At the time of writing it was working correctly only in Internet Explorer browsers, due to their
* `paste` support in `document.execCommand`.
*
* @private
* @param {CKEDITOR.editor} editor An instance of the editor where the command is being executed.
* @param {Object} [data] The options object.
* @param {Boolean/String} [data.notification=true] Content for a notification shown after an unsuccessful
* paste attempt. If `false`, the notification will not be displayed. This parameter was added in 4.7.0.
* @member CKEDITOR.editor.commands.pastetext
*/
exec: function( editor, data ) {
var lang = editor.lang,
// In IE we must display keystroke for `paste` command as blocked `pastetext`
// can fallback only to native paste.
keyInfo = CKEDITOR.tools.keystrokeToString( lang.common.keyboard,
editor.getCommandKeystroke( CKEDITOR.env.ie ? editor.commands.paste : this ) ),
notification = ( data && typeof data.notification !== 'undefined' ) ? data.notification :
!data || !data.from || ( data.from === 'keystrokeHandler' && CKEDITOR.env.ie ),
msg = ( notification && typeof notification === 'string' ) ? notification : lang.pastetext.pasteNotification
.replace( /%1/, '' + keyInfo.display + ' ' );
editor.execCommand( 'paste', {
type: 'text',
notification: notification ? msg : false
} );
}
};
// Register the plugin.
CKEDITOR.plugins.add( 'pastetext', {
requires: 'clipboard',
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
var commandName = 'pastetext',
pasteKeystroke = !CKEDITOR.env.safari ? CKEDITOR.CTRL + CKEDITOR.SHIFT + 86 : // Ctrl + Shift + V
CKEDITOR.CTRL + CKEDITOR.ALT + CKEDITOR.SHIFT + 86; // Ctrl + Shift + Alt + V
editor.addCommand( commandName, pasteTextCmd );
editor.setKeystroke( pasteKeystroke, commandName );
CKEDITOR.plugins.clipboard.addPasteButton( editor, 'PasteText', {
label: editor.lang.pastetext.button,
command: commandName,
toolbar: 'clipboard,40'
} );
if ( editor.config.forcePasteAsPlainText ) {
editor.on( 'beforePaste', function( evt ) {
// Do NOT overwrite if HTML format is explicitly requested.
// This allows pastefromword dominates over pastetext.
if ( evt.data.type != 'html' ) {
evt.data.type = 'text';
}
} );
}
editor.on( 'pasteState', function( evt ) {
editor.getCommand( commandName ).setState( evt.data );
} );
}
} );
} )();
/**
* Whether to force all pasting operations to insert plain text into the
* editor, losing any formatting information possibly available in the source text.
*
* This option accepts the following settings:
*
* * `true` – Pastes all content as plain text.
* * `false` – Preserves content formatting.
* * `allow-word` – Content pasted from Microsoft Word will keep its formatting
* while any other content will be pasted as plain text.
*
* Example:
*
* // All content will be pasted as plain text.
* config.forcePasteAsPlainText = true;
*
* // Only Microsoft Word content formatting will be preserved.
* config.forcePasteAsPlainText = 'allow-word';
*
* @cfg {Boolean/String} [forcePasteAsPlainText=false]
* @member CKEDITOR.config
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Print Plugin
*/
CKEDITOR.plugins.add( 'print', {
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
// Print plugin isn't available in inline mode yet.
if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE )
return;
var pluginName = 'print';
// Register the command.
editor.addCommand( pluginName, CKEDITOR.plugins.print );
// Register the toolbar button.
editor.ui.addButton && editor.ui.addButton( 'Print', {
label: editor.lang.print.toolbar,
command: pluginName,
toolbar: 'document,50'
} );
}
} );
CKEDITOR.plugins.print = {
exec: function( editor ) {
if ( CKEDITOR.env.gecko ) {
editor.window.$.print();
} else {
editor.document.$.execCommand( 'Print' );
}
},
canUndo: false,
readOnly: 1,
modes: { wysiwyg: 1 }
};
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
CKEDITOR.plugins.add( 'removeformat', {
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
editor.addCommand( 'removeFormat', CKEDITOR.plugins.removeformat.commands.removeformat );
editor.ui.addButton && editor.ui.addButton( 'RemoveFormat', {
label: editor.lang.removeformat.toolbar,
command: 'removeFormat',
toolbar: 'cleanup,10'
} );
}
} );
CKEDITOR.plugins.removeformat = {
commands: {
removeformat: {
exec: function( editor ) {
var tagsRegex = editor._.removeFormatRegex || ( editor._.removeFormatRegex = new RegExp( '^(?:' + editor.config.removeFormatTags.replace( /,/g, '|' ) + ')$', 'i' ) );
var removeAttributes = editor._.removeAttributes || ( editor._.removeAttributes = editor.config.removeFormatAttributes.split( ',' ) ),
filter = CKEDITOR.plugins.removeformat.filter,
ranges = editor.getSelection().getRanges(),
iterator = ranges.createIterator(),
isElement = function( element ) {
return element.type == CKEDITOR.NODE_ELEMENT;
},
newRanges = [],
range;
while ( ( range = iterator.getNextRange() ) ) {
var bookmarkForRangeRecreation = range.createBookmark();
range = editor.createRange();
range.setStartBefore( bookmarkForRangeRecreation.startNode );
bookmarkForRangeRecreation.endNode && range.setEndAfter( bookmarkForRangeRecreation.endNode );
if ( !range.collapsed )
range.enlarge( CKEDITOR.ENLARGE_ELEMENT );
// Bookmark the range so we can re-select it after processing.
var bookmark = range.createBookmark(),
// The style will be applied within the bookmark boundaries.
startNode = bookmark.startNode,
endNode = bookmark.endNode,
currentNode;
// We need to check the selection boundaries (bookmark spans) to break
// the code in a way that we can properly remove partially selected nodes.
// For example, removing a style from
// This is [some text to show the] problem
// ... where [ and ] represent the selection, must result:
// This is [some text to show the] problem
// The strategy is simple, we just break the partial nodes before the
// removal logic, having something that could be represented this way:
// This is [some text to show the ] problem
var breakParent = function( node ) {
// Let's start checking the start boundary.
var path = editor.elementPath( node ),
pathElements = path.elements;
for ( var i = 1, pathElement; pathElement = pathElements[ i ]; i++ ) {
if ( pathElement.equals( path.block ) || pathElement.equals( path.blockLimit ) )
break;
// If this element can be removed (even partially).
if ( tagsRegex.test( pathElement.getName() ) && filter( editor, pathElement ) )
node.breakParent( pathElement );
}
};
breakParent( startNode );
if ( endNode ) {
breakParent( endNode );
// Navigate through all nodes between the bookmarks.
currentNode = startNode.getNextSourceNode( true, CKEDITOR.NODE_ELEMENT );
while ( currentNode ) {
// If we have reached the end of the selection, stop looping.
if ( currentNode.equals( endNode ) )
break;
if ( currentNode.isReadOnly() ) {
// In case of non-editable we're skipping to the next sibling *elmenet*.
// We need to be aware that endNode can be nested within current non-editable.
// This condition tests if currentNode (non-editable) contains endNode. If it does
// then we should break the filtering
if ( currentNode.getPosition( endNode ) & CKEDITOR.POSITION_CONTAINS ) {
break;
}
currentNode = currentNode.getNext( isElement );
continue;
}
// Cache the next node to be processed. Do it now, because
// currentNode may be removed.
var nextNode = currentNode.getNextSourceNode( false, CKEDITOR.NODE_ELEMENT ),
isFakeElement =
( currentNode.getName() == 'img' && currentNode.data( 'cke-realelement' ) ) ||
currentNode.hasAttribute( 'data-cke-bookmark' );
// This node must not be a fake element, and must not be read-only.
if ( !isFakeElement && filter( editor, currentNode ) ) {
// Remove elements nodes that match with this style rules.
if ( tagsRegex.test( currentNode.getName() ) )
currentNode.remove( 1 );
else {
currentNode.removeAttributes( removeAttributes );
editor.fire( 'removeFormatCleanup', currentNode );
}
}
currentNode = nextNode;
}
}
bookmark.startNode.remove();
bookmark.endNode && bookmark.endNode.remove();
range.moveToBookmark( bookmarkForRangeRecreation );
newRanges.push( range );
}
// The selection path may not changed, but we should force a selection
// change event to refresh command states, due to the above attribution change. (https://dev.ckeditor.com/ticket/9238)
editor.forceNextSelectionCheck();
editor.getSelection().selectRanges( newRanges );
}
}
},
// Perform the remove format filters on the passed element.
// @param {CKEDITOR.editor} editor
// @param {CKEDITOR.dom.element} element
filter: function( editor, element ) {
// If editor#addRemoveFotmatFilter hasn't been executed yet value is not initialized.
var filters = editor._.removeFormatFilters || [];
for ( var i = 0; i < filters.length; i++ ) {
if ( filters[ i ]( element ) === false )
return false;
}
return true;
}
};
/**
* Add to a collection of functions to decide whether a specific
* element should be considered as formatting element and thus
* could be removed during `removeFormat` command.
*
* **Note:** Only available with the existence of `removeformat` plugin.
*
* // Don't remove empty span.
* editor.addRemoveFormatFilter( function( element ) {
* return !( element.is( 'span' ) && CKEDITOR.tools.isEmpty( element.getAttributes() ) );
* } );
*
* @since 3.3.0
* @member CKEDITOR.editor
* @param {Function} func The function to be called, which will be passed an {@link CKEDITOR.dom.element element} to test.
*/
CKEDITOR.editor.prototype.addRemoveFormatFilter = function( func ) {
if ( !this._.removeFormatFilters )
this._.removeFormatFilters = [];
this._.removeFormatFilters.push( func );
};
/**
* A comma separated list of elements to be removed when executing the `remove
* format` command. Note that only inline elements are allowed.
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.removeFormatTags = 'b,big,cite,code,del,dfn,em,font,i,ins,kbd,q,s,samp,small,span,strike,strong,sub,sup,tt,u,var';
/**
* A comma separated list of elements attributes to be removed when executing
* the `remove format` command.
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.removeFormatAttributes = 'class,style,lang,width,height,align,hspace,valign';
/**
* Fired after an element was cleaned by the removeFormat plugin.
*
* @event removeFormatCleanup
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {CKEDITOR.dom.element} data.element The element that was cleaned up.
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
CKEDITOR.plugins.add( 'resize', {
init: function( editor ) {
function dragHandler( evt ) {
var dx = evt.data.$.screenX - origin.x,
dy = evt.data.$.screenY - origin.y,
width = startSize.width,
height = startSize.height,
internalWidth = width + dx * ( resizeDir == 'rtl' ? -1 : 1 ),
internalHeight = height + dy;
if ( resizeHorizontal )
width = Math.max( config.resize_minWidth, Math.min( internalWidth, config.resize_maxWidth ) );
if ( resizeVertical )
height = Math.max( config.resize_minHeight, Math.min( internalHeight, config.resize_maxHeight ) );
// DO NOT impose fixed size with single direction resize. (https://dev.ckeditor.com/ticket/6308)
editor.resize( resizeHorizontal ? width : null, height );
}
function dragEndHandler() {
CKEDITOR.document.removeListener( 'mousemove', dragHandler );
CKEDITOR.document.removeListener( 'mouseup', dragEndHandler );
if ( editor.document ) {
editor.document.removeListener( 'mousemove', dragHandler );
editor.document.removeListener( 'mouseup', dragEndHandler );
}
}
var config = editor.config;
var spaceId = editor.ui.spaceId( 'resizer' );
// Resize in the same direction of chrome,
// which is identical to dir of editor element. (https://dev.ckeditor.com/ticket/6614)
var resizeDir = editor.element ? editor.element.getDirection( 1 ) : 'ltr';
!config.resize_dir && ( config.resize_dir = 'vertical' );
( config.resize_maxWidth === undefined ) && ( config.resize_maxWidth = 3000 );
( config.resize_maxHeight === undefined ) && ( config.resize_maxHeight = 3000 );
( config.resize_minWidth === undefined ) && ( config.resize_minWidth = 750 );
( config.resize_minHeight === undefined ) && ( config.resize_minHeight = 250 );
if ( config.resize_enabled !== false ) {
var container = null,
origin, startSize,
resizeHorizontal = ( config.resize_dir == 'both' || config.resize_dir == 'horizontal' ) && ( config.resize_minWidth != config.resize_maxWidth ),
resizeVertical = ( config.resize_dir == 'both' || config.resize_dir == 'vertical' ) && ( config.resize_minHeight != config.resize_maxHeight );
var mouseDownFn = CKEDITOR.tools.addFunction( function( $event ) {
if ( !container )
container = editor.getResizable();
startSize = { width: container.$.offsetWidth || 0, height: container.$.offsetHeight || 0 };
origin = { x: $event.screenX, y: $event.screenY };
config.resize_minWidth > startSize.width && ( config.resize_minWidth = startSize.width );
config.resize_minHeight > startSize.height && ( config.resize_minHeight = startSize.height );
CKEDITOR.document.on( 'mousemove', dragHandler );
CKEDITOR.document.on( 'mouseup', dragEndHandler );
if ( editor.document ) {
editor.document.on( 'mousemove', dragHandler );
editor.document.on( 'mouseup', dragEndHandler );
}
$event.preventDefault && $event.preventDefault();
} );
editor.on( 'destroy', function() {
CKEDITOR.tools.removeFunction( mouseDownFn );
} );
editor.on( 'uiSpace', function( event ) {
if ( event.data.space == 'bottom' ) {
var direction = '';
if ( resizeHorizontal && !resizeVertical )
direction = ' cke_resizer_horizontal';
if ( !resizeHorizontal && resizeVertical )
direction = ' cke_resizer_vertical';
var resizerHtml =
'' +
// BLACK LOWER RIGHT TRIANGLE (ltr)
// BLACK LOWER LEFT TRIANGLE (rtl)
( resizeDir == 'ltr' ? '\u25E2' : '\u25E3' ) +
' ';
// Always sticks the corner of botttom space.
resizeDir == 'ltr' && direction == 'ltr' ? event.data.html += resizerHtml : event.data.html = resizerHtml + event.data.html;
}
}, editor, null, 100 );
// Toggle the visibility of the resizer when an editor is being maximized or minimized.
editor.on( 'maximize', function( event ) {
editor.ui.space( 'resizer' )[ event.data == CKEDITOR.TRISTATE_ON ? 'hide' : 'show' ]();
} );
}
}
} );
/**
* The minimum editor width, in pixels, when resizing the editor interface by using the resize handle.
* Note: It falls back to editor's actual width if it is smaller than the default value.
*
* Read more in the {@glink features/resize documentation}
* and see the {@glink examples/resize example}.
*
* config.resize_minWidth = 500;
*
* @cfg {Number} [resize_minWidth=750]
* @member CKEDITOR.config
*/
/**
* The minimum editor height, in pixels, when resizing the editor interface by using the resize handle.
* Note: It falls back to editor's actual height if it is smaller than the default value.
*
* Read more in the {@glink features/resize documentation}
* and see the {@glink examples/resize example}.
*
* config.resize_minHeight = 600;
*
* @cfg {Number} [resize_minHeight=250]
* @member CKEDITOR.config
*/
/**
* The maximum editor width, in pixels, when resizing the editor interface by using the resize handle.
*
* Read more in the {@glink features/resize documentation}
* and see the {@glink examples/resize example}.
*
* config.resize_maxWidth = 750;
*
* @cfg {Number} [resize_maxWidth=3000]
* @member CKEDITOR.config
*/
/**
* The maximum editor height, in pixels, when resizing the editor interface by using the resize handle.
*
* Read more in the {@glink features/resize documentation}
* and see the {@glink examples/resize example}.
*
* config.resize_maxHeight = 600;
*
* @cfg {Number} [resize_maxHeight=3000]
* @member CKEDITOR.config
*/
/**
* Whether to enable the resizing feature. If this feature is disabled, the resize handle will not be visible.
*
* Read more in the {@glink features/resize documentation}
* and see the {@glink examples/resize example}.
*
* config.resize_enabled = false;
*
* @cfg {Boolean} [resize_enabled=true]
* @member CKEDITOR.config
*/
/**
* The dimensions for which the editor resizing is enabled. Possible values
* are `both`, `vertical`, and `horizontal`.
*
* Read more in the {@glink features/resize documentation}
* and see the {@glink examples/resize example}.
*
* config.resize_dir = 'both';
*
* @since 3.3.0
* @cfg {String} [resize_dir='vertical']
* @member CKEDITOR.config
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview The Source Editing Area plugin. It registers the "source" editing
* mode, which displays raw HTML data being edited in the editor.
*/
( function() {
CKEDITOR.plugins.add( 'sourcearea', {
// jscs:disable maximumLineLength
// jscs:enable maximumLineLength
init: function( editor ) {
// Source mode in inline editors is only available through the "sourcedialog" plugin.
if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE )
return;
var sourcearea = CKEDITOR.plugins.sourcearea;
editor.addMode( 'source', function( callback ) {
var contentsSpace = editor.ui.space( 'contents' ),
textarea = contentsSpace.getDocument().createElement( 'textarea' );
textarea.setStyles(
CKEDITOR.tools.extend( {
// IE7 has overflow the
, 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: 1, th: 1 }, true );
if ( nearestCell && isInTable( 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: 1, th: 1 }, true );
if ( parent && !parent.getCustomData( 'selected_cell' ) && isInTable( parent ) ) {
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( selectionOrCells, insertBefore ) {
var cells = isArray( selectionOrCells ) ? selectionOrCells : getSelectedCells( selectionOrCells ),
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 );
return newRow;
}
function deleteRows( selectionOrRow ) {
if ( selectionOrRow instanceof CKEDITOR.dom.selection ) {
var ranges = selectionOrRow.getRanges(),
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 = [];
selectionOrRow.reset();
// 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;
// After deleting whole table, the selection would be broken,
// therefore it's safer to move it outside the table first.
ranges[ 0 ].moveToPosition( table, CKEDITOR.POSITION_BEFORE_START );
// 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 ] );
}
// If all the rows were removed, table gets removed too.
if ( !table.$.parentNode ) {
ranges[ 0 ].select();
return null;
}
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 ) {
var row = cell.getParent(),
rowCells = row.$.cells;
var colIndex = 0;
for ( var i = 0; i < rowCells.length; i++ ) {
var mapCell = rowCells[ i ];
// Not always adding colSpan results in wrong position
// of newly inserted column. (#591) (https://dev.ckeditor.com/ticket/13729)
colIndex += 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 ] );
if ( isStart ? colIndex < retval : colIndex > retval )
retval = colIndex;
}
return retval;
}
function insertColumn( selectionOrCells, insertBefore ) {
var cells = isArray( selectionOrCells ) ? selectionOrCells : getSelectedCells( selectionOrCells ),
firstCell = cells[ 0 ],
table = firstCell.getAscendant( 'table' ),
startCol = getColumnsIndices( cells, 1 ),
lastCol = getColumnsIndices( cells ),
colIndex = insertBefore ? startCol : lastCol,
originalCell;
var map = CKEDITOR.tools.buildTableMap( table ),
cloneCol = [],
nextCol = [],
addedCells = [],
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 {
originalCell = new CKEDITOR.dom.element( cloneCol[ i ] );
cell = originalCell.clone();
cell.removeAttribute( 'colSpan' );
cell.appendBogus();
cell[ insertBefore ? 'insertBefore' : 'insertAfter' ].call( cell, originalCell );
addedCells.push( cell );
cell = cell.$;
}
i += cell.rowSpan - 1;
}
return addedCells;
}
function deleteColumns( selection ) {
function processSelection( selection ) {
// If selection leak to next td/th cell, then preserve it in previous cell.
var ranges,
range,
endNode,
endNodeName,
previous;
ranges = selection.getRanges();
if ( ranges.length !== 1 ) {
return selection;
}
range = ranges[0];
if ( range.collapsed || range.endOffset !== 0 ) {
return selection;
}
endNode = range.endContainer;
endNodeName = endNode.getName().toLowerCase();
if ( !( endNodeName === 'td' || endNodeName === 'th' ) ) {
return selection;
}
// Get previous td/th element or the last from previous row.
previous = endNode.getPrevious();
if ( !previous ) {
previous = endNode.getParent().getPrevious().getLast();
}
// Get most inner text node or br in case of empty cell.
while ( previous.type !== CKEDITOR.NODE_TEXT && previous.getName().toLowerCase() !== 'br' ) {
previous = previous.getLast();
// Generraly previous should never be null, if statement is just for possible weird edge cases.
if ( !previous ) {
return selection;
}
}
range.setEndAt( previous, CKEDITOR.POSITION_BEFORE_END );
return range.select();
}
// Problem occures only on webkit in case of native selection (#577).
// Upstream: https://bugs.webkit.org/show_bug.cgi?id=175131, https://bugs.chromium.org/p/chromium/issues/detail?id=752091
if ( CKEDITOR.env.webkit && !selection.isFake ) {
selection = processSelection( selection );
}
var ranges = selection.getRanges(),
cells = getSelectedCells( selection ),
firstCell = cells[ 0 ],
lastCell = cells[ cells.length - 1 ],
table = firstCell.getAscendant( 'table' ),
map = CKEDITOR.tools.buildTableMap( table ),
startColIndex, endColIndex,
rowsToDelete = [];
selection.reset();
// 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++ ) {
// #577
// Map might contain multiple times this same element, because of existings collspan.
// We don't want to overwrite startIndex in such situation and take first one.
if ( startColIndex === undefined && 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();
} else {
// Reduce the col spans.
cell.$.colSpan -= 1;
}
j += cell.$.rowSpan - 1;
if ( !row.$.cells.length ) {
rowsToDelete.push( row );
}
}
}
}
// 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;
if ( map[ 0 ].length - 1 > endColIndex ) {
cursorPosition = new CKEDITOR.dom.element( map[ 0 ][ endColIndex + 1 ] );
} else if ( startColIndex && map[ 0 ][ startColIndex - 1 ].cellIndex !== -1 ) {
cursorPosition = new CKEDITOR.dom.element( map[ 0 ][ startColIndex - 1 ] );
} else {
cursorPosition = new CKEDITOR.dom.element( table.$.parentNode );
}
// Delete table rows only if all columns are gone (do not remove empty row).
if ( rowsToDelete.length == rows ) {
// After deleting whole table, the selection would be broken,
// therefore it's safer to move it outside the table first.
ranges[ 0 ].moveToPosition( table, CKEDITOR.POSITION_AFTER_END );
ranges[ 0 ].select();
table.remove();
}
return cursorPosition;
}
function insertCell( selection, insertBefore ) {
var startElement = selection.getStartElement(),
cell = startElement.getAscendant( { td: 1, th: 1 }, true );
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 ranges = selectionOrCell.getRanges(),
cellsToDelete = getSelectedCells( selectionOrCell ),
table = cellsToDelete[ 0 ] && cellsToDelete[ 0 ].getAscendant( 'table' ),
cellToFocus = getFocusElementAfterDelCells( cellsToDelete );
selectionOrCell.reset();
for ( var i = cellsToDelete.length - 1; i >= 0; i-- ) {
deleteCells( cellsToDelete[ i ] );
}
if ( cellToFocus ) {
placeCursorInCell( cellToFocus, true );
} else if ( table ) {
// After deleting whole table, the selection would be broken,
// therefore it's safer to move it outside the table first.
ranges[ 0 ].moveToPosition( table, CKEDITOR.POSITION_BEFORE_START );
ranges[ 0 ].select();
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 (https://dev.ckeditor.com/ticket/10308).
// We can not apply this hack to IE8 because
// it causes error (https://dev.ckeditor.com/ticket/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,
styleParse = CKEDITOR.tools.style.parse,
requiredContent = [
'td{width}', 'td{height}', 'td{border-color}', 'td{background-color}', 'td{white-space}', 'td{vertical-align}', 'td{text-align}',
'td[colspan]', 'td[rowspan]', 'th' ];
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: requiredContent,
contentTransformations: [ [ {
element: 'td',
left: function( element ) {
return element.styles.background && styleParse.background( element.styles.background ).color;
},
right: function( element ) {
element.styles[ 'background-color' ] = styleParse.background( element.styles.background ).color;
}
}, {
element: 'td',
check: 'td{vertical-align}',
left: function( element ) {
return element.attributes && element.attributes.valign;
},
right: function( element ) {
element.styles[ 'vertical-align' ] = element.attributes.valign;
delete element.attributes.valign;
}
}
], [
{
// (https://dev.ckeditor.com/ticket/16818)
element: 'tr',
check: 'td{height}',
left: function( element ) {
return element.styles && element.styles.height;
},
right: function( element ) {
CKEDITOR.tools.array.forEach( element.children, function( node ) {
if ( node.name in { td: 1, th: 1 } ) {
node.attributes[ 'cke-row-height' ] = element.styles.height;
}
} );
delete element.styles.height;
}
}
], [
{
// (https://dev.ckeditor.com/ticket/16818)
element: 'td',
check: 'td{height}',
left: function( element ) {
var attributes = element.attributes;
return attributes && attributes[ 'cke-row-height' ];
},
right: function( element ) {
element.styles.height = element.attributes[ 'cke-row-height' ];
delete element.attributes[ 'cke-row-height' ];
}
}
] ]
} ) ) );
CKEDITOR.dialog.add( 'cellProperties', this.path + 'dialogs/tableCell.js' );
addCmd( 'rowDelete', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection(),
cursorPosition = deleteRows( selection );
if ( cursorPosition ) {
placeCursorInCell( cursorPosition );
}
}
} ) );
addCmd( 'rowInsertBefore', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection(),
cells = getSelectedCells( selection );
insertRow( cells, true );
}
} ) );
addCmd( 'rowInsertAfter', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection(),
cells = getSelectedCells( selection );
insertRow( cells );
}
} ) );
addCmd( 'columnDelete', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection();
var element = deleteColumns( selection );
if ( element ) {
placeCursorInCell( element, true );
}
}
} ) );
addCmd( 'columnInsertBefore', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection(),
cells = getSelectedCells( selection );
insertColumn( cells, true );
}
} ) );
addCmd( 'columnInsertAfter', createDef( {
requiredContent: 'table',
exec: function( editor ) {
var selection = editor.getSelection(),
cells = getSelectedCells( selection );
insertColumn( cells );
}
} ) );
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, data ) {
data.cell = mergeCells( editor.getSelection() );
placeCursorInCell( data.cell, true );
}
} ) );
addCmd( 'cellMergeRight', createDef( {
allowedContent: 'td[colspan]',
requiredContent: 'td[colspan]',
exec: function( editor, data ) {
data.cell = mergeCells( editor.getSelection(), 'right' );
placeCursorInCell( data.cell, true );
}
} ) );
addCmd( 'cellMergeDown', createDef( {
allowedContent: 'td[rowspan]',
requiredContent: 'td[rowspan]',
exec: function( editor, data ) {
data.cell = mergeCells( editor.getSelection(), 'down' );
placeCursorInCell( data.cell, 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 ),
items = {
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
};
if ( editor.filter.check( requiredContent ) ) {
items.tablecell_properties = cells.length > 0 ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED;
}
return items;
}
},
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;
} );
}
},
// These methods are needed by tableselection plugin, so we must expose them.
getCellColIndex: getCellColIndex,
insertRow: insertRow,
insertColumn: insertColumn,
getSelectedCells: getSelectedCells
};
CKEDITOR.plugins.add( 'tabletools', CKEDITOR.plugins.tabletools );
} )();
/**
* Creates a two-dimension array that reflects the actual layout of table cells,
* with cell spans, with mappings to the original `td` elements.
*
* It could also create a map for the specified fragment of the table.
*
* @param {CKEDITOR.dom.element} table
* @param {Number} startRow Row index from which the map should be created.
* @param {Number} startCell Cell index from which the map should be created.
* @param {Number} endRow Row index to which the map should be created.
* @param {Number} endCell Cell index to which the map should be created.
* @member CKEDITOR.tools
*/
CKEDITOR.tools.buildTableMap = function( table, startRow, startCell, endRow, endCell ) {
var aRows = table.$.rows;
startRow = startRow || 0;
startCell = startCell || 0;
endRow = typeof endRow === 'number' ? endRow : aRows.length - 1;
endCell = typeof endCell === 'number' ? endCell : -1;
// Row and Column counters.
var r = -1;
var aMap = [];
for ( var i = startRow; i <= endRow; i++ ) {
r++;
!aMap[ r ] && ( aMap[ r ] = [] );
var c = -1;
for ( var j = startCell; j <= ( endCell === -1 ? ( aRows[ i ].cells.length - 1 ) : endCell ); j++ ) {
var oCell = aRows[ i ].cells[ j ];
if ( !oCell ) {
break;
}
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 ( i + rs > endRow ) {
break;
}
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;
if ( endCell !== -1 && c >= endCell ) {
break;
}
}
}
return aMap;
};
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
( function() {
CKEDITOR.plugins.add( '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'
* ];
*
* For a sample template file
* [see `templates/default.js`](https://github.com/ckeditor/ckeditor-dev/blob/master/plugins/templates/templates/default.js).
*
* @cfg {String[]}
* @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 {Boolean}
* @member CKEDITOR.config
*/
CKEDITOR.config.templates_replaceContent = true;
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview Undo/Redo system for saving a snapshot 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 recorded (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.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.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 iteration 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. (https://dev.ckeditor.com/ticket/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 actually content may differ from
// the original snapshot due to dom change. (https://dev.ckeditor.com/ticket/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.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.
// https://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.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.(https://dev.ckeditor.com/ticket/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 occurred.
// 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. (https://dev.ckeditor.com/ticket/12597)
if ( keyCode === 229 ) {
return;
}
// Block undo/redo keystrokes when at the bottom/top of the undo stack (https://dev.ckeditor.com/ticket/11126 and https://dev.ckeditor.com/ticket/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.
// https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/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;
},
/**
* Stops ignoring `input` events.
* @since 4.7.3
*/
activateInputEventListener: function() {
this.ignoreInputEvent = false;
},
/**
* 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 );
// After paste we need to re-enable input event listener (#554).
editor.on( 'afterPaste', that.activateInputEventListener, 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.
// https://dev.ckeditor.com/ticket/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.0
* @event change
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
*/
/**
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview The WYSIWYG Area plugin. It registers the "wysiwyg" editing
* mode, which handles the main editing area space.
*/
( function() {
var framedWysiwyg;
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.
// Microsoft Edge throws "Permission Denied" if treated like an IE (https://dev.ckeditor.com/ticket/13441).
if ( CKEDITOR.env.air ) {
src = 'javascript:void(0)'; // jshint ignore:line
} else if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) {
src = 'javascript:void(function(){' + encodeURIComponent( src ) + '}())'; // jshint ignore:line
} else {
src = '';
}
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.edge ) || 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();
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.0
* @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. (https://dev.ckeditor.com/ticket/141 / https://dev.ckeditor.com/ticket/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();
var editable = this;
// Without it IE8 has problem with removing selection in nested editable. (https://dev.ckeditor.com/ticket/13785)
if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) {
doc.getDocumentElement().addClass( doc.$.compatMode );
}
// Prevent IE/Edge from leaving a new paragraph/div after deleting all contents in body (https://dev.ckeditor.com/ticket/6966, https://dev.ckeditor.com/ticket/13142).
if ( CKEDITOR.env.ie && !CKEDITOR.env.edge && editor.enterMode != CKEDITOR.ENTER_P ) {
removeSuperfluousElement( 'p' );
}
// Starting from Edge 15 additional `div` is not added to the editor.
else if ( CKEDITOR.env.edge && CKEDITOR.env.version < 15 && editor.enterMode != CKEDITOR.ENTER_DIV ) {
removeSuperfluousElement( 'div' );
}
// Fix problem with cursor not appearing in Webkit and IE11+ when clicking below the body (https://dev.ckeditor.com/ticket/10945, https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/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 (https://dev.ckeditor.com/ticket/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 );
function removeSuperfluousElement( tagName ) {
var lockRetain = false;
// Superfluous elements appear after keydown
// and before keyup, so the procedure is as follows:
// 1. On first keydown mark all elements with
// a specified tag name as non-superfluous.
editable.attachListener( editable, 'keydown', function() {
var body = doc.getBody(),
retained = body.getElementsByTag( tagName );
if ( !lockRetain ) {
for ( var i = 0; i < retained.count(); i++ ) {
retained.getItem( i ).setCustomData( 'retain', true );
}
lockRetain = true;
}
}, null, null, 1 );
// 2. On keyup remove all elements that were not marked
// as non-superfluous (which means they must have had appeared in the meantime).
// Also we should preserve all temporary elements inserted by editor – otherwise we'd likely
// leak fake selection's content into editable due to removing hidden selection container (https://dev.ckeditor.com/ticket/14831).
editable.attachListener( editable, 'keyup', function() {
var elements = doc.getElementsByTag( tagName );
if ( lockRetain ) {
if ( elements.count() == 1 && !elements.getItem( 0 ).getCustomData( 'retain' ) &&
CKEDITOR.tools.isEmpty( elements.getItem( 0 ).getAttributes() ) ) {
elements.getItem( 0 ).remove( 1 );
}
lockRetain = false;
}
} );
}
}
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.(https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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( /