Files
BoostrapPageGenerator4/4/ckeditor/plugins/copyformatting/plugin.js
Francesco Malagrino f1fdc89d7d Bootstrap version 4
Bootstrap Version 4
2018-02-06 19:17:39 +00:00

1247 lines
41 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
'use strict';
var indexOf = CKEDITOR.tools.indexOf,
// This flag prevents appending stylesheet more than once.
stylesLoaded = false;
// Detects if the left mouse button was pressed:
// * In all browsers and IE 9+ we use event.button property with standard compliant values.
// * In IE 8- we use event.button with IE's proprietary values.
function detectLeftMouseButton( evt ) {
var evtData = evt.data,
domEvent = evtData && evtData.$;
if ( !( evtData && domEvent ) ) {
// Added in case when there's no data available. That's the case in some unit test in built version which
// mock event but doesn't put data object.'
return false;
}
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
return domEvent.button === 1;
}
return domEvent.button === 0;
}
// Searches for given node in given query. It also checks ancestors of elements in the range.
function getNodeAndApplyCmd( range, query, cmd, stopOnFirst ) {
var walker = new CKEDITOR.dom.walker( range ),
currentNode;
// Walker sometimes does not include all nodes (e.g. if the range is in the middle of text node).
if ( ( currentNode = range.startContainer.getAscendant( query, true ) ||
range.endContainer.getAscendant( query, true ) ) ) {
cmd( currentNode );
if ( stopOnFirst ) {
return;
}
}
while ( currentNode = walker.next() ) {
currentNode = currentNode.getAscendant( query, true );
if ( currentNode ) {
cmd( currentNode );
if ( stopOnFirst ) {
return;
}
}
}
}
// Checks if there is style for specified element in the given array.
function checkForStyle( element, styles ) {
// Some elements are treated interchangeably, e.g. lists.
var stylesAlternatives = {
ul: 'ol',
ol: 'ul'
};
return indexOf( styles, function( style ) {
return style.element === element || style.element === stylesAlternatives[ element ];
} ) !== -1;
}
CKEDITOR.plugins.add( 'copyformatting', {
lang: 'en',
icons: 'copyformatting',
hidpi: true,
init: function( editor ) {
var plugin = CKEDITOR.plugins.copyformatting;
plugin._addScreenReaderContainer();
if ( !stylesLoaded ) {
CKEDITOR.document.appendStyleSheet( this.path + 'styles/copyformatting.css' );
stylesLoaded = true;
}
// Add copyformatting stylesheet.
if ( editor.addContentsCss ) {
editor.addContentsCss( this.path + 'styles/copyformatting.css' );
}
/**
* Current state of the Copy Formatting plugin in this editor instance.
*
* @since 4.6.0
* @property {CKEDITOR.plugins.copyformatting.state} copyFormatting
* @member CKEDITOR.editor
*/
editor.copyFormatting = new plugin.state( editor );
editor.addCommand( 'copyFormatting', plugin.commands.copyFormatting );
editor.addCommand( 'applyFormatting', plugin.commands.applyFormatting );
editor.ui.addButton( 'CopyFormatting', {
label: editor.lang.copyformatting.label,
command: 'copyFormatting',
toolbar: 'cleanup,0'
} );
editor.on( 'contentDom', function() {
var editable = editor.editable(),
// Host element for apply formatting click. In case of classic element it needs to be entire
// document, otherwise clicking in body margins would not trigger the event.
// Editors with divarea plugin enabled should be treated like inline one otherwise
// clicking the whole document messes the focus.
mouseupHost = editable.isInline() ? editable : editor.document,
copyFormattingButton = editor.ui.get( 'CopyFormatting' ),
copyFormattingButtonEl;
editable.attachListener( mouseupHost, 'mouseup', function( evt ) {
if ( detectLeftMouseButton( evt ) ) {
editor.execCommand( 'applyFormatting' );
}
} );
editable.attachListener( CKEDITOR.document, 'mouseup', function( evt ) {
var cmd = editor.getCommand( 'copyFormatting' );
if ( detectLeftMouseButton( evt ) && cmd.state === CKEDITOR.TRISTATE_ON &&
!editable.contains( evt.data.getTarget() ) ) {
editor.execCommand( 'copyFormatting' );
}
} );
if ( copyFormattingButton ) {
copyFormattingButtonEl = CKEDITOR.document.getById( copyFormattingButton._.id );
editable.attachListener( copyFormattingButtonEl, 'dblclick', function() {
editor.execCommand( 'copyFormatting', { sticky: true } );
} );
editable.attachListener( copyFormattingButtonEl, 'mouseup', function( evt ) {
evt.data.stopPropagation();
} );
}
} );
// Set customizable keystrokes.
if ( editor.config.copyFormatting_keystrokeCopy ) {
editor.setKeystroke( editor.config.copyFormatting_keystrokeCopy, 'copyFormatting' );
}
if ( editor.config.copyFormatting_keystrokePaste ) {
editor.setKeystroke( editor.config.copyFormatting_keystrokePaste, 'applyFormatting' );
}
editor.on( 'key', function( evt ) {
var cmd = editor.getCommand( 'copyFormatting' ),
domEvent = evt.data.domEvent;
// Esc should simply disable Copy Formatting. Make sure that getKeystroke is there, as some event stubs are missing it.
if ( domEvent.getKeystroke && domEvent.getKeystroke() === 27 ) { // ESC
if ( cmd.state === CKEDITOR.TRISTATE_ON ) {
editor.execCommand( 'copyFormatting' );
}
}
} );
// Fetch the styles from element.
editor.copyFormatting.on( 'extractFormatting', function( evt ) {
var element = evt.data.element,
style;
// Stop at body and html in classic editors or at .cke_editable element in inline ones.
if ( element.contains( editor.editable() ) || element.equals( editor.editable() ) ) {
return evt.cancel();
}
style = plugin._convertElementToStyleDef( element );
if ( !editor.copyFormatting.filter.check( new CKEDITOR.style( style ), true, true ) ) {
return evt.cancel();
}
evt.data.styleDef = style;
} );
// Remove old styles from element.
editor.copyFormatting.on( 'applyFormatting', function( evt ) {
if ( evt.data.preventFormatStripping ) {
return;
}
var range = evt.data.range,
oldStyles = plugin._extractStylesFromRange( editor, range ),
context = plugin._determineContext( range ),
oldStyle,
bkm,
i;
if ( !editor.copyFormatting._isContextAllowed( context ) ) {
return;
}
for ( i = 0; i < oldStyles.length; i++ ) {
oldStyle = oldStyles[ i ];
// The bookmark is used to prevent the weird behavior of lists (e.g. not converting list type
// while applying styles from bullet list to the numbered one). Restoring the selection to its
// initial state after every change seems to do the trick.
bkm = range.createBookmark();
if ( indexOf( plugin.preservedElements, oldStyle.element ) === -1 ) {
// In Safari we must remove styles exactly from the initial range.
// Otherwise Safari is removing too much.
if ( CKEDITOR.env.webkit && !CKEDITOR.env.chrome ) {
oldStyles[ i ].removeFromRange( evt.data.range, evt.editor );
} else {
oldStyles[ i ].remove( evt.editor );
}
} else if ( checkForStyle( oldStyle.element, evt.data.styles ) ) {
plugin._removeStylesFromElementInRange( range, oldStyle.element );
}
range.moveToBookmark( bkm );
}
} );
// Apply new styles.
editor.copyFormatting.on( 'applyFormatting', function( evt ) {
var plugin = CKEDITOR.plugins.copyformatting,
context = plugin._determineContext( evt.data.range );
if ( context === 'list' && editor.copyFormatting._isContextAllowed( 'list' ) ) {
plugin._applyStylesToListContext( evt.editor, evt.data.range, evt.data.styles );
} else if ( context === 'table' && editor.copyFormatting._isContextAllowed( 'table' ) ) {
plugin._applyStylesToTableContext( evt.editor, evt.data.range, evt.data.styles );
} else if ( editor.copyFormatting._isContextAllowed( 'text' ) ) {
plugin._applyStylesToTextContext( evt.editor, evt.data.range, evt.data.styles );
}
}, null, null, 999 );
}
} );
/**
* Copy Formatting state object created for each CKEditor instance.
*
* @class CKEDITOR.plugins.copyformatting.state
* @mixins CKEDITOR.event
* @constructor Creates a new state object.
* @param {CKEDITOR.editor} editor
*/
function State( editor ) {
/**
* Currently copied styles.
*
* @member CKEDITOR.plugins.copyformatting.state
* @property {CKEDITOR.style[]/null}
*/
this.styles = null;
/**
* Indicates if the Copy Formatting plugin is in sticky mode.
*
* @member CKEDITOR.plugins.copyformatting.state
* @property {Boolean}
*/
this.sticky = false;
/**
* Editor reference.
*
* @member CKEDITOR.plugins.copyformatting.state
* @property {CKEDITOR.editor}
*/
this.editor = editor;
/**
* Filter used by the current Copy Formatting instance.
*
* @member CKEDITOR.plugins.copyformatting.state
* @property {CKEDITOR.filter}
*/
this.filter = new CKEDITOR.filter( editor.config.copyFormatting_allowRules );
if ( editor.config.copyFormatting_allowRules === true ) {
this.filter.disabled = true;
}
if ( editor.config.copyFormatting_disallowRules ) {
this.filter.disallow( editor.config.copyFormatting_disallowRules );
}
}
/**
* Checks if copying and applying styles in the current context is possible.
* See {@link CKEDITOR.config#copyFormatting_allowedContexts} for the list of possible context values.
*
* @member CKEDITOR.plugins.copyformatting.state
* @param {String} testedContext Context name.
* @returns {Boolean} `true` if a given context is allowed in the current Copy Formatting instance.
* @private
*/
State.prototype._isContextAllowed = function( testedContext ) {
var configValue = this.editor.config.copyFormatting_allowedContexts;
return configValue === true || indexOf( configValue, testedContext ) !== -1;
};
CKEDITOR.event.implementOn( State.prototype );
/**
* @since 4.6.0
* @singleton
* @class CKEDITOR.plugins.copyformatting
*/
CKEDITOR.plugins.copyformatting = {
state: State,
/**
* An array of block boundaries that should be always transformed into inline elements with styles, e.g.
* `<div style="font-size: 24px;" class="important">` becomes `<span style="font-size: 24px;" class="important">`.
*
* @property {Array}
*/
inlineBoundary: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div' ],
/**
* An array of attributes that should be excluded from extracted styles.
*
* @property {Array}
*/
excludedAttributes: [ 'id', 'style', 'href', 'data-cke-saved-href', 'dir' ],
/**
* An array of elements that will be transformed into inline styles while
* applying formatting to the plain text context, e.g. trying to apply styles from the `<li>` element
* (`<li style="font-size: 24px;">`) to a regular paragraph will cause changing the `<li>` element
* into a corresponding `<span>` element (`<span style="font-size: 24px;">`).
*
* @property {Array}
*/
elementsForInlineTransform: [ 'li' ],
/**
* An array of elements that will be excluded from the transformation while
* applying formatting to the plain text context.
*
* @property {Array}
*/
excludedElementsFromInlineTransform: [ 'table', 'thead', 'tbody', 'ul', 'ol' ],
/**
* An array of attributes to be excluded while transforming styles from elements inside
* {@link CKEDITOR.plugins.copyformatting#elementsForInlineTransform} into `<span>` elements with styles
* (e.g. when applying these styles to text context).
*
* @property {Array}
*/
excludedAttributesFromInlineTransform: [ 'value', 'type' ],
/**
* An array of elements which should not be deleted when removing old styles
* from the current selection. Instead the styles are stripped from the elements,
* preserving the elements themselves, e.g. `<ul style="font-size: 24px" class="important">`
* becomes `<ul>`.
*
* @property {Array}
*/
preservedElements: [ 'ul', 'ol', 'li', 'td', 'th', 'tr', 'thead', 'tbody', 'table' ],
/**
* An array of elements on which extracting formatting should be stopped.
* If Copy Formatting reaches an element from the array, it ends going up the document tree
* and fetching the element parents' styles.
*
* @property {Array}
*/
breakOnElements: [ 'ul', 'ol', 'table' ],
commands: {
copyFormatting: {
exec: function( editor, data ) {
var cmd = this,
plugin = CKEDITOR.plugins.copyformatting,
copyFormatting = editor.copyFormatting,
isFromKeystroke = data ? data.from == 'keystrokeHandler' : false,
isSticky = data ? ( data.sticky || isFromKeystroke ) : false,
cursorContainer = plugin._getCursorContainer( editor ),
documentElement = CKEDITOR.document.getDocumentElement();
if ( cmd.state === CKEDITOR.TRISTATE_ON ) {
copyFormatting.styles = null;
copyFormatting.sticky = false;
cursorContainer.removeClass( 'cke_copyformatting_active' );
documentElement.removeClass( 'cke_copyformatting_disabled' );
documentElement.removeClass( 'cke_copyformatting_tableresize_cursor' );
plugin._putScreenReaderMessage( editor, 'canceled' );
return cmd.setState( CKEDITOR.TRISTATE_OFF );
}
copyFormatting.styles = plugin._extractStylesFromElement( editor,
editor.elementPath().lastElement );
cmd.setState( CKEDITOR.TRISTATE_ON );
if ( !isFromKeystroke ) {
cursorContainer.addClass( 'cke_copyformatting_active' );
documentElement.addClass( 'cke_copyformatting_tableresize_cursor' );
if ( editor.config.copyFormatting_outerCursor ) {
documentElement.addClass( 'cke_copyformatting_disabled' );
}
}
copyFormatting.sticky = isSticky;
plugin._putScreenReaderMessage( editor, 'copied' );
}
},
applyFormatting: {
exec: function( editor, data ) {
var cmd = editor.getCommand( 'copyFormatting' ),
isFromKeystroke = data ? data.from == 'keystrokeHandler' : false,
plugin = CKEDITOR.plugins.copyformatting,
copyFormatting = editor.copyFormatting,
cursorContainer = plugin._getCursorContainer( editor ),
documentElement = CKEDITOR.document.getDocumentElement(),
isApplied;
if ( !isFromKeystroke && cmd.state !== CKEDITOR.TRISTATE_ON ) {
return;
} else if ( isFromKeystroke && !copyFormatting.styles ) {
return plugin._putScreenReaderMessage( editor, 'failed' );
}
isApplied = plugin._applyFormat( editor, copyFormatting.styles );
if ( !copyFormatting.sticky ) {
copyFormatting.styles = null;
cursorContainer.removeClass( 'cke_copyformatting_active' );
documentElement.removeClass( 'cke_copyformatting_disabled' );
documentElement.removeClass( 'cke_copyformatting_tableresize_cursor' );
cmd.setState( CKEDITOR.TRISTATE_OFF );
}
plugin._putScreenReaderMessage( editor, isApplied ? 'applied' : 'canceled' );
}
}
},
/**
* Returns a container element where the mouse cursor should be overridden.
*
* @param {CKEDITOR.editor} editor The editor instance.
* @return {CKEDITOR.dom.element} For inline editor, it is the editable itself and for classic editor
* it is the document element of the editor iframe.
* @private
*/
_getCursorContainer: function( editor ) {
if ( editor.elementMode === CKEDITOR.ELEMENT_MODE_INLINE ) {
return editor.editable();
}
return editor.editable().getParent();
},
/**
* Converts a given element into a style definition that could be used to create an instance of {@link CKEDITOR.style}.
*
* Note that all definitions have a `type` property set to {@link CKEDITOR#STYLE_INLINE}.
*
* @param {CKEDITOR.dom.element} element The element to be converted.
* @returns {Object} The style definition created from the element.
* @private
*/
_convertElementToStyleDef: function( element ) {
var tools = CKEDITOR.tools,
attributes = element.getAttributes( CKEDITOR.plugins.copyformatting.excludedAttributes ),
styles = tools.parseCssText( element.getAttribute( 'style' ), true, true );
return {
element: element.getName(),
type: CKEDITOR.STYLE_INLINE,
attributes: attributes,
styles: styles
};
},
/**
* Extracts styles from the given element and its ancestors. This function walks up the document tree, starting from
* the given element, and ends on the editor's editable or when the element from
* {@link CKEDITOR.plugins.copyformatting#breakOnElements} is reached.
*
* @param {CKEDITOR.editor} editor The editor instance.
* @param {CKEDITOR.dom.element} element The element whose styles should be extracted.
* @returns {CKEDITOR.style[]} An array containing all extracted styles.
* @private
*/
_extractStylesFromElement: function( editor, element ) {
var eventData = {},
styles = [];
do {
// Skip all non-elements and bookmarks.
if ( element.type !== CKEDITOR.NODE_ELEMENT || element.hasAttribute( 'data-cke-bookmark' ) ) {
continue;
}
eventData.element = element;
if ( editor.copyFormatting.fire( 'extractFormatting', eventData, editor ) && eventData.styleDef ) {
styles.push( new CKEDITOR.style( eventData.styleDef ) );
}
// Break on list root.
if ( element.getName && indexOf( CKEDITOR.plugins.copyformatting.breakOnElements, element.getName() ) !== -1 ) {
break;
}
} while ( ( element = element.getParent() ) && element.type === CKEDITOR.NODE_ELEMENT );
return styles;
},
/**
* Extracts styles from the given range. This function finds all elements in the given range and then applies
* {@link CKEDITOR.plugins.copyformatting#_extractStylesFromElement} on them.
*
* @param {CKEDITOR.editor} editor The editor instance.
* @param {CKEDITOR.dom.range} range The range that styles should be extracted from.
* @returns {CKEDITOR.style[]} An array containing all extracted styles.
* @private
* @todo Styles in the array returned by this method might be duplicated; it should be cleaned later on.
*/
_extractStylesFromRange: function( editor, range ) {
var styles = [],
walker = new CKEDITOR.dom.walker( range ),
currentNode;
while ( ( currentNode = walker.next() ) ) {
styles = styles.concat(
CKEDITOR.plugins.copyformatting._extractStylesFromElement( editor, currentNode ) );
}
return styles;
},
/**
* Removes all styles from the element in a given range without
* removing the element itself.
*
* @param {CKEDITOR.dom.range} range The range where the element
* should be found.
* @param {String} element The tag name of the element.
* @private
*/
_removeStylesFromElementInRange: function( range, element ) {
// In case of lists, we want to remove styling only from the outer list.
var stopOnFirst = indexOf( [ 'ol', 'ul', 'table' ], element ) !== -1,
walker = new CKEDITOR.dom.walker( range ),
currentNode;
while ( ( currentNode = walker.next() ) ) {
currentNode = currentNode.getAscendant( element, true );
if ( currentNode ) {
currentNode.removeAttributes( currentNode.getAttributes() );
if ( stopOnFirst ) {
return;
}
}
}
},
/**
* Gets offsets as well as start and end containers for the selected word.
* It also handles cases like `lu<span style="color: #f00;">n</span>ar`.
*
* @param {CKEDITOR.dom.range} range Selected range.
* @returns {Object} return An object with the following properties:
* @returns {CKEDITOR.dom.element} return.startNode The node where the word's beginning is located.
* @returns {Number} return.startOffset The offset inside the `startNode` indicating the word's beginning.
* @returns {CKEDITOR.dom.element} return.endNode The node where the word's ending is located.
* @returns {Number} return.endOffset The offset inside the `endNode` indicating the word's ending.
* @private
*/
_getSelectedWordOffset: function( range ) {
var regex = /\b\w+\b/ig,
contents, match,
node, startNode, endNode,
startOffset, endOffset;
node = startNode = endNode = range.startContainer;
// Get sibling node, skipping the comments.
function getSibling( node, isPrev ) {
return node[ isPrev ? 'getPrevious' : 'getNext' ]( function( sibling ) {
// We must skip all comments.
return sibling.type !== CKEDITOR.NODE_COMMENT;
} );
}
// Get node contents without tags.
function getNodeContents( node ) {
var html;
// If the node is element, get its HTML and strip all tags and bookmarks
// and then search for word boundaries. In node.getText tags are
// replaced by spaces, which breaks getting the right offset.
if ( node.type == CKEDITOR.NODE_ELEMENT ) {
html = node.getHtml().replace( /<span.*?>&nbsp;<\/span>/g, '' );
return html.replace( /<.*?>/g, '' );
}
return node.getText();
}
// Get the word beggining/ending from previous/next node with content (skipping empty nodes and bookmarks)
function getSiblingNodeOffset( startNode, isPrev ) {
var currentNode = startNode,
regex = /\s/g,
boundaryElements = [ 'p', 'br', 'ol', 'ul', 'li', 'td', 'th', 'div', 'caption', 'body' ],
isBoundary = false,
isParent = false,
sibling, contents, match, offset;
do {
sibling = getSibling( currentNode, isPrev );
// If there is no sibling, text is probably inside element, so get it
// and then fetch its sibling.
while ( !sibling && currentNode.getParent() ) {
currentNode = currentNode.getParent();
// Check if the parent is a boundary.
if ( indexOf( boundaryElements, currentNode.getName() ) !== -1 ) {
isBoundary = true;
isParent = true;
break;
}
sibling = getSibling( currentNode, isPrev );
}
// Check if the fetched element is not a boundary.
if ( sibling && sibling.getName && indexOf( boundaryElements, sibling.getName() ) !== -1 ) {
isBoundary = true;
break;
}
currentNode = sibling;
} while ( currentNode && currentNode.getStyle &&
( currentNode.getStyle( 'display' ) == 'none' || !currentNode.getText() ) );
if ( !currentNode ) {
currentNode = startNode;
}
// If the node is an element, get its text child.
// In case of searching for the next node and reaching boundary (which is not parent),
// we must get the *last* text child.
while ( currentNode.type !== CKEDITOR.NODE_TEXT ) {
if ( isBoundary && !isPrev && !isParent ) {
currentNode = currentNode.getChild( currentNode.getChildCount() - 1 );
} else {
currentNode = currentNode.getChild( 0 );
}
}
contents = getNodeContents( currentNode );
while ( ( match = regex.exec( contents ) ) != null ) {
offset = match.index;
if ( !isPrev ) {
break;
}
}
// There is no space in fetched node and it's not a boundary node,
// so we must fetch one more node.
if ( typeof offset !== 'number' && !isBoundary ) {
return getSiblingNodeOffset( currentNode, isPrev );
}
// A little bit of math:
// * if we are searching for the beginning of the word and the word
// is located on the boundary of block element, set offset to 0.
// * if we are searching for the ending of the word and the word
// is located on the boundary of block element, set offset to
// the last occurrence of non-word character or node's length.
// * if we are searching for the beginning of the word, we must move the offset
// one character to the right (the space is located just before the word).
// * we must also ensure that the space is not located at the boundary of the node,
// otherwise we must return next node with appropriate offset.
if ( isBoundary ) {
if ( isPrev ) {
offset = 0;
} else {
regex = /([\.\b]*$)/;
match = regex.exec( contents );
offset = match ? match.index : contents.length;
}
} else if ( isPrev ) {
offset += 1;
if ( offset > contents.length ) {
return getSiblingNodeOffset( currentNode );
}
}
return {
node: currentNode,
offset: offset
};
}
contents = getNodeContents( node );
while ( ( match = regex.exec( contents ) ) != null ) {
if ( match.index + match[ 0 ].length >= range.startOffset ) {
startOffset = match.index;
endOffset = match.index + match[ 0 ].length;
// The word probably begins in previous node.
if ( match.index === 0 ) {
var startInfo = getSiblingNodeOffset( node, true );
startNode = startInfo.node;
startOffset = startInfo.offset;
}
// The word probably ends in next node.
if ( endOffset >= contents.length ) {
var endInfo = getSiblingNodeOffset( node );
endNode = endInfo.node;
endOffset = endInfo.offset;
}
return {
startNode: startNode,
startOffset: startOffset,
endNode: endNode,
endOffset: endOffset
};
}
}
return null;
},
/**
* Filters styles before applying them by using {@link CKEDITOR.filter}.
*
* @param {CKEDITOR.style[]} styles An array of styles to be filtered.
* @return {CKEDITOR.style[]} Filtered styles.
* @private
*/
_filterStyles: function( styles ) {
var isEmpty = CKEDITOR.tools.isEmpty,
filteredStyles = [],
styleDef,
i;
for ( i = 0; i < styles.length; i++ ) {
styleDef = styles[ i ]._.definition;
// Change element's name to span in case of inline boundary elements.
if ( CKEDITOR.tools.indexOf( CKEDITOR.plugins.copyformatting.inlineBoundary,
styleDef.element ) !== -1 ) {
styleDef.element = styles[ i ].element = 'span';
}
// We don't want to pick empty spans.
if ( styleDef.element === 'span' && isEmpty( styleDef.attributes ) && isEmpty( styleDef.styles ) ) {
continue;
}
filteredStyles.push( styles[ i ] );
}
return filteredStyles;
},
/**
* Determines the context of the given selection. See {@link CKEDITOR.config#copyFormatting_allowedContexts}
* for a list of possible context values.
*
* @param {CKEDITOR.dom.range} range The range that the context should be determined from.
* @returns {String}
* @private
*/
_determineContext: function( range ) {
function detect( query ) {
var walker = new CKEDITOR.dom.walker( range ),
currentNode;
// Walker sometimes does not include all nodes (e.g. if the range is in the middle of text node).
if ( range.startContainer.getAscendant( query, true ) || range.endContainer.getAscendant( query, true ) ) {
return true;
}
while ( ( currentNode = walker.next() ) ) {
if ( currentNode.getAscendant( query, true ) ) {
return true;
}
}
}
if ( detect( { ul: 1, ol: 1 } ) ) {
return 'list';
} else if ( detect( 'table' ) ) {
return 'table';
} else {
return 'text';
}
},
/**
* Applies styles inside the plain text context.
*
* @param {CKEDITOR.editor} editor The editor instance.
* @param {CKEDITOR.dom.range} range The range that the context can be determined from.
* @param {CKEDITOR.style[]} styles The styles to be applied.
* @private
*/
_applyStylesToTextContext: function( editor, range, styles ) {
var plugin = CKEDITOR.plugins.copyformatting,
attrsToExclude = plugin.excludedAttributesFromInlineTransform,
style,
i,
j;
// We must select initial range in WebKit. Otherwise WebKit has problems with applying styles:
// it collapses selection.
if ( CKEDITOR.env.webkit && !CKEDITOR.env.chrome ) {
editor.getSelection().selectRanges( [ range ] );
}
for ( i = 0; i < styles.length; i++ ) {
style = styles[ i ];
if ( indexOf( plugin.excludedElementsFromInlineTransform, style.element ) !== -1 ) {
continue;
}
if ( indexOf( plugin.elementsForInlineTransform, style.element ) !== -1 ) {
style.element = style._.definition.element = 'span';
for ( j = 0; j < attrsToExclude.length; j++ ) {
if ( style._.definition.attributes[ attrsToExclude[ j ] ] ) {
delete style._.definition.attributes[ attrsToExclude[ j ] ];
}
}
}
style.apply( editor );
}
},
/**
* Applies the list style inside the list context.
*
* @param {CKEDITOR.editor} editor The editor instance.
* @param {CKEDITOR.dom.range} range The range where the styles should be applied.
* @param {CKEDITOR.style[]} styles The style to be applied.
* @private
*/
_applyStylesToListContext: function( editor, range, styles ) {
var style,
bkm,
i;
function applyToList( list, style ) {
if ( list.getName() !== style.element ) {
list.renameNode( style.element );
}
style.applyToObject( list );
}
for ( i = 0; i < styles.length; i++ ) {
style = styles[ i ];
// The bookmark is used to prevent the weird behavior of lists (e.g. not converting list type
// while applying styles from bullet list to the numbered one). Restoring the selection to its
// initial state after every change seems to do the trick.
bkm = range.createBookmark();
if ( style.element === 'ol' || style.element === 'ul' ) {
getNodeAndApplyCmd( range, { ul: 1, ol: 1 }, function( currentNode ) {
applyToList( currentNode, style );
}, true );
} else if ( style.element === 'li' ) {
getNodeAndApplyCmd( range, 'li', function( currentNode ) {
style.applyToObject( currentNode );
} );
} else {
CKEDITOR.plugins.copyformatting._applyStylesToTextContext( editor, range, [ style ] );
}
range.moveToBookmark( bkm );
}
},
/**
* Applies the table style inside the table context.
*
* @param {CKEDITOR.editor} editor The editor instance.
* @param {CKEDITOR.dom.range} range The range where the styles should be applied.
* @param {CKEDITOR.style[]} styles The style to be applied.
* @private
*/
_applyStylesToTableContext: function( editor, range, styles ) {
var style,
bkm,
i;
function applyToTableCell( cell, style ) {
if ( cell.getName() !== style.element ) {
style = style.getDefinition();
style.element = cell.getName();
style = new CKEDITOR.style( style );
}
style.applyToObject( cell );
}
for ( i = 0; i < styles.length; i++ ) {
style = styles[ i ];
// The bookmark is used to prevent the weird behavior of tables (e.g. applying style to all cells
// instead of just selected cell). Restoring the selection to its initial state after every change
// seems to do the trick.
bkm = range.createBookmark();
if ( indexOf( [ 'table', 'tr' ], style.element ) !== -1 ) {
getNodeAndApplyCmd( range, style.element, function( currentNode ) {
style.applyToObject( currentNode );
} );
} else if ( indexOf( [ 'td', 'th' ], style.element ) !== -1 ) {
getNodeAndApplyCmd( range, { td: 1, th: 1 }, function( currentNode ) {
applyToTableCell( currentNode, style );
} );
} else if ( indexOf( [ 'thead', 'tbody' ], style.element ) !== -1 ) {
getNodeAndApplyCmd( range, { thead: 1, tbody: 1 }, function( currentNode ) {
applyToTableCell( currentNode, style );
} );
} else {
CKEDITOR.plugins.copyformatting._applyStylesToTextContext( editor, range, [ style ] );
}
range.moveToBookmark( bkm );
}
},
/**
* Initializes applying given styles to the currently selected content in the editor.
*
* The actual applying is performed inside event listeners for the
* {@link CKEDITOR.plugins.copyformatting.state#applyFormatting} event.
*
* @param {CKEDITOR.editor} editor The editor instance.
* @param {CKEDITOR.style[]} newStyles An array of styles to be applied.
* @returns {Boolean} `false` if styles could not be applied, `true` otherwise.
* @private
*/
_applyFormat: function( editor, newStyles ) {
var range = editor.getSelection().getRanges()[ 0 ],
plugin = CKEDITOR.plugins.copyformatting,
word,
bkms,
applyEvtData;
if ( !range ) {
return false;
}
if ( range.collapsed ) {
// Create bookmarks only if range is collapsed otherwise
// it will break walker used in _extractStylesFromRange.
bkms = editor.getSelection().createBookmarks();
if ( !( word = plugin._getSelectedWordOffset( range ) ) ) {
return;
}
range = editor.createRange();
range.setStart( word.startNode, word.startOffset );
range.setEnd( word.endNode, word.endOffset );
range.select();
}
newStyles = plugin._filterStyles( newStyles );
applyEvtData = { styles: newStyles, range: range, preventFormatStripping: false };
// Now apply new styles.
if ( !editor.copyFormatting.fire( 'applyFormatting', applyEvtData, editor ) ) {
return false;
}
if ( bkms ) {
editor.getSelection().selectBookmarks( bkms );
}
return true;
},
/**
* Puts a message solely for screen readers, meant to provide status updates for the Copy Formatting plugin.
*
* @param {CKEDITOR.editor} editor The editor instance.
* @param {string} msg The name of the message in the language file.
* @private
*/
_putScreenReaderMessage: function( editor, msg ) {
var container = this._getScreenReaderContainer();
if ( container ) {
container.setText( editor.lang.copyformatting.notification[ msg ] );
}
},
/**
* Adds the screen reader messages wrapper. Multiple calls will create only one message container.
*
* @private
* @returns {CKEDITOR.dom.element} Inserted `aria-live` container.
*/
_addScreenReaderContainer: function() {
if ( this._getScreenReaderContainer() ) {
return this._getScreenReaderContainer();
}
if ( CKEDITOR.env.ie6Compat || CKEDITOR.env.ie7Compat ) {
// Screen reader notifications are not supported on IE Quirks mode.
return;
}
// We can't use aria-live together with .cke_screen_reader_only class. Based on JAWS it won't read
// `aria-live` which has directly `position: absolute` assigned.
// The trick was simply to put position absolute, and all the hiding CSS into a wrapper,
// while content with `aria-live` attribute inside.
var notificationTpl = '<div class="cke_screen_reader_only cke_copyformatting_notification">' +
'<div aria-live="polite"></div>' +
'</div>';
return CKEDITOR.document.getBody().append( CKEDITOR.dom.element.createFromHtml( notificationTpl ) ).getChild( 0 );
},
/**
* Returns a screen reader messages wrapper.
*
* @private
* @returns
*/
_getScreenReaderContainer: function() {
if ( CKEDITOR.env.ie6Compat || CKEDITOR.env.ie7Compat ) {
// findOne is not supported on Quirks.
return;
}
return CKEDITOR.document.getBody().findOne( '.cke_copyformatting_notification div[aria-live]' );
}
};
/**
* Defines if the "disabled" cursor should be attached to the whole page
* when the Copy Formatting plugin is active.
*
* "Disabled" cursor indicates that Copy Formatting will not work in the place where the mouse cursor is placed.
*
* config.copyFormatting_outerCursor = false;
*
* Read more in the [documentation](#!/guide/dev_copyformatting)
* and see the [SDK sample](http://sdk.ckeditor.com/samples/copyformatting.html).
*
* @since 4.6.0
* @cfg [copyFormatting_outerCursor=true]
* @member CKEDITOR.config
*/
CKEDITOR.config.copyFormatting_outerCursor = true;
/**
* Defines rules for the elements from which the styles should be fetched. If set to `true`, it will disable
* filtering.
*
* This property is using Advanced Content Filter syntax. You can learn more about it in the
* [Content Filtering (ACF)](http://docs.ckeditor.com/#!/guide/dev_acf) documentation.
*
* config.copyFormatting_allowRules = 'span(*)[*]{*}'; // Allows only spans.
* config.copyFormatting_allowRules = true; // Disables filtering.
*
*
* Read more in the [documentation](#!/guide/dev_copyformatting)
* and see the [SDK sample](http://sdk.ckeditor.com/samples/copyformatting.html).
*
* @since 4.6.0
* @cfg [copyFormatting_allowRules='b; s; u; strong; span; p; div; table; thead; tbody; ' +
* 'tr; td; th; ol; ul; li; (*)[*]{*}']
* @member CKEDITOR.config
*/
CKEDITOR.config.copyFormatting_allowRules = 'b s u i em strong span p div td th ol ul li(*)[*]{*}';
/**
* Defines rules for the elements from which fetching styles is explicitly forbidden (eg. widgets).
*
* This property is using Advanced Content Filter syntax. You can learn more about it in the
* [Content Filtering (ACF)](http://docs.ckeditor.com/#!/guide/dev_acf) documentation.
*
* config.copyFormatting_disallowRules = 'span(important)'; // Disallows spans with "important" class.
*
*
* Read more in the [documentation](#!/guide/dev_copyformatting)
* and see the [SDK sample](http://sdk.ckeditor.com/samples/copyformatting.html).
*
* @since 4.6.0
* @cfg [copyFormatting_disallowRules='*[data-cke-widget*,data-widget*,data-cke-realelement](cke_widget*)']
* @member CKEDITOR.config
*/
CKEDITOR.config.copyFormatting_disallowRules = '*[data-cke-widget*,data-widget*,data-cke-realelement](cke_widget*)';
/**
* Defines which contexts should be enabled in the Copy Formatting plugin. Available contexts are:
*
* * `'text'` &ndash; Plain text context.
* * `'list'` &ndash; List context.
* * `'table'` &ndash; Table context.
*
* Examples:
*
* // Enables only plain text context.
* config.copyFormatting_allowedContexts = [ 'text' ];
*
* // If set to "true", enables all contexts.
* config.copyFormatting_allowedContexts = true;
*
* Read more in the [documentation](#!/guide/dev_copyformatting)
* and see the [SDK sample](http://sdk.ckeditor.com/samples/copyformatting.html).
*
* @since 4.6.0
* @cfg {Boolean/String[]} [copyFormatting_allowedContexts=true]
* @member CKEDITOR.config
*/
CKEDITOR.config.copyFormatting_allowedContexts = true;
/**
* Defines the keyboard shortcut for copying styles.
*
* config.copyFormatting_keystrokeCopy = CKEDITOR.CTRL + CKEDITOR.SHIFT + 66; // Ctrl+Shift+B
*
* The keyboard shortcut can also be switched off:
*
* config.copyFormatting_keystrokeCopy = false;
*
* Read more in the [documentation](#!/guide/dev_copyformatting)
* and see the [SDK sample](http://sdk.ckeditor.com/samples/copyformatting.html).
*
* @since 4.6.0
* @cfg {Number} [copyFormatting_keystrokeCopy=CKEDITOR.CTRL + CKEDITOR.SHIFT + 67]
* @member CKEDITOR.config
*/
CKEDITOR.config.copyFormatting_keystrokeCopy = CKEDITOR.CTRL + CKEDITOR.SHIFT + 67;
/**
* Defines the keyboard shortcut for applying styles.
*
* config.copyFormatting_keystrokePaste = CKEDITOR.CTRL + CKEDITOR.SHIFT + 77; // Ctrl+Shift+M
*
* The keyboard shortcut can also be switched off:
*
* config.copyFormatting_keystrokePaste = false;
*
* Read more in the [documentation](#!/guide/dev_copyformatting)
* and see the [SDK sample](http://sdk.ckeditor.com/samples/copyformatting.html).
*
* @since 4.6.0
* @cfg {Number} [copyFormatting_keystrokePaste=CKEDITOR.CTRL + CKEDITOR.SHIFT + 86]
* @member CKEDITOR.config
*/
CKEDITOR.config.copyFormatting_keystrokePaste = CKEDITOR.CTRL + CKEDITOR.SHIFT + 86;
/**
* Fired when the styles are being extracted from the element. This event is fired for each element separately.
* This event listener job is to extract inline styles from the element and modify them if needed.
*
* editor.copyFormatting.on( 'extractFormatting', function( evt ) {
* evt.data.styleDef.attributes.class = 'important';
* } );
*
* This event can also be canceled to indicate that styles from the current element should not
* be extracted.
*
* editor.copyFormatting.on( 'extractFormatting', function( evt ) {
* if ( evt.data.element === 'div' ) {
* evt.cancel();
* }
* } );
*
* This event has a default listener with a default priority of `10`.
* It extracts all styles from the element (from some of the attributes and from
* the element name) and puts them as an object into `evt.data.styleDef`.
*
* @event extractFormatting
* @member CKEDITOR.plugins.copyformatting.state
* @param {Object} data
* @param {CKEDITOR.dom.element} data.element The element whose styles should be fetched.
* @param {Object} data.styleDef Style definition extracted from the element.
*/
/**
* Fired when the copied styles are applied to the current selection position.
* This event listener job is to apply new styles.
*
* editor.copyFormatting.on( 'applyFormatting', function( evt ) {
* for ( var i = 0; i < evt.data.styles.length; i++ ) {
* evt.data.styles[ i ].apply( evt.editor );
* }
* }, null, null, 999 );
*
* By default this event has two listeners: the first one with a default priority of `10`
* and the second with a priority of `999`.
* The first one removes all preexisting styles from the Copy Formatting destination.
* The second one applies all new styles to the current selection.
*
* @event applyFormatting
* @member CKEDITOR.plugins.copyformatting.state
* @param {Object} data
* @param {CKEDITOR.dom.range} data.range The range from the current selection where styling should be applied.
* @param {CKEDITOR.style[]} data.styles The styles to be applied.
* @param {Boolean} [data.preventFormatStripping=false] If set to `true`, it will prevent stripping styles from
* the Copy Formatting destination range.
*/
} )();